362 lines
12 KiB
Zig
362 lines
12 KiB
Zig
// git repository at https://codeberg.org/Vulwsztyn/2048_zig
|
|
const std = @import("std");
|
|
const builtin = @import("builtin");
|
|
|
|
const Io = std.Io;
|
|
const Random = std.Random;
|
|
const fmt = std.fmt;
|
|
|
|
const mibu = @import("mibu");
|
|
const color = mibu.color;
|
|
const events = mibu.events;
|
|
const term = mibu.term;
|
|
const cursor = mibu.cursor;
|
|
|
|
// You can change these constants to modify the game behavior
|
|
const ROW_SIZE = 4; // program won't work if less than 2
|
|
const COL_SIZE = 4; // program won't work if less than 2
|
|
const TARGET_SCORE = 11; // 2^11 = 2048
|
|
const PROBABILITY_OF_FOUR_NUMERATOR = 10;
|
|
const PROBABILITY_OF_FOUR_DENOMINATOR = 100; // these 2 work in tandem to give chance of spawning a 4 instead of 2
|
|
|
|
const Dir = enum { up, down, left, right };
|
|
const Action = union(enum) {
|
|
Move: Dir,
|
|
Quit,
|
|
Noop,
|
|
};
|
|
|
|
fn spawn(comptime X: usize, comptime Y: usize, matrix_p: *[X][Y]u8, rand: Random) void {
|
|
var buffer: [X * Y]u8 = undefined;
|
|
var zero_indices = std.ArrayListUnmanaged(u8).initBuffer(&buffer);
|
|
for (matrix_p, 0..) |row, i| {
|
|
for (row, 0..) |val, j| {
|
|
if (val == 0) {
|
|
zero_indices.appendBounded(@intCast(i * Y + j)) catch unreachable;
|
|
}
|
|
}
|
|
}
|
|
const random_index = rand.intRangeAtMost(u8, 0, @intCast(zero_indices.items.len - 1));
|
|
const chosen = zero_indices.items[random_index];
|
|
const row = chosen / Y;
|
|
const col = chosen % Y;
|
|
const value: u8 = if (rand.intRangeAtMost(u8, 0, PROBABILITY_OF_FOUR_DENOMINATOR) < PROBABILITY_OF_FOUR_NUMERATOR) 2 else 1;
|
|
matrix_p[row][col] = value;
|
|
}
|
|
|
|
fn additives_for_previous(dir: Dir) [2]i8 {
|
|
// assuming that for a move in a direction, we look at the values at the end
|
|
// and check backwards
|
|
// e.g. for up, we look at the top row and check downwards
|
|
// so we return 1, 0 meaning one row down, same column
|
|
switch (dir) {
|
|
.up => {
|
|
return .{ 1, 0 };
|
|
},
|
|
.down => {
|
|
return .{ -1, 0 };
|
|
},
|
|
.left => {
|
|
return .{ 0, 1 };
|
|
},
|
|
.right => {
|
|
return .{ 0, -1 };
|
|
},
|
|
}
|
|
}
|
|
fn is_move_possible(comptime X: usize, comptime Y: usize, matrix_p: [X][Y]u8, dir: Dir) bool {
|
|
// checks if move is possible by checking if any tile can be moved or combined
|
|
const additives = additives_for_previous(dir);
|
|
for (0..X) |i| {
|
|
const i_signed: i8 = @intCast(i);
|
|
const new_i = i_signed + additives[0];
|
|
if (new_i < 0 or new_i >= X) continue;
|
|
for (0..Y) |j| {
|
|
const j_signed: i8 = @intCast(j);
|
|
const new_j = j_signed + additives[1];
|
|
if (new_j < 0 or new_j >= Y) continue;
|
|
const val = matrix_p[i][j];
|
|
const prev_val = matrix_p[@intCast(new_i)][@intCast(new_j)];
|
|
if (val != 0 and val == prev_val) return true;
|
|
if (val == 0 and prev_val != 0) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn is_game_lost(comptime X: usize, comptime Y: usize, matrix_p: [X][Y]u8) bool {
|
|
// checks if move in any direction is possible
|
|
for (std.meta.tags(Dir)) |dir| {
|
|
if (is_move_possible(X, Y, matrix_p, dir)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn is_game_won(comptime X: usize, comptime Y: usize, matrix_p: [X][Y]u8) bool {
|
|
for (matrix_p) |row| {
|
|
for (row) |val| {
|
|
if (val >= TARGET_SCORE) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn last_positions(comptime X: usize, comptime Y: usize, dir: Dir, gpa: std.mem.Allocator) std.ArrayList([2]u8) {
|
|
// see comment in additives_for_previous
|
|
// e.g. for move up we start at the top row and go downwards
|
|
// this function returns the list positions in the top row (for that scenario)
|
|
var list: std.ArrayList([2]u8) = .empty;
|
|
switch (dir) {
|
|
.up => {
|
|
for (0..Y) |i| {
|
|
list.append(gpa, .{ 0, @intCast(i) }) catch unreachable;
|
|
}
|
|
},
|
|
.down => {
|
|
for (0..Y) |i| {
|
|
list.append(gpa, .{ X - 1, @intCast(i) }) catch unreachable;
|
|
}
|
|
},
|
|
.left => {
|
|
for (0..X) |i| {
|
|
list.append(gpa, .{ @intCast(i), 0 }) catch unreachable;
|
|
}
|
|
},
|
|
.right => {
|
|
for (0..X) |i| {
|
|
list.append(gpa, .{ @intCast(i), X - 1 }) catch unreachable;
|
|
}
|
|
},
|
|
}
|
|
return list;
|
|
}
|
|
|
|
fn move_matrix(comptime X: usize, comptime Y: usize, matrix_p: *[X][Y]u8, dir: Dir) void {
|
|
const gpa = std.heap.page_allocator;
|
|
const additives = additives_for_previous(dir);
|
|
var postions = last_positions(X, Y, dir, gpa);
|
|
defer postions.deinit(gpa);
|
|
std.debug.print("\n", .{});
|
|
for (postions.items) |pos| {
|
|
var target_row: i8 = @intCast(pos[0]);
|
|
var target_col: i8 = @intCast(pos[1]);
|
|
var target_val: u8 = matrix_p[pos[0]][pos[1]];
|
|
var current_row: i8 = target_row;
|
|
var current_col: i8 = target_col;
|
|
while (true) {
|
|
current_row += additives[0];
|
|
current_col += additives[1];
|
|
const went_out_of_bounds = current_row < 0 or current_row >= (X) or
|
|
current_col < 0 or current_col >= (Y);
|
|
if (went_out_of_bounds) {
|
|
break;
|
|
}
|
|
const current_val: u8 = matrix_p[@intCast(current_row)][@intCast(current_col)];
|
|
if (current_val == 0) {
|
|
// nothing to do,
|
|
continue;
|
|
}
|
|
if (target_val != 0 and target_val != current_val) {
|
|
// cannot combine, move target forward
|
|
// e.g 0 0 2 4 (for was target, now 2 becomes target)
|
|
matrix_p[@intCast(current_row)][@intCast(current_col)] = 0;
|
|
target_row = (target_row) + (additives[0]);
|
|
target_col = (target_col) + (additives[1]);
|
|
matrix_p[@intCast(target_row)][@intCast(target_col)] = current_val;
|
|
target_val = matrix_p[@intCast(target_row)][@intCast(target_col)];
|
|
continue;
|
|
}
|
|
if (target_val == 0) {
|
|
// move current to target
|
|
// e.g 0 0 2 0 -> 0 0 0 2 (2 remains target, since we want 0 2 2 0 to become 0 0 0 4)
|
|
matrix_p[@intCast(target_row)][@intCast(target_col)] = current_val;
|
|
target_val = current_val;
|
|
} else {
|
|
// combine
|
|
// e.g 0 0 2 2 -> 0 0 0 4 (with rightmost zero becoming target, since the movements are not greedy)
|
|
matrix_p[@intCast(target_row)][@intCast(target_col)] = target_val + 1;
|
|
target_val = 0;
|
|
target_row = target_row + (additives[0]);
|
|
target_col = target_col + (additives[1]);
|
|
}
|
|
// both in case of move and combine we clear current
|
|
matrix_p[@intCast(current_row)][@intCast(current_col)] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn print(comptime X: usize, comptime Y: usize, matrix_p: [X][Y]u8, stdout: *Io.Writer) !void {
|
|
for (matrix_p, 0..) |row, i| {
|
|
try cursor.goTo(stdout, 1, 2 + i);
|
|
var buf: [100]u8 = undefined;
|
|
const all_together_slice = buf[0..];
|
|
for (row) |val| {
|
|
const value = if (val != 0) std.math.pow(u16, 2, @intCast(val)) else 0;
|
|
const value_as_str = if (val > 0) try fmt.bufPrint(all_together_slice, "{d}", .{value}) else "_";
|
|
|
|
try color.fg256(stdout, if (val > 1) @enumFromInt(val) else color.Color.white); // sets colour based on value
|
|
try stdout.print("{s:>5} ", .{value_as_str}); // prints with padding
|
|
}
|
|
try stdout.print("\x1b[K\n", .{});
|
|
}
|
|
}
|
|
|
|
fn init_matrix(comptime X: usize, comptime Y: usize, matrix_p: *[X][Y]u8) void {
|
|
// initializes matrix to all zeros
|
|
for (matrix_p) |*row| {
|
|
for (row) |*val| {
|
|
val.* = @intCast(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn action_from_event(next: events.Event) Action {
|
|
switch (next) {
|
|
.key => |k| switch (k.code) {
|
|
.char => |c| switch (c) {
|
|
'c' => {
|
|
return .Quit;
|
|
},
|
|
'q' => {
|
|
return .Quit;
|
|
},
|
|
'w' => {
|
|
return .{ .Move = .up };
|
|
},
|
|
's' => {
|
|
return .{ .Move = .down };
|
|
},
|
|
'a' => {
|
|
return .{ .Move = .left };
|
|
},
|
|
'd' => {
|
|
return .{ .Move = .right };
|
|
},
|
|
'h' => {
|
|
return .{ .Move = .left };
|
|
},
|
|
'j' => {
|
|
return .{ .Move = .down };
|
|
},
|
|
'k' => {
|
|
return .{ .Move = .up };
|
|
},
|
|
'l' => {
|
|
return .{ .Move = .right };
|
|
},
|
|
else => {
|
|
return .Noop;
|
|
},
|
|
},
|
|
.up => {
|
|
return .{ .Move = .up };
|
|
},
|
|
.down => {
|
|
return .{ .Move = .down };
|
|
},
|
|
.left => {
|
|
return .{ .Move = .left };
|
|
},
|
|
.right => {
|
|
return .{ .Move = .right };
|
|
},
|
|
else => {
|
|
return .Noop;
|
|
},
|
|
},
|
|
else => {
|
|
return .Noop;
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn main() !void {
|
|
const row_size = ROW_SIZE;
|
|
const col_size = COL_SIZE;
|
|
var seed: u64 = undefined;
|
|
try std.posix.getrandom(std.mem.asBytes(&seed));
|
|
|
|
var prng = Random.DefaultPrng.init(seed);
|
|
const rand = prng.random();
|
|
|
|
var stdout_buffer: [1024]u8 = undefined;
|
|
|
|
const stdin = std.fs.File.stdin();
|
|
var stdout_file = std.fs.File.stdout();
|
|
var stdout_writer = stdout_file.writer(&stdout_buffer);
|
|
const stdout = &stdout_writer.interface;
|
|
|
|
if (!std.posix.isatty(stdin.handle)) {
|
|
try stdout.print("The current file descriptor is not a referring to a terminal.\n", .{});
|
|
return;
|
|
}
|
|
|
|
if (builtin.os.tag == .windows) {
|
|
try mibu.enableWindowsVTS(stdout.handle);
|
|
}
|
|
|
|
// Enable terminal raw mode, its very recommended when listening for events
|
|
var raw_term = try term.enableRawMode(stdin.handle);
|
|
defer raw_term.disableRawMode() catch {
|
|
std.debug.print("Failed to disable raw mode\n", .{});
|
|
};
|
|
|
|
try term.enterAlternateScreen(stdout);
|
|
defer term.exitAlternateScreen(stdout) catch {
|
|
std.debug.print("Failed to exit alternate screen\n", .{});
|
|
};
|
|
|
|
try cursor.hide(stdout);
|
|
defer cursor.show(stdout) catch {
|
|
std.debug.print("Failed to show cursor\n", .{});
|
|
};
|
|
|
|
try cursor.goTo(stdout, 1, 1);
|
|
try mibu.style.italic(stdout, true);
|
|
|
|
var matrix: [row_size][col_size]u8 = undefined;
|
|
|
|
init_matrix(row_size, col_size, &matrix);
|
|
|
|
spawn(row_size, col_size, &matrix, rand);
|
|
try stdout.print("move with wsad, hjkl, or arrows; quit with c or q\x1b[K\n", .{}); // prints with padding
|
|
|
|
try print(row_size, col_size, matrix, stdout);
|
|
|
|
try stdout.flush();
|
|
|
|
while (true) {
|
|
try cursor.goTo(stdout, 1, 2);
|
|
const next = try events.next(stdin);
|
|
const action = action_from_event(next);
|
|
switch (action) {
|
|
.Quit => break,
|
|
.Noop => continue,
|
|
.Move => |dir| {
|
|
if (is_move_possible(row_size, col_size, matrix, dir)) {
|
|
move_matrix(row_size, col_size, &matrix, dir);
|
|
try print(row_size, col_size, matrix, stdout);
|
|
if (is_game_won(row_size, col_size, matrix)) {
|
|
try cursor.goTo(stdout, 1, row_size + 3);
|
|
try stdout.print("You won! Congratulations!\n", .{});
|
|
try stdout.flush();
|
|
break;
|
|
}
|
|
spawn(row_size, col_size, &matrix, rand);
|
|
try print(row_size, col_size, matrix, stdout);
|
|
if (is_game_lost(row_size, col_size, matrix)) {
|
|
try cursor.goTo(stdout, 1, (row_size + 3));
|
|
try stdout.print("Game over! No more moves possible!\n", .{});
|
|
try stdout.flush();
|
|
break;
|
|
}
|
|
}
|
|
try stdout.flush();
|
|
},
|
|
}
|
|
}
|
|
}
|