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 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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue