From cdd88973dca2929e6cdc67dad72df1bd51a97cce Mon Sep 17 00:00:00 2001 From: sirlilpanda Date: Mon, 2 Mar 2026 08:09:02 +1300 Subject: [PATCH] init --- .gitignore | 3 + README.md | 0 build.zig | 82 ++++++ build.zig.zon | 13 + c-src/terminal_linux.c | 27 ++ c-src/terminal_windows.c | 51 ++++ include/terminal.h | 10 + src/colour.zig | 199 ++++++++++++++ src/keys.zig | 546 +++++++++++++++++++++++++++++++++++++++ src/main.zig | 480 ++++++++++++++++++++++++++++++++++ src/root.zig | 23 ++ 11 files changed, 1434 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 c-src/terminal_linux.c create mode 100644 c-src/terminal_windows.c create mode 100644 include/terminal.h create mode 100644 src/colour.zig create mode 100644 src/keys.zig create mode 100644 src/main.zig create mode 100644 src/root.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f318f2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +log.log +.zig-cache/ +zig-out/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..35c481d --- /dev/null +++ b/build.zig @@ -0,0 +1,82 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const mod = b.addModule("terimal_tetris", .{ + .root_source_file = b.path("src/root.zig"), + .target = target, + }); + + const exe = b.addExecutable(.{ + .name = "terimal_tetris", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "terimal_tetris", .module = mod }, + }, + }), + }); + + b.installArtifact(exe); + + exe.addIncludePath(b.path("include")); + switch (builtin.os.tag) { + .windows => { + exe.addCSourceFiles(.{ + .files = &.{ + "c-src/terminal_windows.c", + }, + .flags = &.{ + "-lc", + "-w", + }, + }); + exe.linkLibC(); + }, + .linux, .macos => { + exe.addCSourceFiles(.{ + .files = &.{ + "c-src/terminal_linux.c", + }, + .flags = &.{ + "-lc", + "-w", + }, + }); + exe.linkLibC(); + }, + else => unreachable, + } + + const run_step = b.step("run", "Run the app"); + + const run_cmd = b.addRunArtifact(exe); + run_step.dependOn(&run_cmd.step); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const mod_tests = b.addTest(.{ + .root_module = mod, + }); + + const run_mod_tests = b.addRunArtifact(mod_tests); + + const exe_tests = b.addTest(.{ + .root_module = exe.root_module, + }); + + const run_exe_tests = b.addRunArtifact(exe_tests); + + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_mod_tests.step); + test_step.dependOn(&run_exe_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..a10442b --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,13 @@ +.{ + .name = .terimal_tetris, + .version = "0.0.0", + .fingerprint = 0xe0ee61f87efe174a, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/c-src/terminal_linux.c b/c-src/terminal_linux.c new file mode 100644 index 0000000..f737291 --- /dev/null +++ b/c-src/terminal_linux.c @@ -0,0 +1,27 @@ +#include "../include/terminal.h" + +#include +#include + +static struct termios orig_termios; + +void exit_raw_mode() { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); +} + +void enter_raw_mode() { + tcgetattr(STDIN_FILENO, &orig_termios); + // atexit(exit_raw_mode); // zig is handling this step + struct termios raw = orig_termios; + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + raw.c_oflag &= ~(OPOST); + raw.c_cflag |= (CS8); + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + + raw.c_cc[VMIN] = 0; + // raw.c_cc[VTIME] = 1; + + + tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); +} + diff --git a/c-src/terminal_windows.c b/c-src/terminal_windows.c new file mode 100644 index 0000000..c4926d3 --- /dev/null +++ b/c-src/terminal_windows.c @@ -0,0 +1,51 @@ +#include "../include/terminal.h" +#include +#include +#include +#include +#include + +#define DEFAULT_CONSOLE_MODE (\ + ENABLE_ECHO_INPUT |\ + ENABLE_LINE_INPUT |\ + ENABLE_MOUSE_INPUT |\ + ENABLE_PROCESSED_INPUT |\ + ENABLE_QUICK_EDIT_MODE |\ + ENABLE_WINDOW_INPUT |\ + ENABLE_PROCESSED_OUTPUT |\ + ENABLE_WRAP_AT_EOL_OUTPUT |\ + ENABLE_VIRTUAL_TERMINAL_PROCESSING |\ + DISABLE_NEWLINE_AUTO_RETURN |\ + ENABLE_LVB_GRID_WORLDWIDE) + +#define RAW_CONSOLE_MODE \ + ENABLE_WINDOW_INPUT |\ + ENABLE_VIRTUAL_TERMINAL_INPUT + // ENABLE_PROCESSED_INPUT |\ + +static DWORD mode = 0; + +void enter_raw_mode() { + + GetConsoleMode( + GetStdHandle(STD_INPUT_HANDLE), + &mode + ); + + SetConsoleMode( + GetStdHandle(STD_INPUT_HANDLE), + RAW_CONSOLE_MODE + ); + +} + +void exit_raw_mode() { + + SetConsoleMode( + GetStdHandle(STD_INPUT_HANDLE), + mode + ); + +} + + diff --git a/include/terminal.h b/include/terminal.h new file mode 100644 index 0000000..6025dcf --- /dev/null +++ b/include/terminal.h @@ -0,0 +1,10 @@ +#ifndef _TERMINAL_H_ +#define _TERMINAL_H_ + + +void enter_raw_mode(); + +void exit_raw_mode(); + + +#endif \ No newline at end of file diff --git a/src/colour.zig b/src/colour.zig new file mode 100644 index 0000000..b0c99e2 --- /dev/null +++ b/src/colour.zig @@ -0,0 +1,199 @@ +const std = @import("std"); + +/// colour type +pub const Colour = struct { + const Self = @This(); + + r: u8, + g: u8, + b: u8, + + /// inits the struct with a custom colour + pub fn custom(r: u8, g: u8, b: u8) Self { + return Self{ .r = r, .g = g, .b = b }; + } + + /// inits the struct with a red colour + pub fn red() Self { + return Self{ .r = 255, .g = 0, .b = 0 }; + } + + /// inits the struct with a orange colour + pub fn orange() Self { + return Self{ .r = 255, .g = 128, .b = 0 }; + } + + /// inits the struct with a yellow colour + pub fn yellow() Self { + return Self{ .r = 255, .g = 255, .b = 0 }; + } + + /// inits the struct with a gold colour + pub fn gold() Self { + return Self{ .r = 255, .g = 200, .b = 0 }; + } + + /// inits the struct with a lightGreen colour + pub fn lightGreen() Self { + return Self{ .r = 128, .g = 255, .b = 0 }; + } + + /// inits the struct with a green colour + pub fn green() Self { + return Self{ .r = 0, .g = 255, .b = 0 }; + } + + /// inits the struct with a cyan colour + pub fn cyan() Self { + return Self{ .r = 0, .g = 255, .b = 128 }; + } + + /// inits the struct with a turquoise colour + pub fn turquoise() Self { + return Self{ .r = 0, .g = 255, .b = 190 }; + } + + /// inits the struct with a lightBlue colour + pub fn lightBlue() Self { + return Self{ .r = 0, .g = 128, .b = 255 }; + } + + /// inits the struct with a blue colour + pub fn blue() Self { + return Self{ .r = 0, .g = 0, .b = 255 }; + } + + /// inits the struct with a darkPurple colour + pub fn darkPurple() Self { + return Self{ .r = 128, .g = 0, .b = 255 }; + } + + /// inits the struct with a purple colour + pub fn purple() Self { + return Self{ .r = 170, .g = 0, .b = 255 }; + } + + /// inits the struct with a lightPurple colour + pub fn lightPurple() Self { + return Self{ .r = 196, .g = 78, .b = 255 }; + } + + /// inits the struct with a magenta colour + pub fn magenta() Self { + return Self{ .r = 255, .g = 0, .b = 255 }; + } + + /// inits the struct with a pink colour + pub fn pink() Self { + return Self{ .r = 255, .g = 0, .b = 128 }; + } + + /// inits the struct with a peach colour + pub fn peach() Self { + return Self{ .r = 255, .g = 158, .b = 158 }; + } + + /// inits the struct with a black colour + pub fn black() Self { + return Self{ .r = 0, .g = 0, .b = 0 }; + } + + /// inits the struct with a white colour + pub fn white() Self { + return Self{ .r = 255, .g = 255, .b = 255 }; + } + + /// inits the struct with a grey colour + pub fn grey() Self { + return Self{ .r = 128, .g = 128, .b = 128 }; + } + + /// inits the struct with a greyScale colour + pub fn greyScale(scale: u8) Self { + // 255 max + return Self{ .r = scale, .g = scale, .b = scale }; + } + + /// this is the background colour of the defualt windows terminal + pub fn windowsTerminalBackground() Self { + return greyScale(12); + } + + /// this is the colour of the defualt windows terminal front + pub fn windowsTerminalFont() Self { + return greyScale(204); + } + + /// point must be an unsigned number type + pub fn pointToColour(point: anytype) Colour { + const normed_point_hue: u16 = @as(u16, @intFromFloat((@as(f32, @floatFromInt(point)) / @as(f32, @floatFromInt(std.math.maxInt(@TypeOf(point))))) * 255 * 5)) + 255; + const normed_point_rgb: u8 = @intCast((normed_point_hue + 1) % 256); + + if (normed_point_hue <= 255 * 2) { //count up + return custom(255, normed_point_rgb, 0); + } + // 0, 255, 0, 255*3 + if (normed_point_hue <= 255 * 3) { // count down + return custom(255 - normed_point_rgb, 255, 0); + } + // 0, 255, 255, 255*4 + if (normed_point_hue <= 255 * 4) { // count up + return custom(0, 255, normed_point_rgb); + } + // 0, 0, 255, 255*5 + if (normed_point_hue <= 255 * 5) { // count down + return custom(0, 255 - normed_point_rgb, 255); + } + // 255, 0, 255, 255*6 + if (normed_point_hue <= 255 * 6) { // count up + return custom(normed_point_rgb, 0, 255); + } + + return Colour.white(); + } + + pub fn rangeToColour(start: usize, end: usize, point: usize) Colour { + const delta = end - start; + const shifted_point: usize = point - start; + // wrapping mul to insure that this will never overflow + const normed_point_pos: usize = (std.math.maxInt(usize) / delta) *% shifted_point; + return pointToColour(normed_point_pos); + } + + /// this will return a set colour based on the usize number + /// how ever at at point it will start to be random + pub fn usizeToColour(number: usize) Self { + return switch (number) { + 0 => red(), + 1 => orange(), + 2 => yellow(), + 3 => gold(), + 4 => lightGreen(), + 5 => green(), + 6 => cyan(), + 7 => turquoise(), + 8 => lightBlue(), + 9 => blue(), + 10 => darkPurple(), + 11 => purple(), + 12 => lightPurple(), + 13 => magenta(), + 14 => pink(), + 15 => peach(), + else => random(), // just make a random colour + }; + } + + /// returns a random colour + pub fn random() Self { + const static = struct { + var prng = std.rand.DefaultPrng.init(69420); + }; + const rand = static.prng.random(); + return custom( + rand.int(u8), + rand.int(u8), + rand.int(u8), + ); + } +}; diff --git a/src/keys.zig b/src/keys.zig new file mode 100644 index 0000000..433844c --- /dev/null +++ b/src/keys.zig @@ -0,0 +1,546 @@ +const std = @import("std"); +const c = @cImport({ + @cInclude("ctype.h"); +}); + +const escape_char = 27; + +pub fn isControlChar(char: u8) bool { + return c.iscntrl(@as(c_int, char)) != 0; +} + +pub const Key = struct { + const Self = @This(); + + // home and end keys will be added + pub fn get(stdin: *std.Io.Reader) !?Key { + // if (self.poller.pollTimeout(100) catch false) { + // https://en.wikipedia.org/wiki/ANSI_escape_code#Terminal%20input%20sequences + // -> char + // -> esc + // -> esc + // -> Alt-keypress or keycode sequence + // '[' -> Alt-[ + // '[' () -> keycode sequence, is a decimal + // number and defaults to 1 (xterm) + // '[' 1 ; -> xterm + // '[' () (';') '~' -> keycode sequence, and + // are decimal numbers and default to 1 (vt) + + const char = try stdin.takeByte(); + var k: Key = Key{}; + + if (isControlChar(char)) { + k.escape = char; + } else { + k.keycode = char; + } + + // not part of an espace sequence + if (char != '\x1b') { + return k; + } + + k.escape = char; + + const char_2 = try stdin.takeByte(); + + // not part of an Alt-[ sequence + if (char_2 != '[' and char_2 != 'O') { + k.keycode = char_2; + return k; + } + + const char_3 = try stdin.takeByte(); + k.keycode = char_3; + + // bt function keys the strange one + if (char_2 == 'O' and char_3 >= 'P' and char_3 <= 'S') { + k.keycode = char_2; + k.keycode_2 = char_3; + return k; + } + + // strange vt function keys we do the only convertion here + std.debug.print("char2 {} {c}\r\n", .{ char_2, char_2 }); + std.debug.print("char3 {} {c}\r\n", .{ char_3, char_3 }); + + // xterm check + if (char_3 >= 'A' and char_3 <= 'O' or char_3 >= 'T' and char_3 <= 'Z') { + return k; + } + + const char_4 = try stdin.takeByte(); + std.debug.print("char_4 {}\r\n", .{char_4}); + + // modified xterm check + if (char_3 == '1' and char_4 == ';') { + const char_5 = try stdin.takeByte(); + const char_6 = try stdin.takeByte(); + k.modifier = char_5; + k.keycode = char_6; + return k; + } + + // xterm sequence + if (char_3 == 1 and char_4 >= 'P' and char_4 <= 'S') { + k.keycode_2 = char_4; + return k; + } + + if (char_4 == 0x1A) { + return k; + } + + // vt sequence with single keycode + if (char_4 == '~') { + return k; + } + + // vt sequence with multi keycode + if (char_3 >= '1' and char_3 <= '3' and char_4 != ';') { + k.keycode_2 = char_4; + const tilde = try stdin.takeByte(); + if (tilde != '~') { + return null; + } + return k; + } + + if (char_4 != ';') { + k.modifier = char_3; + k.keycode = char_4; + return k; + } + + if (char_4 == ';') { + const char_5 = try stdin.takeByte(); + _ = try stdin.takeByte(); + k.modifier = char_5; + k.keycode = char_3; + return k; + } + + return null; + } + + escape: u8 = 0, + keycode: u8 = 0, + // this fucker is only needed for 2 chars sequences + keycode_2: u8 = 0, + // Shift 1 + // (Left) Alt 2 + // ctrl 4 + // meta 8 + modifier: u8 = 0, + + pub fn eq(a: Self, b: Self) bool { + return (a.escape == b.escape and + a.keycode == b.keycode and + a.modifier == b.modifier); + } + + // make it 64 to make things nice + // this allows for switch cases to be used + pub fn keyToU64(self: Self) u64 { + return (@as(u64, self.escape) << 24) + + (@as(u64, self.modifier) << 16) + + (@as(u64, self.keycode_2) << 8) + + @as(u64, self.keycode); + } + + pub fn isRenderable(self: Self) bool { + return switch (self.escape) { + 0 => true, + 9 => true, + else => false, + }; + } + + pub fn toChar(self: Self) u8 { + return switch (self.escape) { + 0 => self.keycode, + 9 => '\t', + else => 0, + }; + } + + // pub fn format( + // self: Self, + // comptime fmt: []const u8, + // options: std.fmt.FormatOptions, + // writer: anytype, + // ) !void { + // _ = fmt; + // _ = options; + // // if (self.keycode_2 == 0) { + // // try writer.print( + // // "key{{esc={}, keycode={c}, mod={}}}", + // // .{ + // // self.escape, + // // self.keycode, + // // self.modifier, + // // }, + // // ); + // // } else { + // try writer.print( + // "key{{esc={}, keycode=|{c}|{}|, mod={}, id={}}}", + // .{ + // self.escape, + // self.keycode, + // self.keycode_2, + // self.modifier, + // self.keyToU64(), + // }, + // ); + // // } + // } +}; + +// normal seqs + +pub const Enter: Key = Key{ + .escape = 13, +}; + +pub const Backspace: Key = Key{ + .escape = 127, +}; + +// vt sequences: +pub const Home_0: Key = Key{ + .escape = escape_char, + .keycode = '1', +}; + +pub const Insert: Key = Key{ + .escape = escape_char, + .keycode = '2', +}; + +pub const Delete: Key = Key{ + .escape = escape_char, + .keycode = '3', +}; + +pub const End_0: Key = Key{ + .escape = escape_char, + .keycode = '4', +}; + +pub const PgUp: Key = Key{ + .escape = escape_char, + .keycode = '5', +}; + +pub const PgDn: Key = Key{ + .escape = escape_char, + .keycode = '6', +}; + +pub const Home_1: Key = Key{ + .escape = escape_char, + .keycode = '7', +}; + +pub const End_1: Key = Key{ + .escape = escape_char, + .keycode = '8', +}; + +pub const F0: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '0', +}; + +pub const F1: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '1', +}; + +pub const F2: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '2', +}; + +pub const F3: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '3', +}; + +pub const F4: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '4', +}; + +pub const F5: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '5', +}; + +pub const F6: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '7', +}; + +pub const F7: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '8', +}; + +pub const F8: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '9', +}; + +pub const F9: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '0', +}; + +pub const F10: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '1', +}; + +pub const F11: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '3', +}; + +pub const F12: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '4', +}; + +pub const F13: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '5', +}; + +pub const F14: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '6', +}; + +pub const F15: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '8', +}; + +pub const F16: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '9', +}; + +pub const F17: Key = Key{ + .escape = escape_char, + .keycode = '3', + .keycode_2 = '1', +}; + +pub const F18: Key = Key{ + .escape = escape_char, + .keycode = '3', + .keycode_2 = '2', +}; + +pub const F19: Key = Key{ + .escape = escape_char, + .keycode = '3', + .keycode_2 = '3', +}; + +pub const F20: Key = Key{ + .escape = escape_char, + .keycode = '3', + .keycode_2 = '4', +}; + +pub const vt_9: Key = Key{ + .escape = escape_char, + .keycode = '9', +}; + +pub const vt_16: Key = Key{ + .escape = escape_char, + .keycode = '1', + .keycode_2 = '6', +}; + +pub const vt_22: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '2', +}; + +pub const vt_27: Key = Key{ + .escape = escape_char, + .keycode = '2', + .keycode_2 = '7', +}; + +pub const vt_30: Key = Key{ + .escape = escape_char, + .keycode = '3', + .keycode_2 = '0', +}; + +pub const vt_35: Key = Key{ + .escape = escape_char, + .keycode = '3', + .keycode_2 = '5', +}; + +// xterm sequences: + +pub const Up = Key{ + .escape = escape_char, + .keycode = 'A', +}; + +pub const Down = Key{ + .escape = escape_char, + .keycode = 'B', +}; + +pub const Right = Key{ + .escape = escape_char, + .keycode = 'C', +}; + +pub const Left = Key{ + .escape = escape_char, + .keycode = 'D', +}; + +pub const xterm_End = Key{ + .escape = escape_char, + .keycode = 'F', +}; + +pub const Keypad_5 = Key{ + .escape = escape_char, + .keycode = 'G', +}; + +pub const xterm_Home = Key{ + .escape = escape_char, + .keycode = 'H', +}; + +pub const xterm_F1 = Key{ + .escape = escape_char, + .modifier = 1, + .keycode = 'P', +}; + +pub const xterm_F2 = Key{ + .escape = escape_char, + .modifier = 1, + .keycode = 'Q', +}; + +pub const xterm_F3 = Key{ + .escape = escape_char, + .modifier = 1, + .keycode = 'R', +}; + +pub const xterm_F4 = Key{ + .escape = escape_char, + .modifier = 1, + .keycode = 'S', +}; + +pub const xterm_E = Key{ + .escape = escape_char, + .keycode = 'E', +}; + +pub const xterm_I = Key{ + .escape = escape_char, + .keycode = 'I', +}; + +pub const xterm_J = Key{ + .escape = escape_char, + .keycode = 'J', +}; + +pub const xterm_K = Key{ + .escape = escape_char, + .keycode = 'K', +}; + +pub const xterm_L = Key{ + .escape = escape_char, + .keycode = 'L', +}; + +pub const xterm_M = Key{ + .escape = escape_char, + .keycode = 'M', +}; + +pub const xterm_N = Key{ + .escape = escape_char, + .keycode = 'N', +}; + +pub const xterm_O = Key{ + .escape = escape_char, + .keycode = 'O', +}; + +pub const xterm_T = Key{ + .escape = escape_char, + .keycode = 'T', +}; + +pub const xterm_U = Key{ + .escape = escape_char, + .keycode = 'U', +}; + +pub const xterm_V = Key{ + .escape = escape_char, + .keycode = 'V', +}; + +pub const xterm_W = Key{ + .escape = escape_char, + .keycode = 'W', +}; + +pub const xterm_X = Key{ + .escape = escape_char, + .keycode = 'X', +}; + +pub const xterm_Y = Key{ + .escape = escape_char, + .keycode = 'Y', +}; + +pub const xterm_Z = Key{ + .escape = escape_char, + .keycode = 'Z', +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..50b19a9 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,480 @@ +const std = @import("std"); +const terimal_tetris = @import("terimal_tetris"); +const Colour = @import("colour.zig").Colour; +const keys = @import("keys.zig"); + +const c = @cImport({ + @cInclude("terminal.h"); +}); + +const colour_set_string_fg_fmt = "\x1B[38;2;{d};{d};{d}m"; +const colour_set_string_bg_fmt = "\x1B[48;2;{d};{d};{d}m"; +const end_mode = "\x1b[m"; + +const height = 20; +const width = 10; + +const Cell = struct { + colour: Colour, + filled: bool = false, // determins if a give cell as something in it +}; + +const Tetromino = struct { + const Self = @This(); + + // this is needed for SRS rotations + // as j, l, s, z, piece can be rotated + // around a 3x3 grid but i need to be + // on a 4x4 grid and o should also be + // on this grid since the transform + // will be the same also i dont + is_4_piece: bool = false, // because it is 90% of the time + colour: Colour = .white(), + bitmap: [4][4]u1 = .{ + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, + }, + + pub fn i() Self { + return Self{ + .is_4_piece = true, + .colour = .cyan(), + .bitmap = .{ + .{ 0, 0, 0, 0 }, + .{ 1, 1, 1, 1 }, + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, + }, + }; + } + + pub fn t() Self { + return Self{ + .colour = .purple(), + .bitmap = .{ + .{ 0, 0, 0, 0 }, + .{ 1, 1, 1, 0 }, + .{ 0, 1, 0, 0 }, + .{ 0, 0, 0, 0 }, + }, + }; + } + + pub fn j() Self { + return Self{ + .colour = .blue(), + .bitmap = .{ + .{ 0, 0, 0, 0 }, + .{ 1, 1, 1, 0 }, + .{ 0, 0, 1, 0 }, + .{ 0, 0, 0, 0 }, + }, + }; + } + + pub fn l() Self { + return Self{ + .colour = .orange(), + .bitmap = .{ + .{ 0, 0, 0, 0 }, + .{ 1, 1, 1, 0 }, + .{ 1, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, + }, + }; + } + + pub fn s() Self { + return Self{ + .colour = .green(), + .bitmap = .{ + .{ 0, 0, 0, 0 }, + .{ 0, 1, 1, 0 }, + .{ 1, 1, 0, 0 }, + .{ 0, 0, 0, 0 }, + }, + }; + } + + pub fn z() Self { + return Self{ + .colour = .red(), + .bitmap = .{ + .{ 0, 0, 0, 0 }, + .{ 1, 1, 0, 0 }, + .{ 0, 1, 1, 0 }, + .{ 0, 0, 0, 0 }, + }, + }; + } + + pub fn o() Self { + return Self{ + .is_4_piece = true, + .colour = .yellow(), + .bitmap = .{ + .{ 0, 0, 0, 0 }, + .{ 0, 1, 1, 0 }, + .{ 0, 1, 1, 0 }, + .{ 0, 0, 0, 0 }, + }, + }; + } + + // uses SRS rotations + // index[r][c] + // 0 1 2 3 = c + // +----------- + // 0 | 1 2 3 4 + // 1 | 5 6 7 8 + // 2 | 9 10 11 12 + // 3 | 13 14 15 16 + // ║ + // r + // 3x3 + // 0 1 2 + // +------ + // 0 | 1 2 3 + // 1 | 4 5 6 + // 2 | 7 8 9 + // + pub fn rotate(self: *Self, clockwise: bool) void { + const b = self.bitmap; + + if (self.is_4_piece and clockwise) self.bitmap = .{ + .{ b[3][0], b[2][0], b[1][0], b[0][0] }, + .{ b[3][1], b[2][1], b[1][1], b[0][1] }, + .{ b[3][2], b[2][2], b[1][2], b[0][2] }, + .{ b[3][3], b[2][3], b[1][3], b[0][3] }, + }; + + if (self.is_4_piece and !clockwise) self.bitmap = .{ + .{ b[0][3], b[1][3], b[2][3], b[3][3] }, + .{ b[0][2], b[1][2], b[2][2], b[3][2] }, + .{ b[0][1], b[1][1], b[2][1], b[3][1] }, + .{ b[0][0], b[1][0], b[2][0], b[3][0] }, + }; + + if (!self.is_4_piece and clockwise) self.bitmap = .{ + .{ b[2][0], b[1][0], b[0][0], 0 }, + .{ b[2][1], b[1][1], b[0][1], 0 }, + .{ b[2][2], b[1][2], b[0][2], 0 }, + .{ 0, 0, 0, 0 }, + }; + + if (!self.is_4_piece and !clockwise) self.bitmap = .{ + .{ b[0][2], b[1][2], b[2][2], 0 }, + .{ b[0][1], b[1][1], b[2][1], 0 }, + .{ b[0][0], b[1][0], b[2][0], 0 }, + .{ 0, 0, 0, 0 }, + }; + } +}; + +const Board = struct { + const Self = @This(); + + grid: [height][width]Cell, + active_tetromino: Tetromino, + tetromino_pos_x: usize = @divTrunc(width, 2) + 2, + tetromino_pos_y: usize = height - 1, + stop: bool = false, + random: std.Random, + + pub fn init() Self { + var prng: std.Random.DefaultPrng = .init(@as(usize, @intCast(std.time.timestamp()))); + const rand = prng.random(); + + var self = Self{ + .random = rand, + .active_tetromino = undefined, + .grid = undefined, + }; + + self.clearGrid(); + self.resetActiveTetromino(); + + return self; + } + + pub fn resetActiveTetromino(self: *Self) void { + self.tetromino_pos_x = @divTrunc(width, 2) + 2; + self.tetromino_pos_x = 0; + self.tetromino_pos_y = height; + self.active_tetromino = self.newTetromino(); + } + + pub fn newTetromino(self: Self) Tetromino { + const minos = [_]Tetromino{ + Tetromino.i(), + Tetromino.t(), + Tetromino.j(), + Tetromino.l(), + Tetromino.s(), + Tetromino.z(), + Tetromino.o(), + }; + _ = self; + // return minos[@mod(self.random.int(usize), 7)]; + return minos[0]; + } + + pub fn clearGrid(self: *Self) void { + for (0..height) |y| { + for (0..width) |x| { + self.grid[y][x] = Cell{ + .colour = .black(), + .filled = false, + }; + } + } + } + + pub fn collided(self: *Self, mino: Tetromino) bool { + for (mino.bitmap, 0..) |mino_row, y| { + for (mino_row, 0..) |mino_cell, x| { + const mino_x = self.tetromino_pos_x + x; + const mino_y = height -| self.tetromino_pos_y + y; + if (mino_cell == 0) continue; + std.debug.print("mino : x {}, y {}\n", .{ mino_x, mino_y }); + + // make sure that the empty cells dont go out of bounds + if (mino_x >= width and mino_cell == 0) continue; + + // hit the bottom + if (mino_y == height - 1) // because of indexing + return true; + + // debug check to see where mino is + if (mino_cell == 1) { + self.grid[mino_y][mino_x] = Cell{ + .colour = .white(), + .filled = false, + }; + } + if (self.grid[mino_y + 1][mino_x].filled) + return true; + } + } + return false; + } + + pub fn setGridCells(self: *Self, mino: Tetromino) void { + std.debug.print("========== SETTING GRID CELLS ==========", .{}); + for (mino.bitmap, 0..) |mino_row, y| { + for (mino_row, 0..) |mino_cell, x| { + const mino_x = self.tetromino_pos_x + x; + const mino_y = height -| self.tetromino_pos_y + y; + + if (mino_cell == 1) { + self.grid[mino_y][mino_x] = Cell{ + .colour = mino.colour, + .filled = true, + }; + } + } + } + } + + // returns if the given tetromino is out of bound from the current board + // the values are the amount x and amount y the mino is out of bounds + pub fn outOfBounds(self: Self, mino: Tetromino) struct { isize, isize } {} + + pub fn processInputs(self: *Self, inputs: *std.Io.Reader) void { + const key = getKey(inputs) catch |err| switch (err) { + error.EndOfStream => return, + else => { + self.stop = true; + return; + }, + }; + if (key == null) return; + + std.debug.print("key : {?}\n", .{key}); + + const exit_key = comptime (keys.Key{ .escape = 17 }).keyToU64(); + + switch (key.?.keyToU64()) { + exit_key => self.stop = true, + keys.Left.keyToU64() => self.tetromino_pos_x -|= 1, + keys.Right.keyToU64() => { + self.tetromino_pos_x += 1; + // width - 4 since the minos are 4 wide + if (self.tetromino_pos_x + 1 > width - 4) { + self.tetromino_pos_x = width - 4; + } + }, + keys.Down.keyToU64() => self.tetromino_pos_y -|= 1, + (keys.Key{ .keycode = 'r' }).keyToU64() => self.active_tetromino.rotate(true), + else => {}, + } + } + + pub fn step(self: *Self) void { + + // if (self.tetromino_pos_y == 0) {} + const active_mino_collided = self.collided(self.active_tetromino); + if (active_mino_collided) { + self.setGridCells(self.active_tetromino); + self.resetActiveTetromino(); + } + + self.tetromino_pos_y -|= 1; + } + + pub fn enterRawMode() void { + c.enter_raw_mode(); + } + + pub fn exitRawMode() void { + c.exit_raw_mode(); + } + + pub fn getKey(stdin: *std.Io.Reader) !?keys.Key { + return keys.Key.get(stdin); + } + + pub fn clearScreen(writer: *std.Io.Writer) !void { + _ = try writer.write("\x1b[2J"); + } + + pub fn resetCursor(writer: *std.Io.Writer) !void { + try moveCursorTo(writer, 0, 0); + } + + pub fn moveCursorTo(writer: *std.Io.Writer, x: usize, y: usize) !void { + try writer.print("\x1b[{};{}H", .{ y + 1, x + 1 }); + } + + pub fn drawTeromino( + mino: Tetromino, + x_offset: usize, + y_offset: usize, + writer: *std.Io.Writer, + ) !void { + try moveCursorTo( + writer, + x_offset, + y_offset, + ); + _ = try writer.write("@"); + for (mino.bitmap, 0..) |row, y| { + for (row, 0..) |bit, x| { + if (bit == 1) { + try moveCursorTo( + writer, + ((x_offset) + x) * 2 - 1, // mul 2 since the cell is 2 chars wide + y_offset + y, + ); + + try writer.print( + colour_set_string_fg_fmt ++ "██" ++ end_mode, + .{ + mino.colour.r, + mino.colour.b, + mino.colour.g, + }, + ); + } + } + } + } + + pub fn draw(self: Self, writer: *std.Io.Writer) !void { + try resetCursor(writer); + try clearScreen(writer); + + // ======== draw top bar ======== + _ = try writer.write("\n\r╔"); + for (0..height) |_| { + _ = try writer.write("═"); + } + _ = try writer.write("╗\n\r"); + // ======== draw top bar ======== + + // ======== draw play space ======== + + for (self.grid) |row| { + _ = try writer.write("║"); + // try writer.print("{} ║", .{i}); + for (row) |cell| { + if (cell.filled) { + try writer.print( + colour_set_string_fg_fmt ++ "██" ++ end_mode, + .{ + cell.colour.r, cell.colour.b, cell.colour.g, + }, + ); + } else { + _ = try writer.write(" "); + } + } + _ = try writer.write("║\n\r"); + } + // ======== draw play space ======== + + // ======== draw bottom bar ======== + _ = try writer.write("╚"); + for (0..height) |_| { + _ = try writer.write("═"); + } + _ = try writer.write("╝\n\r"); + // ======== draw bottom bar ======== + + // ======== draw debug info ======== + + try writer.print("x : {}, y : {}\n\r", .{ + self.tetromino_pos_x, + self.tetromino_pos_y, + }); + + // ======== draw debug info ======== + + // ======== draw tetromino ======== + try drawTeromino( + self.active_tetromino, + self.tetromino_pos_x + 1, + height -| self.tetromino_pos_y + 1, + writer, + ); + // ======== draw tetromino ======== + try resetCursor(writer); + + try writer.flush(); + } +}; + +// we wrap the game to make sure/hope we can exit out of rawmode if something occurs +pub fn gameMain(stdout: *std.Io.Writer, stdin: *std.Io.Reader) !void { + var b = Board.init(); + Board.enterRawMode(); + + while (!b.stop) { + const current_time = std.time.microTimestamp() + 1 * 500000; + while (std.time.microTimestamp() < current_time) continue; + // b.active_tetromino.rotate(true); + b.processInputs(stdin); + b.step(); + try b.draw(stdout); + } + + Board.exitRawMode(); +} + +pub fn main() !void { + var stdout_buffer: [1024]u8 = undefined; + var stdin_buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + var stdin_reader = std.fs.File.stdin().reader(&stdin_buffer); + + const stdout = &stdout_writer.interface; + const stdin = &stdin_reader.interface; + + gameMain(stdout, stdin) catch |e| { + Board.exitRawMode(); + try Board.clearScreen(stdout); + try stdout.print("game exit got error {any}\n", .{e}); + }; +} diff --git a/src/root.zig b/src/root.zig new file mode 100644 index 0000000..94c7cd0 --- /dev/null +++ b/src/root.zig @@ -0,0 +1,23 @@ +//! By convention, root.zig is the root source file when making a library. +const std = @import("std"); + +pub fn bufferedPrint() !void { + // Stdout is for the actual output of your application, for example if you + // are implementing gzip, then only the compressed bytes should be sent to + // stdout, not any debugging messages. + var stdout_buffer: [1024]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer); + const stdout = &stdout_writer.interface; + + try stdout.print("Run `zig build test` to run the tests.\n", .{}); + + try stdout.flush(); // Don't forget to flush! +} + +pub fn add(a: i32, b: i32) i32 { + return a + b; +} + +test "basic add functionality" { + try std.testing.expect(add(3, 7) == 10); +}