From 2600a4028078e9b23c97e8958fe560479c31a64a Mon Sep 17 00:00:00 2001 From: markmental Date: Thu, 26 Mar 2026 20:42:49 -0400 Subject: [PATCH] Adds undo feature --- README.md | 2 + linux-freecell.py | 105 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 87f8962..4b66fae 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ This branch keeps the implementation within a Python-1.5.2-friendly subset and f - FreeCell rules with 8 tableau columns, 4 free cells, and 4 foundations - keyboard-driven curses interface - visual stack selection mode +- single-step undo for completed moves - lightweight `?` hint system - ASCII-only suit tags for old terminals and fonts - compatibility fallbacks for weak or incomplete curses implementations @@ -19,6 +20,7 @@ This branch keeps the implementation within a Python-1.5.2-friendly subset and f - `v` - enter or exit tableau selection mode - `y` - pick up selected card or stack - `p` - drop held card or stack +- `u` - undo the last completed move - `Enter` / `Space` - alternate pick up / drop - `?` - show a suggested move - `f` - quick move selected card to a free cell diff --git a/linux-freecell.py b/linux-freecell.py index 4040f12..1fe99b0 100644 --- a/linux-freecell.py +++ b/linux-freecell.py @@ -16,6 +16,7 @@ Arrow keys / h j k l : Move cursor v : Enter / exit tableau visual selection y : Pick up selected card / stack p : Drop held card / stack +u : Undo the last completed move ? : Show a suggested move Space or Enter : Pick up / drop a card or stack f : Quick move selected card to a free cell @@ -252,6 +253,15 @@ class HintMove: self.score = score +class MoveRecord: + def __init__(self, source_type, source_index, dest_type, dest_index, cards): + self.source_type = source_type + self.source_index = source_index + self.dest_type = dest_type + self.dest_index = dest_index + self.cards = cards + + class BoardLayout: def __init__( self, @@ -277,6 +287,7 @@ class FreeCellGame: self.foundations = [[], [], [], []] self.held = None self.hint_move = None + self.move_history = [] self.visual_mode = 0 self.visual_row = 0 self.cursor_zone = "bottom" @@ -307,6 +318,63 @@ class FreeCellGame: if had_hint: self.request_full_repaint() + def record_move(self, source_type, source_index, dest_type, dest_index, cards): + self.move_history.append( + MoveRecord(source_type, source_index, dest_type, dest_index, cards[:]) + ) + + def _remove_cards_from_destination(self, move): + cards = move.cards + if move.dest_type == "col": + column = self.columns[move.dest_index] + if len(column) < len(cards): + return 0 + if column[-len(cards) :] != cards: + return 0 + del column[-len(cards) :] + return 1 + if move.dest_type == "free": + if self.freecells[move.dest_index] != cards[0]: + return 0 + self.freecells[move.dest_index] = None + return 1 + foundation = self.foundations[move.dest_index] + if not foundation or foundation[-1] != cards[0]: + return 0 + foundation.pop() + return 1 + + def _restore_cards_to_source(self, move): + cards = move.cards + if move.source_type == "col": + self.columns[move.source_index].extend(cards) + return + self.freecells[move.source_index] = cards[0] + + def undo_last_move(self): + if self.held is not None: + self.status = "Drop or cancel the held cards before undoing a move." + return + if not self.move_history: + self.status = "Nothing to undo." + return + move = self.move_history[-1] + if not self._remove_cards_from_destination(move): + self.status = ( + "Undo failed because the board no longer matches the last move." + ) + return + del self.move_history[-1] + self._restore_cards_to_source(move) + self.clear_hint() + if self.visual_mode: + self.exit_visual_mode(None) + self.request_full_repaint() + if len(move.cards) == 1: + self.status = "Undid move of %s." % move.cards[0].short_name() + else: + self.status = "Undid move of %d cards." % len(move.cards) + def _deal_new_game(self): deck = [] for suit in SUITS: @@ -807,6 +875,13 @@ class FreeCellGame: if self.can_place_stack_on_column(cards, self.cursor_index): self.clear_hint() self.columns[self.cursor_index].extend(cards) + self.record_move( + self.held.source_type, + self.held.source_index, + "col", + self.cursor_index, + cards, + ) if moving_count == 1: self.status = "Placed %s on column %d." % ( moving_top.short_name(), @@ -829,6 +904,13 @@ class FreeCellGame: if self.freecells[self.cursor_index] is None: self.clear_hint() self.freecells[self.cursor_index] = moving_top + self.record_move( + self.held.source_type, + self.held.source_index, + "free", + self.cursor_index, + cards, + ) self.status = "Placed %s in free cell %d." % ( moving_top.short_name(), self.cursor_index + 1, @@ -845,6 +927,13 @@ class FreeCellGame: self.clear_hint() foundation = self.foundation_for_suit(moving_top.suit.name) foundation.append(moving_top) + self.record_move( + self.held.source_type, + self.held.source_index, + "foundation", + find_suit_index(moving_top.suit.name), + cards, + ) self.status = "Moved %s to foundation." % moving_top.short_name() self.held = None self.request_full_repaint() @@ -868,7 +957,9 @@ class FreeCellGame: self.status = "Quick freecell move uses only the bottom card." return self.clear_hint() - self.freecells[free_idx] = col.pop() + moved_card = col.pop() + self.freecells[free_idx] = moved_card + self.record_move("col", self.cursor_index, "free", free_idx, [moved_card]) self.status = "Moved card to free cell %d." % (free_idx + 1) self.request_full_repaint() return @@ -907,6 +998,13 @@ class FreeCellGame: self.columns[source[1]].pop() else: self.freecells[source[1]] = None + self.record_move( + source[0], + source[1], + "foundation", + find_suit_index(card.suit.name), + [card], + ) self.status = "Moved %s to foundation." % card.short_name() self.request_full_repaint() @@ -1261,7 +1359,7 @@ def draw_help(stdscr, max_y, max_x): stdscr.clrtoeol() except curses.error: pass - help_text = "Move arrows/hjkl v select y/p/ENT/SPC move ? hint f freecell d foundation Esc cancel q quit" + help_text = "Move arrows/hjkl v select y/p/ENT/SPC move u undo ? hint f freecell d foundation Esc cancel q quit" safe_addnstr(stdscr, y, 2, help_text, max_x - 4, pair_attr(1)) @@ -1382,6 +1480,9 @@ def handle_key(game, ch): else: game.drop() return 1 + if ch == ord("u") or ch == ord("U"): + game.undo_last_move() + return 1 if ch == ord("?"): game.show_hint() return 1