diff --git a/linux-freecell.py b/linux-freecell.py index 444e7bc..f0939e6 100644 --- a/linux-freecell.py +++ b/linux-freecell.py @@ -28,6 +28,7 @@ Notes import curses import random +import time from dataclasses import dataclass from typing import List, Optional, Tuple @@ -141,8 +142,21 @@ class FreeCellGame: self.cursor_index = 0 self.status = "Arrow keys move, v selects a stack, ? shows a hint." + self.start_time = time.monotonic() + self.finished_time_seconds: Optional[int] = None self._deal_new_game() + def elapsed_time_seconds(self) -> int: + """Return elapsed play time, freezing it after a win.""" + if self.finished_time_seconds is not None: + return self.finished_time_seconds + + elapsed = int(time.monotonic() - self.start_time) + if self.is_won(): + self.finished_time_seconds = elapsed + return self.finished_time_seconds + return elapsed + def clear_hint(self) -> None: """Drop the currently remembered hint highlight.""" self.hint_move = None @@ -211,7 +225,9 @@ class FreeCellGame: move = self.move_history[-1] if not self._remove_cards_from_destination(move): - self.status = "Undo failed because the board no longer matches the last move." + self.status = ( + "Undo failed because the board no longer matches the last move." + ) return self.move_history.pop() @@ -917,7 +933,9 @@ class FreeCellGame: else: self.freecells[source[1]] = None - self.record_move(source[0], source[1], "foundation", SUITS.index(card.suit), [card]) + self.record_move( + source[0], source[1], "foundation", SUITS.index(card.suit), [card] + ) self.status = f"Moved {card.short_name()} to foundation." @@ -1229,11 +1247,18 @@ def draw_help(stdscr, max_y: int) -> None: stdscr.addnstr(y, 2, help_text, max(0, curses.COLS - 4), curses.color_pair(1)) +def format_elapsed_time(seconds: int) -> str: + """Format elapsed seconds as MM:SS.""" + minutes, seconds = divmod(max(0, seconds), 60) + return f"{minutes:02d}:{seconds:02d}" + + def render(stdscr, game: FreeCellGame) -> None: """Redraw the full screen.""" stdscr.erase() max_y, max_x = stdscr.getmaxyx() layout = compute_board_layout(max_x) + elapsed_text = f"Time: {format_elapsed_time(game.elapsed_time_seconds())}" title = "mcfreecell 0.1" if len(title) > max_x - 4: @@ -1247,6 +1272,16 @@ def render(stdscr, game: FreeCellGame) -> None: curses.color_pair(1) | curses.A_BOLD, ) + timer_x = max(2, max_x - len(elapsed_text) - 2) + if timer_x > title_x + len(title): + stdscr.addnstr( + 0, + timer_x, + elapsed_text, + max(0, max_x - timer_x - 1), + curses.color_pair(1) | curses.A_BOLD, + ) + if game.visual_mode: visual_text = "-- VISUAL --" visual_x = min(max_x - len(visual_text) - 2, title_x + len(title) + 3) @@ -1262,7 +1297,14 @@ def render(stdscr, game: FreeCellGame) -> None: draw_help(stdscr, max_y) if game.is_won(): - stdscr.addstr(3, 2, "You won.", curses.color_pair(5) | curses.A_BOLD) + win_text = f"You won in {format_elapsed_time(game.elapsed_time_seconds())}." + stdscr.addnstr( + 3, + 2, + win_text, + max(0, max_x - 4), + curses.color_pair(5) | curses.A_BOLD, + ) stdscr.refresh() @@ -1395,6 +1437,7 @@ def curses_main(stdscr) -> None: """Main UI entry point.""" curses.curs_set(0) stdscr.keypad(True) + stdscr.timeout(100) init_colors() @@ -1403,6 +1446,8 @@ def curses_main(stdscr) -> None: while True: render(stdscr, game) ch = stdscr.getch() + if ch == -1: + continue if not handle_key(game, ch): break