Adds undo feature
This commit is contained in:
parent
6ca7417011
commit
2600a40280
2 changed files with 105 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue