From 9035209472837d943ac098bc4b7e8e3bd37621cc Mon Sep 17 00:00:00 2001 From: TheDevConnor Date: Sat, 18 Oct 2025 01:23:41 -0400 Subject: [PATCH 1/2] Updated the terminal.lx file to now include a raw mode --- c_test_files/tetris.c | 44 ++------- std/terminal.lx | 87 +++++++++++++++- tests/terminal_test.lx | 220 +++++++++++++++++++++++++++++++++++------ 3 files changed, 281 insertions(+), 70 deletions(-) diff --git a/c_test_files/tetris.c b/c_test_files/tetris.c index 571970f2..cb55b41d 100644 --- a/c_test_files/tetris.c +++ b/c_test_files/tetris.c @@ -51,40 +51,13 @@ int readch() { // Tetromino definitions (7 pieces, each as 4x4 block string) const char *tetromino[7] = { - "...." - "XXXX" - "...." - "....", // I - - ".X.." - ".X.." - ".XX." - "....", // J - - "..X." - "..X." - ".XX." - "....", // L - - ".XX." - ".XX." - "...." - "....", // O - - ".XX." - "XX.." - "...." - "....", // S - - ".X.." - ".XXX" - "...." - "....", // T - - "XX.." - ".XX." - "...." - "...." // Z + "....XXXX........", // I + ".X...X...XX.....", // J + "..X...X..XX.....", // L + ".XX..XX.........", // O + ".XX.XX..........", // S + ".X...XXX........", // T + "XX...XX........." // Z }; // ANSI color codes for each piece @@ -153,8 +126,7 @@ void draw_screen(int currentPiece, int currentRotation, int currentX, // Draw header with box drawing characters printf("\x1b[0m"); printf("╔════════════════════════════╗ ╔═══════════════╗\n"); - printf("║ \x1b[1;97mT E T R I S\x1b[0m ║ ║ \x1b[1mNEXT " - "PIECE\x1b[0m ║\n"); + printf("║ \x1b[1;97mT E T R I S\x1b[0m ║ ║ \x1b[1mNEXT PIECE\x1b[0m ║\n"); printf("╠════════════════════════════╣ ║ ║\n"); // Draw visible area with next piece preview diff --git a/std/terminal.lx b/std/terminal.lx index 6e611fb8..162d65d8 100644 --- a/std/terminal.lx +++ b/std/terminal.lx @@ -1,6 +1,64 @@ @module "terminal" -// @use "string" as string +@use "string" as string +@use "termfx" as fx + +// Global flag to track raw mode state +let raw_mode_enabled: int = 0; + +// Enable raw mode - BLOCKING (waits for input) +pub const enable_raw_mode -> fn () void { + if (raw_mode_enabled == 1) { + return; // Already enabled + } + + // Save terminal state and set up restoration on exit + system("stty -g > /tmp/luma_termios_backup"); + + // Configure raw mode: + // -icanon: disable line buffering (read char by char) + // -echo: don't echo input + // -isig: disable interrupt signals (Ctrl+C, Ctrl+Z) + // -ixon: disable software flow control (Ctrl+S, Ctrl+Q) + // -icrnl: don't translate CR to NL + // min 1 time 0: blocking read (wait for at least 1 character) + system("stty -icanon -echo -isig -ixon -icrnl min 1 time 0"); + + // Hide cursor + output(fx::CURSOR_HIDE); + + raw_mode_enabled = 1; +} + +// Disable raw mode - restore terminal to normal +pub const disable_raw_mode -> fn () void { + if (raw_mode_enabled == 0) { + return; // Already disabled + } + + // Restore original terminal settings + system("stty $(cat /tmp/luma_termios_backup)"); + system("rm /tmp/luma_termios_backup"); + + // Show cursor + output(fx::CURSOR_SHOW); + + // Reset colors + output(fx::RESET); + + raw_mode_enabled = 0; +} + +// Blocking input in raw mode - waits for a key but no Enter needed +// Use with enable_raw_mode() (not enable_raw_mode_nonblock) +pub const getch_raw -> fn () char { + if (raw_mode_enabled == 0) { + enable_raw_mode(); + } + + let c: char = input(""); + return c; +} // Get a single character without waiting for Enter pub const getch -> fn () char { @@ -35,10 +93,18 @@ pub const getche -> fn () char { } // Check if a key is pressed (non-blocking) +// WARNING: This consumes the character! Don't use in raw mode. pub const kbhit -> fn () int { + // Save current state + system("stty -g > /tmp/luma_kbhit_backup"); + + // Set non-blocking system("stty -icanon -echo min 0 time 0"); let c: char = input(""); - system("stty icanon echo"); + + // Restore previous state + system("stty $(cat /tmp/luma_kbhit_backup)"); + system("rm /tmp/luma_kbhit_backup"); if (c == cast(0)) { return 0; @@ -110,3 +176,20 @@ pub const getpass -> fn (prompt: *char) *char { return password; } + +// Sleep for milliseconds (more precise than system("sleep")) +pub const sleep_ms -> fn (ms: int) void { + // Convert to seconds string + let seconds: *char = string::from_int(ms); + defer free(seconds); + + // TODO: Better implementation with usleep or nanosleep + system("sleep 0.001"); // Placeholder +} + +// Get terminal size +pub const get_terminal_size -> fn () void { + system("tput cols > /tmp/luma_cols"); + system("tput lines > /tmp/luma_lines"); + // Read back from files... +} \ No newline at end of file diff --git a/tests/terminal_test.lx b/tests/terminal_test.lx index 05b685e8..e37bce4b 100644 --- a/tests/terminal_test.lx +++ b/tests/terminal_test.lx @@ -86,6 +86,159 @@ const reaction_game -> fn () void { } } +const raw_mode_demo -> fn () void { + output(fx::CLEAR_SCREEN, fx::CURSOR_HOME); + output(fx::BOLD, fx::BRIGHT_MAGENTA, "=== Raw Mode Demo ===\n", fx::RESET); + output("Press keys immediately without Enter.\n"); + output("Press Q to exit.\n\n"); + + term::enable_raw_mode(); + defer { term::disable_raw_mode(); } + + let running: int = 1; + + loop (running == 1) { + let key: char = term::getch_raw(); + + output("Key: '", string::from_char(key), + "' (ASCII: ", cast(key), ")\n"); + + if (key == 'q' || key == 'Q') { + running = 0; + } + } + + output(fx::BRIGHT_CYAN, "\nExiting...\n", fx::RESET); +} + +const moving_character_demo -> fn () void { + // Enable raw mode + term::enable_raw_mode(); + defer { term::disable_raw_mode(); } + + // Character position + let x: int = 20; + let y: int = 10; + let running: int = 1; + + // Initial draw + output(fx::CLEAR_SCREEN, fx::CURSOR_HOME); + + loop (running == 1) { + // Draw screen at home position (overwrites previous) + output(fx::CURSOR_HOME); + output(fx::BOLD, fx::BRIGHT_YELLOW, "=== Moving Character Demo ===\n", fx::RESET); + output("Position: (", x, ", ", y, ") \n"); + output("Use WASD to move, Q to quit.\n\n"); + + // Draw border + output(fx::BRIGHT_WHITE, "┌"); + loop [i: int = 0](i < 40) : (++i) { output("─"); } + output("┐\n", fx::RESET); + + // Draw play area (rows 0-19, so y from 0 to 19) + loop [row: int = 0](row < 20) : (++row) { + output(fx::BRIGHT_WHITE, "│", fx::RESET); + + loop [col: int = 0](col < 40) : (++col) { + if (row == y && col == x) { + output(fx::BRIGHT_GREEN, "◉", fx::RESET); + } else { + output(" "); + } + } + + output(fx::BRIGHT_WHITE, "│\n", fx::RESET); + } + + // Bottom border + output(fx::BRIGHT_WHITE, "└"); + loop [i: int = 0](i < 40) : (++i) { output("─"); } + output("┘\n", fx::RESET); + + // Wait for input (blocking) + let key: char = term::getch_raw(); + + // Handle movement with boundary checks + switch (key) { + 'w', 'W' -> if (y > 0) { y = y - 1; } + 's', 'S' -> if (y < 19) { y = y + 1; } + 'a', 'A' -> if (x > 0) { x = x - 1; } + 'd', 'D' -> if (x < 39) { x = x + 1; } + 'q', 'Q' -> running = 0; + } + } + + output(fx::CLEAR_SCREEN, fx::CURSOR_HOME); + output(fx::BRIGHT_CYAN, "Thanks for playing!\n", fx::RESET); +} + +const typing_speed_demo -> fn () void { + output(fx::CLEAR_SCREEN, fx::CURSOR_HOME); + output(fx::BOLD, fx::BRIGHT_BLUE, "=== Typing Speed Test ===\n", fx::RESET); + + let target: *char = "The quick brown fox jumps over the lazy dog"; + output("Type this: ", fx::BRIGHT_YELLOW, target, fx::RESET, "\n\n", fx::CURSOR_SHOW); + output("Your input: "); + + // Enable raw mode for immediate feedback + term::enable_raw_mode(); + defer { term::disable_raw_mode(); } + + let typed: *char = cast<*char>(alloc(100)); + defer { free(typed); } + + let index: int = 0; + let target_len: int = string::strlen(target); + let correct: int = 0; + let wrong: int = 0; + + loop (index < target_len) { + let key: char = term::getch_raw(); // Blocking in raw mode + + // Handle backspace + if (key == cast(127) || key == cast(8)) { + if (index > 0) { + // Check if we need to undo a correct or wrong count + if (typed[index - 1] == target[index - 1]) { + correct = correct - 1; + } else { + wrong = wrong - 1; + } + + index = index - 1; + output("\b\b"); // Erase character visually + } + continue; + } + + // Store typed character + typed[index] = key; + + // Check if correct + if (key == target[index]) { + output(fx::GREEN, string::from_char(key), fx::RESET); + correct = correct + 1; + } else { + output(fx::RED, string::from_char(key), fx::RESET); + wrong = wrong + 1; + } + + index = index + 1; + } + + typed[index] = cast(0); // Null terminate + + // Show results + output("\n\n"); + output(fx::CURSOR_HIDE, fx::BOLD, "Results:\n", fx::RESET); + output(fx::GREEN, "✓ Correct: ", correct, "\n", fx::RESET); + output(fx::RED, "✗ Wrong: ", wrong, "\n", fx::RESET); + + let accuracy: int = (correct * 100) / target_len; + output(fx::BRIGHT_CYAN, "Accuracy: ", accuracy, "%\n", fx::RESET); +} + const yes_no_prompt -> fn (question: *char) int { output(question, " (y/n): "); @@ -93,12 +246,12 @@ const yes_no_prompt -> fn (question: *char) int { let c: char = term::getch(); output(string::from_char(c), "\n"); - if (c == 'y' || c == 'Y') { - return 1; - } elif (c == 'n' || c == 'N') { - return 0; - } else { + switch (c) { + 'y', 'Y' -> return 1; + 'n', 'N' -> return 0; + _ -> { output(fx::YELLOW, "Please press 'y' or 'n': ", fx::RESET); + } } } @@ -112,38 +265,41 @@ pub const main -> fn () int { output(fx::BOLD, fx::BRIGHT_CYAN, "║ Interactive Input Demos ║\n", fx::RESET); output(fx::BOLD, fx::BRIGHT_CYAN, "╚════════════════════════════╝\n\n", fx::RESET); - output("1. Menu Demo\n"); - output("2. Password Input Demo\n"); - output("3. Keystroke Demo\n"); - output("4. Reaction Game\n"); - output("5. Exit\n\n"); + output("Basic Demos:\n"); + output(" 1. Menu Demo\n"); + output(" 2. Password Input Demo\n"); + output(" 3. Keystroke Demo\n"); + output(" 4. Reaction Game\n\n"); + output(fx::BRIGHT_MAGENTA, "Raw Mode Demos (NEW!):\n", fx::RESET); + output(" 5. Raw Mode Demo\n"); + output(" 6. Moving Character Game\n"); + output(" 7. Typing Speed Test\n\n"); + output(" 8. Exit\n\n"); output("Choose: "); let choice: char = term::getch(); output(string::from_char(choice), "\n\n"); - - if (choice == '1') { - menu_demo(); - term::wait_for_key(); - } elif (choice == '2') { - password_demo(); - term::wait_for_key(); - } elif (choice == '3') { - keystroke_demo(); - term::wait_for_key(); - } elif (choice == '4') { - reaction_game(); - term::wait_for_key(); - } elif (choice == '5') { - if (yes_no_prompt("Are you sure you want to exit?") == 1) { - output(fx::BRIGHT_YELLOW, "Goodbye!\n", fx::RESET); - break; + + switch(choice) { + '1' -> { menu_demo(); term::wait_for_key(); } + '2' -> { password_demo(); term::wait_for_key(); } + '3' -> { keystroke_demo(); term::wait_for_key(); } + '4' -> { reaction_game(); term::wait_for_key(); } + '5' -> { raw_mode_demo(); term::wait_for_key(); } + '6' -> { moving_character_demo(); term::wait_for_key(); } + '7' -> { typing_speed_demo(); term::wait_for_key(); } + '8' -> { + if (yes_no_prompt("Are you sure you want to exit?") == 1) { + output(fx::BRIGHT_YELLOW, "Goodbye!\n", fx::RESET); + break; + } + } + _ -> { + output(fx::RED, "Invalid choice!\n", fx::RESET); + term::wait_for_key(); + } } - } else { - output(fx::RED, "Invalid choice!\n", fx::RESET); - term::wait_for_key(); - } } return 0; -} +} \ No newline at end of file From 518e8d426f598571c4d6c622e03cc2b8173fdc85 Mon Sep 17 00:00:00 2001 From: TheDevConnor Date: Sat, 18 Oct 2025 06:16:12 -0400 Subject: [PATCH 2/2] Made tetris in luma and cleaned up some libs --- c_test_files/tetris.c | 18 +-- std/termfx.lx | 16 +- std/terminal.lx | 47 +++--- tests/tetris.lx | 345 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 371 insertions(+), 55 deletions(-) create mode 100644 tests/tetris.lx diff --git a/c_test_files/tetris.c b/c_test_files/tetris.c index cb55b41d..780bd96c 100644 --- a/c_test_files/tetris.c +++ b/c_test_files/tetris.c @@ -267,23 +267,7 @@ int main() { gameOver = 1; break; } - // Handle escape sequences for arrow keys - if (c == 27) { // ESC - if (kbhit()) { - int c2 = readch(); - if (c2 == '[' && kbhit()) { - int c3 = readch(); - if (c3 == 'A') - key = 'U'; // Up arrow - if (c3 == 'B') - key = 'D'; // Down arrow - if (c3 == 'C') - key = 'R'; // Right arrow - if (c3 == 'D') - key = 'L'; // Left arrow - } - } - } + // Regular keys if (c == 'a' || c == 'A') key = 'L'; diff --git a/std/termfx.lx b/std/termfx.lx index afe4d031..1b55ead0 100644 --- a/std/termfx.lx +++ b/std/termfx.lx @@ -12,14 +12,14 @@ pub const MAGENTA: *char = "\x1b[35m"; pub const CYAN: *char = "\x1b[36m"; pub const WHITE: *char = "\x1b[37m"; -pub const BRIGHT_BLACK: *char = "\x1b[90m"; -pub const BRIGHT_RED: *char = "\x1b[91m"; -pub const BRIGHT_GREEN: *char = "\x1b[92m"; -pub const BRIGHT_YELLOW: *char = "\x1b[93m"; -pub const BRIGHT_BLUE: *char = "\x1b[94m"; -pub const BRIGHT_MAGENTA: *char = "\x1b[95m"; -pub const BRIGHT_CYAN: *char = "\x1b[96m"; -pub const BRIGHT_WHITE: *char = "\x1b[97m"; +pub const BRIGHT_BLACK: *char = "\x1b[1;90m"; +pub const BRIGHT_RED: *char = "\x1b[1;91m"; +pub const BRIGHT_GREEN: *char = "\x1b[1;92m"; +pub const BRIGHT_YELLOW: *char = "\x1b[1;93m"; +pub const BRIGHT_BLUE: *char = "\x1b[1;94m"; +pub const BRIGHT_MAGENTA: *char = "\x1b[1;95m"; +pub const BRIGHT_CYAN: *char = "\x1b[1;96m"; +pub const BRIGHT_WHITE: *char = "\x1b[1;97m"; pub const BG_BLACK: *char = "\x1b[40m"; pub const BG_RED: *char = "\x1b[41m"; diff --git a/std/terminal.lx b/std/terminal.lx index 162d65d8..8f3affcd 100644 --- a/std/terminal.lx +++ b/std/terminal.lx @@ -22,7 +22,7 @@ pub const enable_raw_mode -> fn () void { // -ixon: disable software flow control (Ctrl+S, Ctrl+Q) // -icrnl: don't translate CR to NL // min 1 time 0: blocking read (wait for at least 1 character) - system("stty -icanon -echo -isig -ixon -icrnl min 1 time 0"); + system("stty -icanon -echo -isig -ixon -icrnl min 0 time 0"); // Hide cursor output(fx::CURSOR_HIDE); @@ -62,15 +62,8 @@ pub const getch_raw -> fn () char { // Get a single character without waiting for Enter pub const getch -> fn () char { - // Disable canonical mode and echo - system("stty -icanon -echo"); - - // Read single character - let c: char = input(""); - - // Restore terminal settings - system("stty icanon echo"); - + // Just try to read input; don't touch stty here + let c: char = input(""); return c; } @@ -95,21 +88,14 @@ pub const getche -> fn () char { // Check if a key is pressed (non-blocking) // WARNING: This consumes the character! Don't use in raw mode. pub const kbhit -> fn () int { - // Save current state - system("stty -g > /tmp/luma_kbhit_backup"); - - // Set non-blocking - system("stty -icanon -echo min 0 time 0"); - let c: char = input(""); - - // Restore previous state - system("stty $(cat /tmp/luma_kbhit_backup)"); - system("rm /tmp/luma_kbhit_backup"); - - if (c == cast(0)) { - return 0; + // Non-blocking test only + let result: int = system("read -t 0 -n 1 key < /dev/tty"); + if (result == cast(0)) { + // put the char back for getch() + system("read -t 0 -n 1 key < /dev/tty && printf '%s' $key > /tmp/luma_kbhit_buf"); + return 1; } - return 1; + return 0; } // Wait for any key press @@ -179,12 +165,13 @@ pub const getpass -> fn (prompt: *char) *char { // Sleep for milliseconds (more precise than system("sleep")) pub const sleep_ms -> fn (ms: int) void { - // Convert to seconds string - let seconds: *char = string::from_int(ms); - defer free(seconds); - - // TODO: Better implementation with usleep or nanosleep - system("sleep 0.001"); // Placeholder + let cmd: *char = cast<*char>(alloc(64 * sizeof)); + defer { free(cmd); } + + // Multiply milliseconds by 1000 for microseconds + string::cat(cmd, "usleep ", string::from_int(ms * 1000)); + + system(cmd); } // Get terminal size diff --git a/tests/tetris.lx b/tests/tetris.lx new file mode 100644 index 00000000..b15c5b7c --- /dev/null +++ b/tests/tetris.lx @@ -0,0 +1,345 @@ +@module "main" + +@use "terminal" as term +@use "termfx" as tx + +const FIELD_W: int = 14; +const FIELD_H: int = 24; +const VISIB_H: int = 23; + +const teromino: [[*char; 4]; 7] = [ + ["....", "XXXX", "....", "...."], // I + [".X..", ".X..", ".XX.", "...."], // J + ["..X.", "..X.", ".XX.", "...."], // L + [".XX.", ".XX.", "....", "...."], // O + [".XX.", "XX..", "....", "...."], // S + [".X..", ".XXX", "....", "...."], // T + ["XX..", ".XX.", "....", "...."] // Z +]; + +const piece_color: [*char; 7] = [ + "\x1b[96m", // Cyan - I piece + "\x1b[94m", // Blue - J piece + "\x1b[38;5;208m", // Orange - L piece + "\x1b[93m", // Yellow - O piece + "\x1b[92m", // Green - S piece + "\x1b[95m", // Magenta - T piece + "\x1b[91m" // Red - Z piece +]; + +const offset: [[int; 16]; 4] = [ + [0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15], + [12, 8, 4, 0, 13, 9, 5, 1, + 14, 10, 6, 2, 15, 11, 7, 3], + [15, 14, 13, 12, 11, 10, 9, 8, + 7, 6, 5, 4, 3, 2, 1, 0], + [3, 7, 11, 15, 2, 6, 10, 14, + 1, 5, 9, 13, 0, 4, 8, 12] +]; + +let field: *int; + +const init_field -> fn () void { + loop [x: int = 0, y: int = 0](x < FIELD_W) : (++x) { + y = 0; + loop (y < FIELD_H) : (++y) { + if (x == 0 || x == (FIELD_W - 1) || y == (FIELD_H-1)) { + field[y * FIELD_W + x] = 9; // wall + } else { + field[y * FIELD_W + x] = 0; // empty + } + } + } +} + +const rotate -> fn (px: int, py: int, r: int) int { + return offset[r % 4][py * 4 + px]; +} + +const draw_screen -> fn (c_piece: int, c_rotation: int, c_x: int, c_y: int, + score: int, lines: int, next_piece: int) void { + output(tx::move_cursor(3, 1), tx::RESET, "╔════════════════════════════╗ ╔═══════════════╗\n", + "║ " tx::BRIGHT_WHITE,"T E T R I S", tx::RESET, " ║ ║ ", + tx::BRIGHT_WHITE,"NEXT PIECE", tx::RESET, " ║\n", + "╠════════════════════════════╣ ║ ║\n"); + + loop [y: int = 0](y < VISIB_H) : (++y) { + output("║"); + loop [x: int = 0](x < FIELD_W) : (++x) { + let cell: int = field[y * FIELD_W + x]; + + // Check if current piece occupies this position + let is_piece: int = 0; + loop [py: int = 0](py < 4) : (++py) { + loop [px: int = 0](px < 4) : (++px) { + let pi: int = rotate(px, py, c_rotation); + let row: int = pi / 4; + let col: int = pi % 4; + let str_ptr: *char = teromino[c_piece][row]; + if (str_ptr[col] == 'X') { + if ((c_x + px) == x && (c_y + py) == y) { + is_piece = 1; + } + } + } + } + + if (is_piece == 1) { + output(piece_color[c_piece], "██"); + } elif (cell == 9) { + output("\x1b[90m", "██"); // Wall (dark gray) + } elif (cell > 0) { + output(piece_color[cell - 1], "██"); // Locked piece + } else { + output(" "); // Empty space + } + } + output(tx::RESET, "║"); + + switch(y) { + 1, 2, 3, 4 -> { + let py: int = y - 1; + output(" ║ "); + loop [px: int = 0](px < 4) : (++px) { + let pi: int = rotate(px, py, 0); + let row: int = pi / 4; + let col: int = pi % 4; + let str_ptr: *char = teromino[next_piece][row]; + if (str_ptr[col] == 'X') { + output(piece_color[next_piece], "██", tx::RESET); + } else { + output(" "); + } + } + output(" ║"); + } + 5 -> output(" ╠═══════════════╣"); + 6 -> output(" ║ \x1b[1mScore:\x1b[0m ║"); + 7 -> output(" ║ \x1b[93m", score, "\x1b[0m ║"); + 8 -> output(" ║ ║"); + 9 -> output(" ║ \x1b[1mLines:\x1b[0m ║"); + 10 -> output(" ║ \x1b[92m", lines, "\x1b[0m ║"); + 11 -> output(" ╠═══════════════╣"); + 12 -> output(" ║ \x1b[1mControls:\x1b[0m ║"); + 13 -> output(" ║ \x1b[96mW/↑\x1b[0m - Rotate ║"); + 14 -> output(" ║ \x1b[96mA/←\x1b[0m - Left ║"); + 15 -> output(" ║ \x1b[96mD/→\x1b[0m - Right ║"); + 16 -> output(" ║ \x1b[96mS/↓\x1b[0m - Down ║"); + 17 -> output(" ║ \x1b[96mSPACE\x1b[0m - Drop ║"); + 18 -> output(" ║ \x1b[96mQ\x1b[0m - Quit ║"); + 22 -> output(" ╚═══════════════╝"); + _ -> output(" ║ ║"); + } + + output("\n"); + } + + output("╚════════════════════════════╝\n"); +} + +const does_piece_fit -> fn (piece: int, rotation: int, pos_x: int, pos_y: int) int { + loop [py: int = 0](py < 4) : (++py) { + loop [px: int = 0](px < 4) : (++px) { + let pi: int = rotate(px, py, rotation); + let row: int = pi / 4; + let col: int = pi % 4; + let str_ptr: *char = teromino[piece][row]; + if (str_ptr[col] == 'X') { + let field_x: int = pos_x + px; + let field_y: int = pos_y + py; + + // Check boundaries + if (field_x < 0 || field_x >= FIELD_W || field_y >= FIELD_H) { + return 0; + } + + // Check collision with locked pieces + if (field_y >= 0 && field[field_y * FIELD_W + field_x] != 0) { + return 0; + } + } + } + } + return 1; +} + +const lock_piece -> fn (piece: int, rotation: int, pos_x: int, pos_y: int) void { + loop [py: int = 0](py < 4) : (++py) { + loop [px: int = 0](px < 4) : (++px) { + let pi: int = rotate(px, py, rotation); + let row: int = pi / 4; + let col: int = pi % 4; + let str_ptr: *char = teromino[piece][row]; + if (str_ptr[col] == 'X') { + let field_x: int = pos_x + px; + let field_y: int = pos_y + py; + + if (field_y >= 0 && field_y < FIELD_H && field_x >= 0 && field_x < FIELD_W) { + field[field_y * FIELD_W + field_x] = piece + 1; + } + } + } + } +} + +const check_lines -> fn () int { + let lines_cleared: int = 0; + + loop [y: int = FIELD_H - 2](y > 0) : (--y) { + let is_full: int = 1; + + loop [x: int = 1](x < FIELD_W - 1) : (++x) { + if (field[y * FIELD_W + x] == 0) { + is_full = 0; + } + } + + if (is_full == 1) { + lines_cleared = lines_cleared + 1; + + // Move lines down + loop [yy: int = y](yy > 1) : (--yy) { + loop [x: int = 1](x < FIELD_W - 1) : (++x) { + field[yy * FIELD_W + x] = field[(yy - 1) * FIELD_W + x]; + } + } + + // Clear top line + loop [x: int = 1](x < FIELD_W - 1) : (++x) { + field[FIELD_W + x] = 0; + } + + ++y; // Check this line again + } + } + + return lines_cleared; +} + +pub const main -> fn () int { + let total: int = FIELD_W * FIELD_H; + field = cast<*int>(alloc(sizeof * total)); + defer { free(field); } + + term::enable_raw_mode(); + output(tx::CLEAR_SCREEN, "\n\n"); + init_field(); + + let current_piece: int = 1; + let current_rotation: int = 0; + let current_x: int = FIELD_W / 2 - 2; + let current_y: int = 0; + let next_piece: int = 3; + + let game_over: int = 0; + let score: int = 0; + let lines_cleared_total: int = 0; + let speed: int = 20; + let speed_count: int = 0; + let force_down: int = 0; + + loop (game_over == 0) { + ++speed_count; + force_down = -cast((speed_count == speed) && (1 == 1)); + if (force_down == 1) { speed_count = 0; } + + // Input + if (term::kbhit() == 1) { + let c: char = term::getch(); + switch(c) { + 'q', 'Q' -> { game_over = 1; } + 'a', 'A' -> { + if (does_piece_fit(current_piece, current_rotation, current_x - 1, current_y) == 1) { + --current_x; + } + } + 'd', 'D' -> { + if (does_piece_fit(current_piece, current_rotation, current_x + 1, current_y) == 1) { + ++current_x; + } + } + 's', 'S' -> { + if (does_piece_fit(current_piece, current_rotation, current_x, current_y + 1) == 1) { + ++current_y; + } + } + 'w', 'W' -> { + let new_rotation: int = (current_rotation + 1) % 4; + if (does_piece_fit(current_piece, new_rotation, current_x, current_y) == 1) { + current_rotation = new_rotation; + } + } + ' ' -> { + // Hard drop + loop (does_piece_fit(current_piece, current_rotation, current_x, current_y + 1) == 1) { + ++current_y; + score = score + 2; + } + force_down = 1; + } + } + } + + // Gravity + if (force_down == 1) { + if (does_piece_fit(current_piece, current_rotation, current_x, current_y + 1) == 1) { + ++current_y; + ++score; + } else { + // Lock piece + lock_piece(current_piece, current_rotation, current_x, current_y); + + // Check for lines + let lines: int = check_lines(); + if (lines > 0) { + lines_cleared_total = lines_cleared_total + lines; + score = score + (lines * lines * 100); + } + + // Spawn new piece + current_piece = next_piece; + next_piece = (next_piece + 1) % 7; + current_rotation = 0; + current_x = FIELD_W / 2 - 2; + current_y = 0; + + // Check game over + if (does_piece_fit(current_piece, current_rotation, current_x, current_y) == 0) { + output(tx::CLEAR_SCREEN, tx::move_cursor(1, 1), "\n\n\n"); + output("╔══════════════════════════════╗\n"); + output("║ ", tx::BRIGHT_RED, "GAME OVER!", tx::RESET, " ║\n"); + output("╠══════════════════════════════╣\n"); + output("║ ║\n"); + output("║ Final Score: ", tx::BRIGHT_YELLOW, score, tx::RESET, " ║\n"); + output("║ Lines: ", tx::BRIGHT_GREEN, lines_cleared_total, tx::RESET, " ║\n"); + output("║ ║\n"); + output("╠══════════════════════════════╣\n"); + output("║ Press Q to quit ║\n"); + output("╚══════════════════════════════╝\n"); + + loop { + if (term::kbhit() == 1) { + let c: char = term::getch(); + if (c == 'q' || c == 'Q') { + game_over = 1; + break; + } + } + } + } + } + + force_down = 0; + } + + draw_screen(current_piece, current_rotation, current_x, current_y, + score, lines_cleared_total, next_piece); + + loop [i: int = 0](i < 20000000) :(++i) {} + } + + term::disable_raw_mode(); + output(tx::BRIGHT_CYAN, "\n\nGame Over! Final Score: ", score, "\n", tx::RESET); + return 0; +} \ No newline at end of file