diff --git a/.Jules/palette.md b/.Jules/palette.md index 8aed79d..3138a56 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -17,3 +17,7 @@ ## 2026-02-13 - Tactile Feedback in CLI **Learning:** In terminal-based games, users expect immediate visual feedback for their actions. Relying on a periodic "tick" to update the UI creates a laggy feel. Using `poll()` with a dynamic timeout allows the application to remain idle yet wake up instantly to process and render user input. **Action:** Always trigger a UI refresh immediately after processing user input in CLI applications, and use efficient waiting mechanisms (like `poll`) that can be interrupted by input. + +## 2024-10-24 - Cursor Visibility and Readiness in CLI Games +**Learning:** A visible blinking cursor in a fast-paced CLI game is visually distracting. Additionally, throwing users directly into a timed game loop without a readiness prompt leads to immediate initial failure and a poor UX. +**Action:** Always hide the cursor (`\033[?25l`) during gameplay (restoring it gracefully on normal or interrupted exits) and implement a "Press any key to start..." prompt before entering active timed loops to ensure the user is prepared. diff --git a/src/main.cpp b/src/main.cpp index 775bf66..bc84610 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,8 +25,8 @@ struct termios oldt; void restore_terminal(int signum) { tcsetattr(STDIN_FILENO, TCSANOW, &oldt); // Use write() and _exit() because they are async-signal-safe - const char* msg = "\033[0m\n\nGame interrupted. Terminal settings restored.\n"; - write(STDOUT_FILENO, msg, 52); + const char msg[] = "\033[?25h\033[0m\n\nGame interrupted. Terminal settings restored.\n"; + write(STDOUT_FILENO, msg, sizeof(msg) - 1); _exit(signum); } @@ -47,10 +47,15 @@ int main() { } long long score = 0; bool hardMode = false; char input; + std::cout << "\033[?25l"; // Hide cursor std::cout << CLR_CTRL << "==========================\n SPEED CLICKER\n==========================\n" << CLR_RESET << "Controls:\n " << CLR_CTRL << "[h]" << CLR_RESET << " Toggle Hard Mode (10x Speed!)\n " << CLR_CTRL << "[q]" << CLR_RESET << " Quit Game\n " << CLR_CTRL << "[Any key]" << CLR_RESET << " Click!\n\n"; + std::cout << "Press any key to start..." << std::flush; + read(STDIN_FILENO, &input, 1); + std::cout << "\r \r" << std::flush; // Clear the start prompt + struct pollfd fds[1] = {{STDIN_FILENO, POLLIN, 0}}; auto last_tick = std::chrono::steady_clock::now(); bool updateUI = true; @@ -86,6 +91,7 @@ int main() { } } tcsetattr(STDIN_FILENO, TCSANOW, &oldt); + std::cout << "\033[?25h"; // Restore cursor std::cout << "\n\n" << CLR_SCORE << "Final Score: " << score << CLR_RESET << "\nThanks for playing!\n"; return 0; } diff --git a/verify_ux.py b/verify_ux.py new file mode 100644 index 0000000..89ff3b2 --- /dev/null +++ b/verify_ux.py @@ -0,0 +1,82 @@ +import pty +import os +import select +import time + +def verify_ux(): + master, slave = pty.openpty() + pid = os.fork() + + if pid == 0: + # Child process: run the game + os.setsid() + os.dup2(slave, 0) + os.dup2(slave, 1) + os.dup2(slave, 2) + os.close(slave) + os.close(master) + os.execl('./game', './game') + else: + # Parent process + os.close(slave) + output = b'' + + # Read the initial prompt and wait for it + timeout = 2.0 + start_time = time.time() + while time.time() - start_time < timeout: + r, _, _ = select.select([master], [], [], 0.1) + if r: + data = os.read(master, 1024) + if not data: + break + output += data + if b'Press any key to start...' in output: + break + + # We need to send a key to start the game + os.write(master, b' ') + + # Read until we see the first score or wait a little + start_time = time.time() + while time.time() - start_time < timeout: + r, _, _ = select.select([master], [], [], 0.1) + if r: + data = os.read(master, 1024) + if not data: + break + output += data + if b'Score: 0' in output: + break + + # Send quit command + os.write(master, b'q') + + # Read the rest of the output + while True: + r, _, _ = select.select([master], [], [], 0.5) + if r: + try: + data = os.read(master, 1024) + if not data: + break + output += data + except OSError: + break + else: + break + + os.close(master) + os.waitpid(pid, 0) + + output_str = output.decode('utf-8', errors='ignore') + print("Captured output:") + print(repr(output_str)) + + assert '\033[?25l' in output_str, "Cursor hide sequence missing!" + assert '\033[?25h' in output_str, "Cursor restore sequence missing!" + assert 'Press any key to start...' in output_str, "Start prompt missing!" + print("All UX checks passed!") + +if __name__ == '__main__': + verify_ux()