Add timer to legacy version
This commit is contained in:
parent
0ab0abf6b2
commit
6ca7417011
2 changed files with 188 additions and 1 deletions
61
README.md
Normal file
61
README.md
Normal 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.
|
||||
|
|
@ -25,6 +25,7 @@ q : Quit
|
|||
"""
|
||||
|
||||
import curses
|
||||
import time
|
||||
|
||||
try:
|
||||
import random
|
||||
|
|
@ -168,6 +169,14 @@ def pad_center(text, width):
|
|||
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):
|
||||
if steps <= 0:
|
||||
return start_x
|
||||
|
|
@ -273,10 +282,30 @@ class FreeCellGame:
|
|||
self.cursor_zone = "bottom"
|
||||
self.cursor_index = 0
|
||||
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()
|
||||
|
||||
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):
|
||||
had_hint = self.hint_move is not None
|
||||
self.hint_move = None
|
||||
if had_hint:
|
||||
self.request_full_repaint()
|
||||
|
||||
def _deal_new_game(self):
|
||||
deck = []
|
||||
|
|
@ -612,9 +641,11 @@ class FreeCellGame:
|
|||
if not moves:
|
||||
self.hint_move = None
|
||||
self.status = "Hint: no obvious legal move found."
|
||||
self.request_full_repaint()
|
||||
return
|
||||
self.hint_move = moves[0]
|
||||
self.status = "Hint: %s." % self.describe_hint_move(self.hint_move)
|
||||
self.request_full_repaint()
|
||||
|
||||
def hinted_freecell(self, index):
|
||||
if self.hint_move is None:
|
||||
|
|
@ -734,6 +765,7 @@ class FreeCellGame:
|
|||
moving_count,
|
||||
self.cursor_index + 1,
|
||||
)
|
||||
self.request_full_repaint()
|
||||
self.exit_visual_mode(None)
|
||||
return
|
||||
if self.cursor_index < 4:
|
||||
|
|
@ -748,6 +780,7 @@ class FreeCellGame:
|
|||
card.short_name(),
|
||||
self.cursor_index + 1,
|
||||
)
|
||||
self.request_full_repaint()
|
||||
return
|
||||
self.status = "Cannot pick up directly from foundations."
|
||||
|
||||
|
|
@ -761,6 +794,7 @@ class FreeCellGame:
|
|||
self.freecells[self.held.source_index] = cards[0]
|
||||
self.held = None
|
||||
self.clear_hint()
|
||||
self.request_full_repaint()
|
||||
|
||||
def drop(self):
|
||||
if self.held is None:
|
||||
|
|
@ -784,6 +818,7 @@ class FreeCellGame:
|
|||
self.cursor_index + 1,
|
||||
)
|
||||
self.held = None
|
||||
self.request_full_repaint()
|
||||
else:
|
||||
self.status = "Illegal move for tableau column."
|
||||
return
|
||||
|
|
@ -799,6 +834,7 @@ class FreeCellGame:
|
|||
self.cursor_index + 1,
|
||||
)
|
||||
self.held = None
|
||||
self.request_full_repaint()
|
||||
else:
|
||||
self.status = "That free cell is occupied."
|
||||
return
|
||||
|
|
@ -811,6 +847,7 @@ class FreeCellGame:
|
|||
foundation.append(moving_top)
|
||||
self.status = "Moved %s to foundation." % moving_top.short_name()
|
||||
self.held = None
|
||||
self.request_full_repaint()
|
||||
else:
|
||||
self.status = "Illegal move for foundation."
|
||||
|
||||
|
|
@ -833,6 +870,7 @@ class FreeCellGame:
|
|||
self.clear_hint()
|
||||
self.freecells[free_idx] = col.pop()
|
||||
self.status = "Moved card to free cell %d." % (free_idx + 1)
|
||||
self.request_full_repaint()
|
||||
return
|
||||
if self.cursor_zone == "top" and self.cursor_index < 4:
|
||||
self.status = "Already on a free cell."
|
||||
|
|
@ -870,6 +908,7 @@ class FreeCellGame:
|
|||
else:
|
||||
self.freecells[source[1]] = None
|
||||
self.status = "Moved %s to foundation." % card.short_name()
|
||||
self.request_full_repaint()
|
||||
|
||||
def is_won(self):
|
||||
total = 0
|
||||
|
|
@ -991,6 +1030,8 @@ def draw_top_row(stdscr, game, layout):
|
|||
index = 0
|
||||
while index < 4:
|
||||
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
|
||||
hinted = game.hinted_freecell(index)
|
||||
marker = " "
|
||||
|
|
@ -1019,6 +1060,8 @@ def draw_top_row(stdscr, game, layout):
|
|||
index = 0
|
||||
while index < 4:
|
||||
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
|
||||
hinted = game.hinted_foundation(index)
|
||||
marker = " "
|
||||
|
|
@ -1080,6 +1123,8 @@ def draw_columns(stdscr, game, layout):
|
|||
col_idx = 0
|
||||
while col_idx < 8:
|
||||
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
|
||||
hinted_dest = game.hinted_column_destination(col_idx)
|
||||
header_label = str(col_idx + 1)
|
||||
|
|
@ -1093,6 +1138,15 @@ def draw_columns(stdscr, game, layout):
|
|||
header_attr = pair_attr(1)
|
||||
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]
|
||||
if not column:
|
||||
safe_addstr(stdscr, COL_Y + 1, x + 2, ".", pair_attr(1))
|
||||
|
|
@ -1215,6 +1269,7 @@ def render(stdscr, game):
|
|||
stdscr.erase()
|
||||
max_y, max_x = stdscr.getmaxyx()
|
||||
layout = compute_board_layout(max_x)
|
||||
elapsed_text = "Time: %s" % format_elapsed_time(game.elapsed_time_seconds())
|
||||
title = "mcfreecell 1.5.2"
|
||||
if len(title) > max_x - 4:
|
||||
title = "mcfreecell"
|
||||
|
|
@ -1229,6 +1284,18 @@ def render(stdscr, game):
|
|||
max_x - title_x - 1,
|
||||
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:
|
||||
visual_text = "-- VISUAL --"
|
||||
visual_x = title_x + len(title) + 3
|
||||
|
|
@ -1242,7 +1309,10 @@ def render(stdscr, game):
|
|||
draw_status(stdscr, game, max_y, max_x)
|
||||
draw_help(stdscr, max_y, max_x)
|
||||
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()
|
||||
|
||||
|
||||
|
|
@ -1332,6 +1402,8 @@ def handle_key(game, ch):
|
|||
|
||||
|
||||
def curses_main(stdscr):
|
||||
idle_ticks = 0
|
||||
poll_delay = 0
|
||||
if hasattr(curses, "curs_set"):
|
||||
try:
|
||||
curses.curs_set(0)
|
||||
|
|
@ -1342,11 +1414,65 @@ def curses_main(stdscr):
|
|||
stdscr.keypad(1)
|
||||
except:
|
||||
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()
|
||||
game = FreeCellGame()
|
||||
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)
|
||||
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):
|
||||
break
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue