Add timer to legacy version

This commit is contained in:
markmental 2026-03-22 22:42:37 -04:00
commit 6ca7417011
2 changed files with 188 additions and 1 deletions

61
README.md Normal file
View file

@ -0,0 +1,61 @@
# mcfreecell
`mcfreecell` on `leg-py1.5.2` is a terminal-native FreeCell build aimed at old Python and old curses environments.
This branch keeps the implementation within a Python-1.5.2-friendly subset and favors compatibility over modern terminal features.
## Features
- FreeCell rules with 8 tableau columns, 4 free cells, and 4 foundations
- keyboard-driven curses interface
- visual stack selection mode
- lightweight `?` hint system
- ASCII-only suit tags for old terminals and fonts
- compatibility fallbacks for weak or incomplete curses implementations
## Controls
- `Arrow keys` / `h j k l` - move cursor
- `v` - enter or exit tableau selection mode
- `y` - pick up selected card or stack
- `p` - drop held card or stack
- `Enter` / `Space` - alternate pick up / drop
- `?` - show a suggested move
- `f` - quick move selected card to a free cell
- `d` - quick move selected card to foundation
- `Esc` - cancel held move
- `q` - quit
## Suits
- Debian (`Deb`)
- Red Hat (`RHt`)
- Solaris (`Sol`)
- HP-UX (`HPx`)
These are split into the two FreeCell color groups as:
- red group: Debian, Red Hat
- black-equivalent group: Solaris, HP-UX
On older terminals, the non-red group may render as blue, cyan, or plain monochrome depending on curses support.
## Running
Run with whatever Python interpreter is available on the target machine:
```sh
python linux-freecell.py
```
## Terminal Notes
Terminal and curses behavior can vary significantly on older systems.
- `xterm` generally gives the most reliable screen redraw behavior for this branch.
- Older `konsole` builds, including KDE 2.x-era versions, may show redraw artifacts or ghosting until the screen is resized.
- The game includes several repaint fallbacks for weaker curses implementations, but some VT100-style emulator quirks are still terminal-specific.
## Status
This branch is intended as a playable legacy-friendly FreeCell build for old Unix-like systems and older Python environments.

View file

