Add basic hint system
This commit is contained in:
parent
9aa5c5ef06
commit
08292b2aea
1 changed files with 315 additions and 5 deletions
|
|
@ -10,6 +10,7 @@ Arrow keys / h j k l : Move cursor
|
||||||
v : Enter / exit tableau visual selection
|
v : Enter / exit tableau visual selection
|
||||||
y : Pick up selected card / stack
|
y : Pick up selected card / stack
|
||||||
p : Drop held card / stack
|
p : Drop held card / stack
|
||||||
|
? : Show a suggested move
|
||||||
Space or Enter : Pick up / drop a card or stack
|
Space or Enter : Pick up / drop a card or stack
|
||||||
f : Quick move selected card to a free cell
|
f : Quick move selected card to a free cell
|
||||||
d : Quick move selected card to foundation
|
d : Quick move selected card to foundation
|
||||||
|
|
@ -96,6 +97,18 @@ class HeldCards:
|
||||||
source_index: int
|
source_index: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class HintMove:
|
||||||
|
"""Describes one suggested legal move for the current board."""
|
||||||
|
|
||||||
|
source_type: str
|
||||||
|
source_index: int
|
||||||
|
dest_type: str
|
||||||
|
dest_index: int
|
||||||
|
cards: Tuple[Card, ...]
|
||||||
|
score: int
|
||||||
|
|
||||||
|
|
||||||
class FreeCellGame:
|
class FreeCellGame:
|
||||||
"""Holds the state and rules for a small FreeCell prototype."""
|
"""Holds the state and rules for a small FreeCell prototype."""
|
||||||
|
|
||||||
|
|
@ -104,6 +117,7 @@ class FreeCellGame:
|
||||||
self.freecells: List[Optional[Card]] = [None] * 4
|
self.freecells: List[Optional[Card]] = [None] * 4
|
||||||
self.foundations: List[List[Card]] = [[] for _ in range(4)]
|
self.foundations: List[List[Card]] = [[] for _ in range(4)]
|
||||||
self.held: Optional[HeldCards] = None
|
self.held: Optional[HeldCards] = None
|
||||||
|
self.hint_move: Optional[HintMove] = None
|
||||||
self.visual_mode = False
|
self.visual_mode = False
|
||||||
self.visual_row = 0
|
self.visual_row = 0
|
||||||
|
|
||||||
|
|
@ -113,9 +127,13 @@ class FreeCellGame:
|
||||||
self.cursor_zone = "bottom"
|
self.cursor_zone = "bottom"
|
||||||
self.cursor_index = 0
|
self.cursor_index = 0
|
||||||
|
|
||||||
self.status = "Arrow keys move, v selects a stack, Enter picks up/drops."
|
self.status = "Arrow keys move, v selects a stack, ? shows a hint."
|
||||||
self._deal_new_game()
|
self._deal_new_game()
|
||||||
|
|
||||||
|
def clear_hint(self) -> None:
|
||||||
|
"""Drop the currently remembered hint highlight."""
|
||||||
|
self.hint_move = None
|
||||||
|
|
||||||
def _deal_new_game(self) -> None:
|
def _deal_new_game(self) -> None:
|
||||||
"""Build and shuffle a standard 52-card deck, then deal to 8 columns."""
|
"""Build and shuffle a standard 52-card deck, then deal to 8 columns."""
|
||||||
deck: List[Card] = [Card(rank, suit) for suit in SUITS for rank in RANKS]
|
deck: List[Card] = [Card(rank, suit) for suit in SUITS for rank in RANKS]
|
||||||
|
|
@ -269,6 +287,256 @@ class FreeCellGame:
|
||||||
return i
|
return i
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def count_hidden_low_cards(self) -> int:
|
||||||
|
"""Count aces and twos that are not currently exposed."""
|
||||||
|
count = 0
|
||||||
|
for column in self.columns:
|
||||||
|
for card in column[:-1]:
|
||||||
|
if card.value <= 2:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
def card_can_help_foundation(self, card: Card) -> bool:
|
||||||
|
"""Return True for low cards that usually unlock early progress."""
|
||||||
|
return card.value <= 3
|
||||||
|
|
||||||
|
def describe_hint_move(self, move: HintMove) -> str:
|
||||||
|
"""Return a compact human-readable hint description."""
|
||||||
|
if move.source_type == "col":
|
||||||
|
source_name = f"column {move.source_index + 1}"
|
||||||
|
else:
|
||||||
|
source_name = f"free cell {move.source_index + 1}"
|
||||||
|
|
||||||
|
if move.dest_type == "col":
|
||||||
|
dest_name = f"column {move.dest_index + 1}"
|
||||||
|
elif move.dest_type == "free":
|
||||||
|
dest_name = f"free cell {move.dest_index + 1}"
|
||||||
|
else:
|
||||||
|
dest_name = f"{SUITS[move.dest_index].name} foundation"
|
||||||
|
|
||||||
|
if len(move.cards) == 1:
|
||||||
|
return (
|
||||||
|
f"move {move.cards[0].short_name()} from {source_name} to {dest_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"move {len(move.cards)} cards {move.cards[0].short_name()}..{move.cards[-1].short_name()} "
|
||||||
|
f"from {source_name} to {dest_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def score_hint_move(
|
||||||
|
self,
|
||||||
|
source_type: str,
|
||||||
|
source_index: int,
|
||||||
|
dest_type: str,
|
||||||
|
dest_index: int,
|
||||||
|
cards: List[Card],
|
||||||
|
) -> int:
|
||||||
|
"""Assign a cheap deterministic score to a legal move."""
|
||||||
|
score = 0
|
||||||
|
moving_top = cards[0]
|
||||||
|
|
||||||
|
source_column = self.columns[source_index] if source_type == "col" else None
|
||||||
|
source_exposes = None
|
||||||
|
hidden_low_before = self.count_hidden_low_cards()
|
||||||
|
|
||||||
|
if source_type == "col":
|
||||||
|
assert source_column is not None
|
||||||
|
remaining = source_column[: -len(cards)]
|
||||||
|
source_exposes = remaining[-1] if remaining else None
|
||||||
|
if not remaining:
|
||||||
|
score += 250
|
||||||
|
if source_exposes and self.card_can_help_foundation(source_exposes):
|
||||||
|
score += 320
|
||||||
|
|
||||||
|
if dest_type == "foundation":
|
||||||
|
score += 1000 + moving_top.value * 5
|
||||||
|
elif dest_type == "col":
|
||||||
|
destination = self.columns[dest_index]
|
||||||
|
if destination:
|
||||||
|
score += 220 + len(cards) * 25
|
||||||
|
if len(cards) > 1:
|
||||||
|
score += 120
|
||||||
|
else:
|
||||||
|
score += 60 if len(cards) > 1 else -120
|
||||||
|
elif dest_type == "free":
|
||||||
|
score -= 80
|
||||||
|
if moving_top.value <= 3:
|
||||||
|
score += 140
|
||||||
|
|
||||||
|
if source_exposes and self.can_move_to_foundation(source_exposes):
|
||||||
|
score += 180
|
||||||
|
|
||||||
|
if moving_top.value <= 3:
|
||||||
|
score += 80
|
||||||
|
|
||||||
|
hidden_low_after = hidden_low_before
|
||||||
|
if source_type == "col" and source_exposes and source_exposes.value <= 2:
|
||||||
|
hidden_low_after -= 1
|
||||||
|
score += (hidden_low_before - hidden_low_after) * 120
|
||||||
|
|
||||||
|
if source_type == "free" and dest_type == "col":
|
||||||
|
score += 200
|
||||||
|
|
||||||
|
return score
|
||||||
|
|
||||||
|
def enumerate_hint_moves(self) -> List[HintMove]:
|
||||||
|
"""Enumerate cheap one-ply legal moves for hinting."""
|
||||||
|
moves: List[HintMove] = []
|
||||||
|
|
||||||
|
for col_index, column in enumerate(self.columns):
|
||||||
|
if not column:
|
||||||
|
continue
|
||||||
|
|
||||||
|
top_card = column[-1]
|
||||||
|
if self.can_move_to_foundation(top_card):
|
||||||
|
cards = (top_card,)
|
||||||
|
moves.append(
|
||||||
|
HintMove(
|
||||||
|
source_type="col",
|
||||||
|
source_index=col_index,
|
||||||
|
dest_type="foundation",
|
||||||
|
dest_index=SUITS.index(top_card.suit),
|
||||||
|
cards=cards,
|
||||||
|
score=self.score_hint_move(
|
||||||
|
"col",
|
||||||
|
col_index,
|
||||||
|
"foundation",
|
||||||
|
SUITS.index(top_card.suit),
|
||||||
|
[top_card],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
free_idx = self.first_empty_freecell()
|
||||||
|
if free_idx is not None:
|
||||||
|
cards = (top_card,)
|
||||||
|
moves.append(
|
||||||
|
HintMove(
|
||||||
|
source_type="col",
|
||||||
|
source_index=col_index,
|
||||||
|
dest_type="free",
|
||||||
|
dest_index=free_idx,
|
||||||
|
cards=cards,
|
||||||
|
score=self.score_hint_move(
|
||||||
|
"col", col_index, "free", free_idx, [top_card]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
tail_start = self.valid_tail_start_index(col_index)
|
||||||
|
if tail_start is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for start_index in range(len(column) - 1, tail_start - 1, -1):
|
||||||
|
cards_list = column[start_index:]
|
||||||
|
if len(cards_list) > self.max_movable_cards(col_index, len(cards_list)):
|
||||||
|
continue
|
||||||
|
for dest_index in range(8):
|
||||||
|
if dest_index == col_index:
|
||||||
|
continue
|
||||||
|
if self.can_place_stack_on_column(cards_list, dest_index):
|
||||||
|
moves.append(
|
||||||
|
HintMove(
|
||||||
|
source_type="col",
|
||||||
|
source_index=col_index,
|
||||||
|
dest_type="col",
|
||||||
|
dest_index=dest_index,
|
||||||
|
cards=tuple(cards_list),
|
||||||
|
score=self.score_hint_move(
|
||||||
|
"col", col_index, "col", dest_index, cards_list
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for free_index, card in enumerate(self.freecells):
|
||||||
|
if card is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.can_move_to_foundation(card):
|
||||||
|
moves.append(
|
||||||
|
HintMove(
|
||||||
|
source_type="free",
|
||||||
|
source_index=free_index,
|
||||||
|
dest_type="foundation",
|
||||||
|
dest_index=SUITS.index(card.suit),
|
||||||
|
cards=(card,),
|
||||||
|
score=self.score_hint_move(
|
||||||
|
"free",
|
||||||
|
free_index,
|
||||||
|
"foundation",
|
||||||
|
SUITS.index(card.suit),
|
||||||
|
[card],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for dest_index in range(8):
|
||||||
|
if self.can_place_stack_on_column([card], dest_index):
|
||||||
|
moves.append(
|
||||||
|
HintMove(
|
||||||
|
source_type="free",
|
||||||
|
source_index=free_index,
|
||||||
|
dest_type="col",
|
||||||
|
dest_index=dest_index,
|
||||||
|
cards=(card,),
|
||||||
|
score=self.score_hint_move(
|
||||||
|
"free", free_index, "col", dest_index, [card]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
moves.sort(key=lambda move: (-move.score, self.describe_hint_move(move)))
|
||||||
|
return moves
|
||||||
|
|
||||||
|
def show_hint(self) -> None:
|
||||||
|
"""Pick the best cheap legal move and remember it for highlighting."""
|
||||||
|
if self.held is not None:
|
||||||
|
self.status = "Drop or cancel the held cards before asking for a hint."
|
||||||
|
return
|
||||||
|
|
||||||
|
moves = self.enumerate_hint_moves()
|
||||||
|
if not moves:
|
||||||
|
self.hint_move = None
|
||||||
|
self.status = "Hint: no obvious legal move found."
|
||||||
|
return
|
||||||
|
|
||||||
|
self.hint_move = moves[0]
|
||||||
|
self.status = f"Hint: {self.describe_hint_move(self.hint_move)}."
|
||||||
|
|
||||||
|
def hinted_freecell(self, index: int) -> bool:
|
||||||
|
"""Return True when a freecell is part of the current hint."""
|
||||||
|
if self.hint_move is None:
|
||||||
|
return False
|
||||||
|
return (
|
||||||
|
self.hint_move.source_type == "free"
|
||||||
|
and self.hint_move.source_index == index
|
||||||
|
) or (self.hint_move.dest_type == "free" and self.hint_move.dest_index == index)
|
||||||
|
|
||||||
|
def hinted_foundation(self, index: int) -> bool:
|
||||||
|
"""Return True when a foundation is the current hint destination."""
|
||||||
|
return (
|
||||||
|
self.hint_move is not None
|
||||||
|
and self.hint_move.dest_type == "foundation"
|
||||||
|
and self.hint_move.dest_index == index
|
||||||
|
)
|
||||||
|
|
||||||
|
def hinted_column_source_start(self, index: int) -> Optional[int]:
|
||||||
|
"""Return the start row for a hinted tableau source stack."""
|
||||||
|
if self.hint_move is None:
|
||||||
|
return None
|
||||||
|
if self.hint_move.source_type != "col" or self.hint_move.source_index != index:
|
||||||
|
return None
|
||||||
|
return len(self.columns[index]) - len(self.hint_move.cards)
|
||||||
|
|
||||||
|
def hinted_column_destination(self, index: int) -> bool:
|
||||||
|
"""Return True when a tableau column is the hinted destination."""
|
||||||
|
return (
|
||||||
|
self.hint_move is not None
|
||||||
|
and self.hint_move.dest_type == "col"
|
||||||
|
and self.hint_move.dest_index == index
|
||||||
|
)
|
||||||
|
|
||||||
def enter_visual_mode(self) -> None:
|
def enter_visual_mode(self) -> None:
|
||||||
"""Enter tableau visual selection mode on the current column."""
|
"""Enter tableau visual selection mode on the current column."""
|
||||||
if self.held is not None:
|
if self.held is not None:
|
||||||
|
|
@ -352,6 +620,7 @@ class FreeCellGame:
|
||||||
return
|
return
|
||||||
|
|
||||||
moving_count = len(cards)
|
moving_count = len(cards)
|
||||||
|
self.clear_hint()
|
||||||
del self.columns[self.cursor_index][-moving_count:]
|
del self.columns[self.cursor_index][-moving_count:]
|
||||||
self.held = HeldCards(
|
self.held = HeldCards(
|
||||||
cards=cards, source_type="col", source_index=self.cursor_index
|
cards=cards, source_type="col", source_index=self.cursor_index
|
||||||
|
|
@ -369,6 +638,7 @@ class FreeCellGame:
|
||||||
if card is None:
|
if card is None:
|
||||||
self.status = "That free cell is empty."
|
self.status = "That free cell is empty."
|
||||||
return
|
return
|
||||||
|
self.clear_hint()
|
||||||
self.freecells[self.cursor_index] = None
|
self.freecells[self.cursor_index] = None
|
||||||
self.held = HeldCards(
|
self.held = HeldCards(
|
||||||
cards=[card], source_type="free", source_index=self.cursor_index
|
cards=[card], source_type="free", source_index=self.cursor_index
|
||||||
|
|
@ -392,6 +662,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()
|
||||||
|
|
||||||
def drop(self) -> None:
|
def drop(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -411,6 +682,7 @@ class FreeCellGame:
|
||||||
|
|
||||||
if self.cursor_zone == "bottom":
|
if self.cursor_zone == "bottom":
|
||||||
if self.can_place_stack_on_column(cards, self.cursor_index):
|
if self.can_place_stack_on_column(cards, self.cursor_index):
|
||||||
|
self.clear_hint()
|
||||||
self.columns[self.cursor_index].extend(cards)
|
self.columns[self.cursor_index].extend(cards)
|
||||||
if moving_count == 1:
|
if moving_count == 1:
|
||||||
self.status = f"Placed {moving_top.short_name()} on column {self.cursor_index + 1}."
|
self.status = f"Placed {moving_top.short_name()} on column {self.cursor_index + 1}."
|
||||||
|
|
@ -428,6 +700,7 @@ class FreeCellGame:
|
||||||
self.status = "Free cells accept only one card."
|
self.status = "Free cells accept only one card."
|
||||||
return
|
return
|
||||||
if self.freecells[self.cursor_index] is None:
|
if self.freecells[self.cursor_index] is None:
|
||||||
|
self.clear_hint()
|
||||||
self.freecells[self.cursor_index] = moving_top
|
self.freecells[self.cursor_index] = moving_top
|
||||||
self.status = f"Placed {moving_top.short_name()} in free cell {self.cursor_index + 1}."
|
self.status = f"Placed {moving_top.short_name()} in free cell {self.cursor_index + 1}."
|
||||||
self.held = None
|
self.held = None
|
||||||
|
|
@ -441,6 +714,7 @@ class FreeCellGame:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.can_move_to_foundation(moving_top):
|
if self.can_move_to_foundation(moving_top):
|
||||||
|
self.clear_hint()
|
||||||
foundation = self.foundation_for_suit(moving_top.suit.name)
|
foundation = self.foundation_for_suit(moving_top.suit.name)
|
||||||
foundation.append(moving_top)
|
foundation.append(moving_top)
|
||||||
self.status = f"Moved {moving_top.short_name()} to foundation."
|
self.status = f"Moved {moving_top.short_name()} to foundation."
|
||||||
|
|
@ -470,6 +744,7 @@ class FreeCellGame:
|
||||||
if self.visual_mode:
|
if self.visual_mode:
|
||||||
self.status = "Quick freecell move uses only the bottom card."
|
self.status = "Quick freecell move uses only the bottom card."
|
||||||
return
|
return
|
||||||
|
self.clear_hint()
|
||||||
self.freecells[free_idx] = col.pop()
|
self.freecells[free_idx] = col.pop()
|
||||||
self.status = f"Moved card to free cell {free_idx + 1}."
|
self.status = f"Moved card to free cell {free_idx + 1}."
|
||||||
return
|
return
|
||||||
|
|
@ -518,6 +793,7 @@ class FreeCellGame:
|
||||||
return
|
return
|
||||||
|
|
||||||
foundation = self.foundation_for_suit(card.suit.name)
|
foundation = self.foundation_for_suit(card.suit.name)
|
||||||
|
self.clear_hint()
|
||||||
foundation.append(card)
|
foundation.append(card)
|
||||||
|
|
||||||
if source[0] == "col":
|
if source[0] == "col":
|
||||||
|
|
@ -561,6 +837,9 @@ def init_colors() -> None:
|
||||||
# Pair 5: held card
|
# Pair 5: held card
|
||||||
curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_GREEN)
|
curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_GREEN)
|
||||||
|
|
||||||
|
# Pair 6: hint highlight
|
||||||
|
curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
||||||
|
|
||||||
|
|
||||||
def card_color_pair(card: Card) -> int:
|
def card_color_pair(card: Card) -> int:
|
||||||
"""Choose a color pair for a card."""
|
"""Choose a color pair for a card."""
|
||||||
|
|
@ -589,7 +868,14 @@ def draw_top_row(stdscr, game: FreeCellGame) -> None:
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
x = 2 + i * (CARD_W + 1)
|
x = 2 + i * (CARD_W + 1)
|
||||||
selected = game.cursor_zone == "top" and game.cursor_index == i
|
selected = game.cursor_zone == "top" and game.cursor_index == i
|
||||||
attr = curses.color_pair(4) if selected else curses.color_pair(1)
|
hinted = game.hinted_freecell(i)
|
||||||
|
attr = (
|
||||||
|
curses.color_pair(4)
|
||||||
|
if selected
|
||||||
|
else curses.color_pair(6)
|
||||||
|
if hinted
|
||||||
|
else curses.color_pair(1)
|
||||||
|
)
|
||||||
|
|
||||||
card = game.freecells[i]
|
card = game.freecells[i]
|
||||||
draw_box(stdscr, TOP_Y, x, CARD_W, draw_card_label(card), attr)
|
draw_box(stdscr, TOP_Y, x, CARD_W, draw_card_label(card), attr)
|
||||||
|
|
@ -607,7 +893,14 @@ def draw_top_row(stdscr, game: FreeCellGame) -> None:
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
x = 40 + i * (CARD_W + 1)
|
x = 40 + i * (CARD_W + 1)
|
||||||
selected = game.cursor_zone == "top" and game.cursor_index == i + 4
|
selected = game.cursor_zone == "top" and game.cursor_index == i + 4
|
||||||
attr = curses.color_pair(4) if selected else curses.color_pair(1)
|
hinted = game.hinted_foundation(i)
|
||||||
|
attr = (
|
||||||
|
curses.color_pair(4)
|
||||||
|
if selected
|
||||||
|
else curses.color_pair(6)
|
||||||
|
if hinted
|
||||||
|
else curses.color_pair(1)
|
||||||
|
)
|
||||||
|
|
||||||
foundation = game.foundations[i]
|
foundation = game.foundations[i]
|
||||||
top_card = foundation[-1] if foundation else None
|
top_card = foundation[-1] if foundation else None
|
||||||
|
|
@ -639,7 +932,14 @@ def draw_columns(stdscr, game: FreeCellGame) -> None:
|
||||||
for col_idx in range(8):
|
for col_idx in range(8):
|
||||||
x = 2 + col_idx * 9
|
x = 2 + col_idx * 9
|
||||||
selected = game.cursor_zone == "bottom" and game.cursor_index == col_idx
|
selected = game.cursor_zone == "bottom" and game.cursor_index == col_idx
|
||||||
header_attr = curses.color_pair(4) if selected else curses.color_pair(1)
|
hinted_dest = game.hinted_column_destination(col_idx)
|
||||||
|
header_attr = (
|
||||||
|
curses.color_pair(4)
|
||||||
|
if selected
|
||||||
|
else curses.color_pair(6)
|
||||||
|
if hinted_dest
|
||||||
|
else curses.color_pair(1)
|
||||||
|
)
|
||||||
|
|
||||||
draw_box(stdscr, COL_Y, x, CARD_W, str(col_idx + 1), header_attr)
|
draw_box(stdscr, COL_Y, x, CARD_W, str(col_idx + 1), header_attr)
|
||||||
|
|
||||||
|
|
@ -653,6 +953,7 @@ def draw_columns(stdscr, game: FreeCellGame) -> None:
|
||||||
valid_tail_start = game.valid_tail_start_index(col_idx)
|
valid_tail_start = game.valid_tail_start_index(col_idx)
|
||||||
if valid_tail_start is None:
|
if valid_tail_start is None:
|
||||||
valid_tail_start = len(column)
|
valid_tail_start = len(column)
|
||||||
|
hint_source_start = game.hinted_column_source_start(col_idx)
|
||||||
|
|
||||||
if selected and game.visual_mode:
|
if selected and game.visual_mode:
|
||||||
selected_cards, selection_reason = game.selected_stack_from_column(col_idx)
|
selected_cards, selection_reason = game.selected_stack_from_column(col_idx)
|
||||||
|
|
@ -670,12 +971,17 @@ def draw_columns(stdscr, game: FreeCellGame) -> None:
|
||||||
in_selected_stack = (
|
in_selected_stack = (
|
||||||
selected and game.visual_mode and row_idx >= selected_start
|
selected and game.visual_mode and row_idx >= selected_start
|
||||||
)
|
)
|
||||||
|
in_hint_source = (
|
||||||
|
hint_source_start is not None and row_idx >= hint_source_start
|
||||||
|
)
|
||||||
if in_selected_stack:
|
if in_selected_stack:
|
||||||
attr = curses.color_pair(4)
|
attr = curses.color_pair(4)
|
||||||
if selection_reason:
|
if selection_reason:
|
||||||
attr |= curses.A_DIM
|
attr |= curses.A_DIM
|
||||||
else:
|
else:
|
||||||
attr |= curses.A_BOLD
|
attr |= curses.A_BOLD
|
||||||
|
elif in_hint_source:
|
||||||
|
attr = curses.color_pair(6) | curses.A_BOLD
|
||||||
elif in_valid_tail:
|
elif in_valid_tail:
|
||||||
attr = curses.color_pair(card_color_pair(card)) | curses.A_DIM
|
attr = curses.color_pair(card_color_pair(card)) | curses.A_DIM
|
||||||
elif selected and row_idx == len(column) - 1:
|
elif selected and row_idx == len(column) - 1:
|
||||||
|
|
@ -728,7 +1034,7 @@ def draw_help(stdscr, max_y: int) -> None:
|
||||||
y = max_y - 1
|
y = max_y - 1
|
||||||
stdscr.move(y, 0)
|
stdscr.move(y, 0)
|
||||||
stdscr.clrtoeol()
|
stdscr.clrtoeol()
|
||||||
help_text = "Move: arrows/hjkl v: visual y/p: pick/drop Enter/Space: pick/drop f: freecell d: foundation Esc: cancel q: quit"
|
help_text = "Move: arrows/hjkl v: visual y/p: pick/drop ?: hint Enter/Space: pick/drop f: freecell d: foundation Esc: cancel q: quit"
|
||||||
stdscr.addnstr(y, 2, help_text, max(0, curses.COLS - 4), curses.color_pair(1))
|
stdscr.addnstr(y, 2, help_text, max(0, curses.COLS - 4), curses.color_pair(1))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -845,6 +1151,10 @@ def handle_key(game: FreeCellGame, ch: int) -> bool:
|
||||||
game.drop()
|
game.drop()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if ch == ord("?"):
|
||||||
|
game.show_hint()
|
||||||
|
return True
|
||||||
|
|
||||||
# Quick helpers
|
# Quick helpers
|
||||||
if ch in (ord("f"), ord("F")):
|
if ch in (ord("f"), ord("F")):
|
||||||
game.quick_to_freecell()
|
game.quick_to_freecell()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue