Adds timer to gameplay

This commit is contained in:
markmental 2026-03-22 17:05:01 -04:00
commit ee14d51d35

View file

@ -28,6 +28,7 @@ Notes
import curses import curses
import random import random
import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
@ -141,8 +142,21 @@ class FreeCellGame:
self.cursor_index = 0 self.cursor_index = 0
self.status = "Arrow keys move, v selects a stack, ? shows a hint." 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() 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: def clear_hint(self) -> None:
"""Drop the currently remembered hint highlight.""" """Drop the currently remembered hint highlight."""
self.hint_move = None self.hint_move = None
@ -211,7 +225,9 @@ class FreeCellGame:
move = self.move_history[-1] move = self.move_history[-1]
if not self._remove_cards_from_destination(move): 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 return
self.move_history.pop() self.move_history.pop()
@ -917,7 +933,9 @@ class FreeCellGame:
else: else:
self.freecells[source[1]] = None 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." 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)) 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: def render(stdscr, game: FreeCellGame) -> None:
"""Redraw the full screen.""" """Redraw the full screen."""
stdscr.erase() stdscr.erase()
max_y, max_x = stdscr.getmaxyx() max_y, max_x = stdscr.getmaxyx()
layout = compute_board_layout(max_x) layout = compute_board_layout(max_x)
elapsed_text = f"Time: {format_elapsed_time(game.elapsed_time_seconds())}"
title = "mcfreecell 0.1" title = "mcfreecell 0.1"
if len(title) > max_x - 4: if len(title) > max_x - 4:
@ -1247,6 +1272,16 @@ def render(stdscr, game: FreeCellGame) -> None:
curses.color_pair(1) | curses.A_BOLD, 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: if game.visual_mode:
visual_text = "-- VISUAL --" visual_text = "-- VISUAL --"
visual_x = min(max_x - len(visual_text) - 2, title_x + len(title) + 3) 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) draw_help(stdscr, max_y)
if game.is_won(): 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() stdscr.refresh()
@ -1395,6 +1437,7 @@ def curses_main(stdscr) -> None:
"""Main UI entry point.""" """Main UI entry point."""
curses.curs_set(0) curses.curs_set(0)
stdscr.keypad(True) stdscr.keypad(True)
stdscr.timeout(100)
init_colors() init_colors()
@ -1403,6 +1446,8 @@ def curses_main(stdscr) -> None:
while True: while True:
render(stdscr, game) render(stdscr, game)
ch = stdscr.getch() ch = stdscr.getch()
if ch == -1:
continue
if not handle_key(game, ch): if not handle_key(game, ch):
break break