@ -25,6 +25,7 @@ q : Quit
""" """
import curses import curses
import time
try: try:
import random import random
@ -168,6 +169,14 @@ def pad_center(text, width):
return (" " * left) + text + (" " * right) return (" " * left) + text + (" " * right)
def format_elapsed_time(seconds):
if seconds < 0:
seconds = 0
minutes = int(seconds / 60)
remaining = seconds % 60
return "%02d:%02d" % (minutes, remaining)
def rounded_position(start_x, span, index, steps): def rounded_position(start_x, span, index, steps):
if steps <= 0: if steps <= 0:
return start_x return start_x
@ -273,10 +282,30 @@ class FreeCellGame:
self.cursor_zone = "bottom" self.cursor_zone = "bottom"
self.cursor_index = 0 self.cursor_index = 0
self.status = "Arrows move, v selects, ? hints." self.status = "Arrows move, v selects, ? hints."
self.start_time = time.time()
self.finished_time_seconds = None
self.needs_full_repaint = 0
self._deal_new_game() self._deal_new_game()
def request_full_repaint(self):
self.needs_full_repaint = 1
def elapsed_time_seconds(self):
if self.finished_time_seconds is not None:
return self.finished_time_seconds
elapsed = int(time.time() - self.start_time)
if elapsed < 0:
elapsed = 0
if self.is_won():
self.finished_time_seconds = elapsed
return self.finished_time_seconds
return elapsed
def clear_hint(self): def clear_hint(self):
had_hint = self.hint_move is not None
self.hint_move = None self.hint_move = None
if had_hint:
self.request_full_repaint()
def _deal_new_game(self): def _deal_new_game(self):
deck = [] deck = []
@ -612,9 +641,11 @@ class FreeCellGame:
if not moves: if not moves:
self.hint_move = None self.hint_move = None
self.status = "Hint: no obvious legal move found." self.status = "Hint: no obvious legal move found."
self.request_full_repaint()
return return
self.hint_move = moves[0] self.hint_move = moves[0]
self.status = "Hint: %s." % self.describe_hint_move(self.hint_move) self.status = "Hint: %s." % self.describe_hint_move(self.hint_move)
self.request_full_repaint()
def hinted_freecell(self, index): def hinted_freecell(self, index):
if self.hint_move is None: if self.hint_move is None:
@ -734,6 +765,7 @@ class FreeCellGame:
moving_count, moving_count,
self.cursor_index + 1, self.cursor_index + 1,
) )
self.request_full_repaint()
self.exit_visual_mode(None) self.exit_visual_mode(None)
return return
if self.cursor_index < 4: if self.cursor_index < 4:
@ -748,6 +780,7 @@ class FreeCellGame:
card.short_name(), card.short_name(),
self.cursor_index + 1, self.cursor_index + 1,
) )
self.request_full_repaint()
return return
self.status = "Cannot pick up directly from foundations." self.status = "Cannot pick up directly from foundations."
@ -761,6 +794,7 @@ class FreeCellGame:
self.freecells[self.held.source_index] = cards[0] self.freecells[self.held.source_index] = cards[0]
self.held = None self.held = None
self.clear_hint() self.clear_hint()
self.request_full_repaint()
def drop(self): def drop(self):
if self.held is None: if self.held is None:
@ -784,6 +818,7 @@ class FreeCellGame:
self.cursor_index + 1, self.cursor_index + 1,
) )
self.held = None self.held = None
self.request_full_repaint()
else: else:
self.status = "Illegal move for tableau column." self.status = "Illegal move for tableau column."
return return
@ -799,6 +834,7 @@ class FreeCellGame:
self.cursor_index + 1, self.cursor_index + 1,
) )
self.held = None self.held = None
self.request_full_repaint()
else: else:
self.status = "That free cell is occupied." self.status = "That free cell is occupied."
return return
@ -811,6 +847,7 @@ class FreeCellGame:
foundation.append(moving_top) foundation.append(moving_top)
self.status = "Moved %s to foundation." % moving_top.short_name() self.status = "Moved %s to foundation." % moving_top.short_name()
self.held = None self.held = None
self.request_full_repaint()
else: else:
self.status = "Illegal move for foundation." self.status = "Illegal move for foundation."
@ -833,6 +870,7 @@ class FreeCellGame:
self.clear_hint() self.clear_hint()
self.freecells[free_idx] = col.pop() self.freecells[free_idx] = col.pop()
self.status = "Moved card to free cell %d." % (free_idx + 1) self.status = "Moved card to free cell %d." % (free_idx + 1)
self.request_full_repaint()
return return
if self.cursor_zone == "top" and self.cursor_index < 4: if self.cursor_zone == "top" and self.cursor_index < 4:
self.status = "Already on a free cell." self.status = "Already on a free cell."
@ -870,6 +908,7 @@ class FreeCellGame:
else: else:
self.freecells[source[1]] = None self.freecells[source[1]] = None
self.status = "Moved %s to foundation." % card.short_name() self.status = "Moved %s to foundation." % card.short_name()
self.request_full_repaint()
def is_won(self): def is_won(self):
total = 0 total = 0
@ -991,6 +1030,8 @@ def draw_top_row(stdscr, game, layout):
index = 0 index = 0
while index < 4: while index < 4:
x = layout.freecell_xs[index] x = layout.freecell_xs[index]
safe_addstr(stdscr, TOP_Y + 1, x - 1, " ", pair_attr(1))
safe_addnstr(stdscr, TOP_Y + 1, x, " " * CARD_W, CARD_W, pair_attr(1))
selected = game.cursor_zone == "top" and game.cursor_index == index selected = game.cursor_zone == "top" and game.cursor_index == index
hinted = game.hinted_freecell(index) hinted = game.hinted_freecell(index)
marker = " " marker = " "
@ -1019,6 +1060,8 @@ def draw_top_row(stdscr, game, layout):
index = 0 index = 0
while index < 4: while index < 4:
x = layout.foundation_xs[index] x = layout.foundation_xs[index]
safe_addstr(stdscr, TOP_Y + 1, x - 1, " ", pair_attr(1))
safe_addnstr(stdscr, TOP_Y + 1, x, " " * CARD_W, CARD_W, pair_attr(1))
selected = game.cursor_zone == "top" and game.cursor_index == index + 4 selected = game.cursor_zone == "top" and game.cursor_index == index + 4
hinted = game.hinted_foundation(index) hinted = game.hinted_foundation(index)
marker = " " marker = " "
@ -1080,6 +1123,8 @@ def draw_columns(stdscr, game, layout):
col_idx = 0 col_idx = 0
while col_idx < 8: while col_idx < 8:
x = layout.tableau_xs[col_idx] x = layout.tableau_xs[col_idx]
safe_addstr(stdscr, COL_Y, x - 1, " ", pair_attr(1))
safe_addnstr(stdscr, COL_Y, x, " " * CARD_W, CARD_W, pair_attr(1))
selected = game.cursor_zone == "bottom" and game.cursor_index == col_idx selected = game.cursor_zone == "bottom" and game.cursor_index == col_idx
hinted_dest = game.hinted_column_destination(col_idx) hinted_dest = game.hinted_column_destination(col_idx)
header_label = str(col_idx + 1) header_label = str(col_idx + 1)
@ -1093,6 +1138,15 @@ def draw_columns(stdscr, game, layout):
header_attr = pair_attr(1) header_attr = pair_attr(1)
draw_box(stdscr, COL_Y, x, CARD_W, header_label, header_attr) draw_box(stdscr, COL_Y, x, CARD_W, header_label, header_attr)
clear_y = COL_Y + 1
clear_limit = COL_Y + 8 + max_height
while clear_y < clear_limit:
safe_addstr(stdscr, clear_y, x - 1, " ", pair_attr(1))
safe_addnstr(
stdscr, clear_y, x + 1, " " * (CARD_W - 2), CARD_W - 2, pair_attr(1)
)
clear_y = clear_y + 1
column = game.columns[col_idx] column = game.columns[col_idx]
if not column: if not column:
safe_addstr(stdscr, COL_Y + 1, x + 2, ".", pair_attr(1)) safe_addstr(stdscr, COL_Y + 1, x + 2, ".", pair_attr(1))
@ -1215,6 +1269,7 @@ def render(stdscr, game):
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 = "Time: %s" % format_elapsed_time(game.elapsed_time_seconds())
title = "mcfreecell 1.5.2" title = "mcfreecell 1.5.2"
if len(title) > max_x - 4: if len(title) > max_x - 4:
title = "mcfreecell" title = "mcfreecell"
@ -1229,6 +1284,18 @@ def render(stdscr, game):
max_x - title_x - 1, max_x - title_x - 1,
pair_attr(1) | text_attr("A_BOLD"), pair_attr(1) | text_attr("A_BOLD"),
) )
timer_x = max_x - len(elapsed_text) - 2
if timer_x < 2:
timer_x = 2
if timer_x > title_x + len(title):
safe_addnstr(
stdscr,
0,
timer_x,
elapsed_text,
max_x - timer_x - 1,
pair_attr(1) | text_attr("A_BOLD"),
)
if game.visual_mode: if game.visual_mode:
visual_text = "-- VISUAL --" visual_text = "-- VISUAL --"
visual_x = title_x + len(title) + 3 visual_x = title_x + len(title) + 3
@ -1242,7 +1309,10 @@ def render(stdscr, game):
draw_status(stdscr, game, max_y, max_x) draw_status(stdscr, game, max_y, max_x)
draw_help(stdscr, max_y, max_x) draw_help(stdscr, max_y, max_x)
if game.is_won(): if game.is_won():
safe_addstr(stdscr, 3, 2, "You won.", pair_attr(5) | text_attr("A_BOLD")) win_text = "You won in %s." % format_elapsed_time(game.elapsed_time_seconds())
safe_addnstr(
stdscr, 3, 2, win_text, max_x - 4, pair_attr(5) | text_attr("A_BOLD")
)
stdscr.refresh() stdscr.refresh()
@ -1332,6 +1402,8 @@ def handle_key(game, ch):
def curses_main(stdscr): def curses_main(stdscr):
idle_ticks = 0
poll_delay = 0
if hasattr(curses, "curs_set"): if hasattr(curses, "curs_set"):
try: try:
curses.curs_set(0) curses.curs_set(0)
@ -1342,11 +1414,65 @@ def curses_main(stdscr):
stdscr.keypad(1) stdscr.keypad(1)
except: except:
pass pass
if hasattr(stdscr, "timeout"):
try:
stdscr.timeout(100)
except:
pass
elif hasattr(stdscr, "nodelay"):
try:
stdscr.nodelay(1)
poll_delay = 1
except:
poll_delay = 0
init_colors() init_colors()
game = FreeCellGame() game = FreeCellGame()
while 1: while 1:
if game.needs_full_repaint:
game.needs_full_repaint = 0
if hasattr(stdscr, "redrawwin"):
try:
stdscr.redrawwin()
except:
pass
elif hasattr(stdscr, "touchwin"):
try:
stdscr.touchwin()
except:
pass
elif hasattr(stdscr, "clearok"):
try:
stdscr.clearok(1)
except:
pass
render(stdscr, game) render(stdscr, game)
ch = stdscr.getch() ch = stdscr.getch()
if ch == -1:
idle_ticks = idle_ticks + 1
if idle_ticks >= 5:
idle_ticks = 0
if hasattr(stdscr, "redrawwin"):
try:
stdscr.redrawwin()
except:
pass
elif hasattr(stdscr, "touchwin"):
try:
stdscr.touchwin()
except:
pass
elif hasattr(stdscr, "clearok"):
try:
stdscr.clearok(1)
except:
pass
if poll_delay:
try:
time.sleep(0.1)
except:
pass
continue
idle_ticks = 0
if not handle_key(game, ch): if not handle_key(game, ch):
break break