diff --git a/README.md b/README.md new file mode 100644 index 0000000..87f8962 --- /dev/null +++ b/README.md @@ -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. diff --git a/linux-freecell.py b/linux-freecell.py index 384f2f2..4040f12 100644 --- a/linux-freecell.py +++ b/linux-freecell.py @@ -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