1399 lines
43 KiB
Python
1399 lines
43 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
mcfreecell
|
|
|
|
A curses-based FreeCell game aimed at legacy Python and terminal setups.
|
|
|
|
This branch keeps the game within a Python 1.5.2-friendly subset:
|
|
- no dataclasses
|
|
- no type annotations
|
|
- no f-strings
|
|
- ASCII-only suit tags
|
|
|
|
Controls
|
|
--------
|
|
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
|
|
? : Show a suggested move
|
|
Space or Enter : Pick up / drop a card or stack
|
|
f : Quick move selected card to a free cell
|
|
d : Quick move selected card to foundation
|
|
Esc : Cancel held move
|
|
q : Quit
|
|
"""
|
|
|
|
import curses
|
|
|
|
try:
|
|
import random
|
|
except ImportError:
|
|
random = None
|
|
|
|
try:
|
|
import whrandom # type: ignore
|
|
except ImportError:
|
|
whrandom = None
|
|
|
|
|
|
try:
|
|
enumerate
|
|
except NameError:
|
|
|
|
def enumerate(sequence):
|
|
index = 0
|
|
result = []
|
|
for item in sequence:
|
|
result.append((index, item))
|
|
index = index + 1
|
|
return result
|
|
|
|
|
|
RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
|
|
RANK_VALUES = {}
|
|
_rank_index = 0
|
|
for _rank_name in RANKS:
|
|
_rank_index = _rank_index + 1
|
|
RANK_VALUES[_rank_name] = _rank_index
|
|
del _rank_index
|
|
|
|
|
|
CARD_W = 7
|
|
TOP_Y = 1
|
|
COL_Y = 6
|
|
|
|
|
|
def find_suit_index(suit_name):
|
|
index = 0
|
|
for suit in SUITS:
|
|
if suit.name == suit_name:
|
|
return index
|
|
index = index + 1
|
|
return 0
|
|
|
|
|
|
def count_sequence(sequence):
|
|
count = 0
|
|
for _item in sequence:
|
|
count = count + 1
|
|
return count
|
|
|
|
|
|
def safe_addnstr(stdscr, y, x, text, limit, attr):
|
|
if limit <= 0:
|
|
return
|
|
max_y, max_x = stdscr.getmaxyx()
|
|
if y < 0 or y >= max_y or x >= max_x:
|
|
return
|
|
if x < 0:
|
|
text = text[-x:]
|
|
limit = limit + x
|
|
x = 0
|
|
if limit <= 0:
|
|
return
|
|
width = max_x - x
|
|
if width <= 0:
|
|
return
|
|
if limit > width:
|
|
limit = width
|
|
text = text[:limit]
|
|
try:
|
|
if hasattr(stdscr, "addnstr"):
|
|
stdscr.addnstr(y, x, text, limit, attr)
|
|
else:
|
|
stdscr.addstr(y, x, text, attr)
|
|
except curses.error:
|
|
pass
|
|
|
|
|
|
def safe_addstr(stdscr, y, x, text, attr):
|
|
safe_addnstr(stdscr, y, x, text, len(text), attr)
|
|
|
|
|
|
def pair_attr(index):
|
|
if hasattr(curses, "color_pair"):
|
|
try:
|
|
return curses.color_pair(index)
|
|
except:
|
|
return 0
|
|
return 0
|
|
|
|
|
|
def text_attr(name):
|
|
if hasattr(curses, name):
|
|
try:
|
|
return getattr(curses, name)
|
|
except:
|
|
return 0
|
|
return 0
|
|
|
|
|
|
def highlight_attr():
|
|
attr = text_attr("A_REVERSE")
|
|
if attr:
|
|
return attr
|
|
attr = text_attr("A_STANDOUT")
|
|
if attr:
|
|
return attr
|
|
attr = text_attr("A_BOLD")
|
|
if attr:
|
|
return attr
|
|
attr = text_attr("A_UNDERLINE")
|
|
if attr:
|
|
return attr
|
|
return 0
|
|
|
|
|
|
def pad_right(text, width):
|
|
if width <= 0:
|
|
return ""
|
|
if len(text) > width:
|
|
text = text[:width]
|
|
if len(text) < width:
|
|
text = text + (" " * (width - len(text)))
|
|
return text
|
|
|
|
|
|
def pad_center(text, width):
|
|
if width <= 0:
|
|
return ""
|
|
if len(text) > width:
|
|
text = text[:width]
|
|
pad = width - len(text)
|
|
if pad <= 0:
|
|
return text
|
|
left = int(pad / 2)
|
|
right = pad - left
|
|
return (" " * left) + text + (" " * right)
|
|
|
|
|
|
def rounded_position(start_x, span, index, steps):
|
|
if steps <= 0:
|
|
return start_x
|
|
return start_x + int((float(span) * float(index) / float(steps)) + 0.5)
|
|
|
|
|
|
def legacy_random():
|
|
if random is not None and hasattr(random, "random"):
|
|
return random.random()
|
|
if whrandom is not None and hasattr(whrandom, "random"):
|
|
return whrandom.random()
|
|
return 0.5
|
|
|
|
|
|
def shuffle_in_place(sequence):
|
|
index = len(sequence) - 1
|
|
while index > 0:
|
|
swap_index = int(legacy_random() * (index + 1))
|
|
if swap_index < 0:
|
|
swap_index = 0
|
|
if swap_index > index:
|
|
swap_index = index
|
|
temp = sequence[index]
|
|
sequence[index] = sequence[swap_index]
|
|
sequence[swap_index] = temp
|
|
index = index - 1
|
|
|
|
|
|
class SuitInfo:
|
|
def __init__(self, name, tag, color_group):
|
|
self.name = name
|
|
self.tag = tag
|
|
self.color_group = color_group
|
|
|
|
|
|
class Card:
|
|
def __init__(self, rank, suit):
|
|
self.rank = rank
|
|
self.suit = suit
|
|
|
|
def value(self):
|
|
return RANK_VALUES[self.rank]
|
|
|
|
def color_group(self):
|
|
return self.suit.color_group
|
|
|
|
def short_name(self):
|
|
return self.rank + self.suit.tag
|
|
|
|
|
|
SUITS = [
|
|
SuitInfo("Debian", "Deb", "red"),
|
|
SuitInfo("Red Hat", "RHt", "red"),
|
|
SuitInfo("Solaris", "Sol", "black"),
|
|
SuitInfo("HP-UX", "HPx", "black"),
|
|
]
|
|
|
|
|
|
class HeldCards:
|
|
def __init__(self, cards, source_type, source_index):
|
|
self.cards = cards
|
|
self.source_type = source_type
|
|
self.source_index = source_index
|
|
|
|
|
|
class HintMove:
|
|
def __init__(self, source_type, source_index, dest_type, dest_index, cards, score):
|
|
self.source_type = source_type
|
|
self.source_index = source_index
|
|
self.dest_type = dest_type
|
|
self.dest_index = dest_index
|
|
self.cards = cards
|
|
self.score = score
|
|
|
|
|
|
class BoardLayout:
|
|
def __init__(
|
|
self,
|
|
freecell_xs,
|
|
foundation_xs,
|
|
tableau_xs,
|
|
freecells_label_x,
|
|
foundations_label_x,
|
|
tableau_label_x,
|
|
):
|
|
self.freecell_xs = freecell_xs
|
|
self.foundation_xs = foundation_xs
|
|
self.tableau_xs = tableau_xs
|
|
self.freecells_label_x = freecells_label_x
|
|
self.foundations_label_x = foundations_label_x
|
|
self.tableau_label_x = tableau_label_x
|
|
|
|
|
|
class FreeCellGame:
|
|
def __init__(self):
|
|
self.columns = [[], [], [], [], [], [], [], []]
|
|
self.freecells = [None, None, None, None]
|
|
self.foundations = [[], [], [], []]
|
|
self.held = None
|
|
self.hint_move = None
|
|
self.visual_mode = 0
|
|
self.visual_row = 0
|
|
self.cursor_zone = "bottom"
|
|
self.cursor_index = 0
|
|
self.status = "Arrows move, v selects, ? hints."
|
|
self._deal_new_game()
|
|
|
|
def clear_hint(self):
|
|
self.hint_move = None
|
|
|
|
def _deal_new_game(self):
|
|
deck = []
|
|
for suit in SUITS:
|
|
for rank in RANKS:
|
|
deck.append(Card(rank, suit))
|
|
shuffle_in_place(deck)
|
|
index = 0
|
|
for card in deck:
|
|
self.columns[index % 8].append(card)
|
|
index = index + 1
|
|
|
|
def is_valid_tableau_sequence(self, cards):
|
|
if not cards:
|
|
return 0
|
|
index = 0
|
|
while index < len(cards) - 1:
|
|
upper = cards[index]
|
|
lower = cards[index + 1]
|
|
if upper.value() != lower.value() + 1:
|
|
return 0
|
|
if upper.color_group() == lower.color_group():
|
|
return 0
|
|
index = index + 1
|
|
return 1
|
|
|
|
def count_empty_freecells(self):
|
|
count = 0
|
|
for cell in self.freecells:
|
|
if cell is None:
|
|
count = count + 1
|
|
return count
|
|
|
|
def count_empty_columns(self, exclude_index):
|
|
count = 0
|
|
index = 0
|
|
for column in self.columns:
|
|
if index != exclude_index and not column:
|
|
count = count + 1
|
|
index = index + 1
|
|
return count
|
|
|
|
def max_movable_cards(self, source_col, moving_count):
|
|
if moving_count == len(self.columns[source_col]):
|
|
empty_columns = self.count_empty_columns(source_col)
|
|
else:
|
|
empty_columns = self.count_empty_columns(-1)
|
|
return (self.count_empty_freecells() + 1) * (2**empty_columns)
|
|
|
|
def valid_tail_start_index(self, col_index):
|
|
column = self.columns[col_index]
|
|
if not column:
|
|
return None
|
|
start = len(column) - 1
|
|
while start > 0:
|
|
upper = column[start - 1]
|
|
lower = column[start]
|
|
if upper.value() != lower.value() + 1:
|
|
break
|
|
if upper.color_group() == lower.color_group():
|
|
break
|
|
start = start - 1
|
|
return start
|
|
|
|
def current_visual_stack(self):
|
|
if self.cursor_zone != "bottom":
|
|
return [], "Visual mode only works on tableau columns."
|
|
column = self.columns[self.cursor_index]
|
|
if not column:
|
|
return [], "That column is empty."
|
|
tail_start = self.valid_tail_start_index(self.cursor_index)
|
|
if tail_start is None:
|
|
return [], "That column is empty."
|
|
start_index = self.visual_row
|
|
if start_index < tail_start:
|
|
start_index = tail_start
|
|
if start_index > len(column) - 1:
|
|
start_index = len(column) - 1
|
|
cards = column[start_index:]
|
|
max_cards = self.max_movable_cards(self.cursor_index, len(cards))
|
|
if len(cards) > max_cards:
|
|
return cards, "Move limit is %d cards with current free space." % max_cards
|
|
return cards, ""
|
|
|
|
def selected_stack_from_column(self, col_index):
|
|
column = self.columns[col_index]
|
|
if not column:
|
|
return [], "That column is empty."
|
|
if self.visual_mode and self.cursor_zone == "bottom":
|
|
return self.current_visual_stack()
|
|
return [column[-1]], ""
|
|
|
|
def can_place_stack_on_column(self, cards, col_index):
|
|
if not self.is_valid_tableau_sequence(cards):
|
|
return 0
|
|
column = self.columns[col_index]
|
|
if not column:
|
|
return 1
|
|
moving_top = cards[0]
|
|
destination_top = column[-1]
|
|
if destination_top.value() != moving_top.value() + 1:
|
|
return 0
|
|
if destination_top.color_group() == moving_top.color_group():
|
|
return 0
|
|
return 1
|
|
|
|
def can_move_to_foundation(self, card):
|
|
foundation = self.foundation_for_suit(card.suit.name)
|
|
if not foundation:
|
|
return card.rank == "A"
|
|
top = foundation[-1]
|
|
if top.suit.name != card.suit.name:
|
|
return 0
|
|
return card.value() == top.value() + 1
|
|
|
|
def foundation_for_suit(self, suit_name):
|
|
return self.foundations[find_suit_index(suit_name)]
|
|
|
|
def first_empty_freecell(self):
|
|
index = 0
|
|
for cell in self.freecells:
|
|
if cell is None:
|
|
return index
|
|
index = index + 1
|
|
return None
|
|
|
|
def count_hidden_low_cards(self):
|
|
count = 0
|
|
for column in self.columns:
|
|
index = 0
|
|
while index < len(column) - 1:
|
|
if column[index].value() <= 2:
|
|
count = count + 1
|
|
index = index + 1
|
|
return count
|
|
|
|
def card_can_help_foundation(self, card):
|
|
return card.value() <= 3
|
|
|
|
def describe_hint_move(self, move):
|
|
if move.source_type == "col":
|
|
source_name = "column %d" % (move.source_index + 1)
|
|
else:
|
|
source_name = "free cell %d" % (move.source_index + 1)
|
|
if move.dest_type == "col":
|
|
dest_name = "column %d" % (move.dest_index + 1)
|
|
elif move.dest_type == "free":
|
|
dest_name = "free cell %d" % (move.dest_index + 1)
|
|
else:
|
|
dest_name = "%s foundation" % SUITS[move.dest_index].name
|
|
if len(move.cards) == 1:
|
|
return "move %s from %s to %s" % (
|
|
move.cards[0].short_name(),
|
|
source_name,
|
|
dest_name,
|
|
)
|
|
return "move %d cards %s..%s from %s to %s" % (
|
|
len(move.cards),
|
|
move.cards[0].short_name(),
|
|
move.cards[-1].short_name(),
|
|
source_name,
|
|
dest_name,
|
|
)
|
|
|
|
def score_hint_move(self, source_type, source_index, dest_type, dest_index, cards):
|
|
score = 0
|
|
moving_top = cards[0]
|
|
source_exposes = None
|
|
hidden_low_before = self.count_hidden_low_cards()
|
|
if source_type == "col":
|
|
source_column = self.columns[source_index]
|
|
remaining = source_column[: len(source_column) - len(cards)]
|
|
if remaining:
|
|
source_exposes = remaining[-1]
|
|
else:
|
|
score = score + 250
|
|
if source_exposes is not None and self.card_can_help_foundation(
|
|
source_exposes
|
|
):
|
|
score = score + 320
|
|
if dest_type == "foundation":
|
|
score = score + 1000 + moving_top.value() * 5
|
|
elif dest_type == "col":
|
|
destination = self.columns[dest_index]
|
|
if destination:
|
|
score = score + 220 + len(cards) * 25
|
|
if len(cards) > 1:
|
|
score = score + 120
|
|
else:
|
|
if len(cards) > 1:
|
|
score = score + 60
|
|
else:
|
|
score = score - 120
|
|
elif dest_type == "free":
|
|
score = score - 80
|
|
if moving_top.value() <= 3:
|
|
score = score + 140
|
|
if source_exposes is not None and self.can_move_to_foundation(source_exposes):
|
|
score = score + 180
|
|
if moving_top.value() <= 3:
|
|
score = score + 80
|
|
hidden_low_after = hidden_low_before
|
|
if (
|
|
source_type == "col"
|
|
and source_exposes is not None
|
|
and source_exposes.value() <= 2
|
|
):
|
|
hidden_low_after = hidden_low_after - 1
|
|
score = score + (hidden_low_before - hidden_low_after) * 120
|
|
if source_type == "free" and dest_type == "col":
|
|
score = score + 200
|
|
return score
|
|
|
|
def enumerate_hint_moves(self):
|
|
moves = []
|
|
col_index = 0
|
|
for column in self.columns:
|
|
if column:
|
|
top_card = column[-1]
|
|
suit_index = find_suit_index(top_card.suit.name)
|
|
if self.can_move_to_foundation(top_card):
|
|
moves.append(
|
|
HintMove(
|
|
"col",
|
|
col_index,
|
|
"foundation",
|
|
suit_index,
|
|
[top_card],
|
|
self.score_hint_move(
|
|
"col", col_index, "foundation", suit_index, [top_card]
|
|
),
|
|
)
|
|
)
|
|
free_idx = self.first_empty_freecell()
|
|
if free_idx is not None:
|
|
moves.append(
|
|
HintMove(
|
|
"col",
|
|
col_index,
|
|
"free",
|
|
free_idx,
|
|
[top_card],
|
|
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 not None:
|
|
start_index = len(column) - 1
|
|
while start_index >= tail_start:
|
|
cards_list = column[start_index:]
|
|
if len(cards_list) <= self.max_movable_cards(
|
|
col_index, len(cards_list)
|
|
):
|
|
dest_index = 0
|
|
while dest_index < 8:
|
|
if (
|
|
dest_index != col_index
|
|
and self.can_place_stack_on_column(
|
|
cards_list, dest_index
|
|
)
|
|
):
|
|
moves.append(
|
|
HintMove(
|
|
"col",
|
|
col_index,
|
|
"col",
|
|
dest_index,
|
|
cards_list[:],
|
|
self.score_hint_move(
|
|
"col",
|
|
col_index,
|
|
"col",
|
|
dest_index,
|
|
cards_list,
|
|
),
|
|
)
|
|
)
|
|
dest_index = dest_index + 1
|
|
start_index = start_index - 1
|
|
col_index = col_index + 1
|
|
|
|
free_index = 0
|
|
for card in self.freecells:
|
|
if card is not None:
|
|
suit_index = find_suit_index(card.suit.name)
|
|
if self.can_move_to_foundation(card):
|
|
moves.append(
|
|
HintMove(
|
|
"free",
|
|
free_index,
|
|
"foundation",
|
|
suit_index,
|
|
[card],
|
|
self.score_hint_move(
|
|
"free", free_index, "foundation", suit_index, [card]
|
|
),
|
|
)
|
|
)
|
|
dest_index = 0
|
|
while dest_index < 8:
|
|
if self.can_place_stack_on_column([card], dest_index):
|
|
moves.append(
|
|
HintMove(
|
|
"free",
|
|
free_index,
|
|
"col",
|
|
dest_index,
|
|
[card],
|
|
self.score_hint_move(
|
|
"free", free_index, "col", dest_index, [card]
|
|
),
|
|
)
|
|
)
|
|
dest_index = dest_index + 1
|
|
free_index = free_index + 1
|
|
|
|
decorated = []
|
|
for move in moves:
|
|
decorated.append((0 - move.score, self.describe_hint_move(move), move))
|
|
decorated.sort()
|
|
result = []
|
|
for item in decorated:
|
|
result.append(item[2])
|
|
return result
|
|
|
|
def show_hint(self):
|
|
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 = "Hint: %s." % self.describe_hint_move(self.hint_move)
|
|
|
|
def hinted_freecell(self, index):
|
|
if self.hint_move is None:
|
|
return 0
|
|
if (
|
|
self.hint_move.source_type == "free"
|
|
and self.hint_move.source_index == index
|
|
):
|
|
return 1
|
|
if self.hint_move.dest_type == "free" and self.hint_move.dest_index == index:
|
|
return 1
|
|
return 0
|
|
|
|
def hinted_foundation(self, index):
|
|
if self.hint_move is None:
|
|
return 0
|
|
if (
|
|
self.hint_move.dest_type == "foundation"
|
|
and self.hint_move.dest_index == index
|
|
):
|
|
return 1
|
|
return 0
|
|
|
|
def hinted_column_source_start(self, index):
|
|
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):
|
|
if self.hint_move is None:
|
|
return 0
|
|
if self.hint_move.dest_type == "col" and self.hint_move.dest_index == index:
|
|
return 1
|
|
return 0
|
|
|
|
def enter_visual_mode(self):
|
|
if self.held is not None:
|
|
self.status = "Drop the held cards before selecting a new stack."
|
|
return
|
|
if self.cursor_zone != "bottom":
|
|
self.status = "Visual mode only works on tableau columns."
|
|
return
|
|
column = self.columns[self.cursor_index]
|
|
if not column:
|
|
self.status = "That column is empty."
|
|
return
|
|
self.visual_mode = 1
|
|
self.visual_row = len(column) - 1
|
|
cards, reason = self.current_visual_stack()
|
|
if reason:
|
|
self.status = "Visual: %d cards selected. %s" % (len(cards), reason)
|
|
elif len(cards) == 1:
|
|
self.status = "Visual: 1 card selected."
|
|
else:
|
|
self.status = "Visual: %d cards selected." % len(cards)
|
|
|
|
def exit_visual_mode(self, message):
|
|
self.visual_mode = 0
|
|
self.visual_row = 0
|
|
if message is not None:
|
|
self.status = message
|
|
|
|
def move_visual_selection(self, delta):
|
|
if not self.visual_mode:
|
|
self.status = "Press v to start selecting a stack."
|
|
return
|
|
column = self.columns[self.cursor_index]
|
|
tail_start = self.valid_tail_start_index(self.cursor_index)
|
|
if not column or tail_start is None:
|
|
self.exit_visual_mode("That column is empty.")
|
|
return
|
|
old_row = self.visual_row
|
|
self.visual_row = self.visual_row + delta
|
|
if self.visual_row < tail_start:
|
|
self.visual_row = tail_start
|
|
if self.visual_row > len(column) - 1:
|
|
self.visual_row = len(column) - 1
|
|
cards, reason = self.current_visual_stack()
|
|
if self.visual_row == old_row:
|
|
if len(cards) == 1:
|
|
self.status = "Visual selection stays at 1 card."
|
|
else:
|
|
self.status = "Visual selection stays at %d cards." % len(cards)
|
|
return
|
|
if reason:
|
|
self.status = "Visual: %d cards selected. %s" % (len(cards), reason)
|
|
elif len(cards) == 1:
|
|
self.status = "Visual: 1 card selected."
|
|
else:
|
|
self.status = "Visual: %d cards selected." % len(cards)
|
|
|
|
def pick_up(self):
|
|
if self.held is not None:
|
|
self.status = "You are already holding cards."
|
|
return
|
|
if self.cursor_zone == "bottom":
|
|
cards, reason = self.selected_stack_from_column(self.cursor_index)
|
|
if not cards:
|
|
self.status = reason
|
|
return
|
|
if reason:
|
|
self.status = reason
|
|
return
|
|
moving_count = len(cards)
|
|
self.clear_hint()
|
|
del self.columns[self.cursor_index][-moving_count:]
|
|
self.held = HeldCards(cards[:], "col", self.cursor_index)
|
|
if moving_count == 1:
|
|
self.status = "Picked up %s from column %d." % (
|
|
cards[0].short_name(),
|
|
self.cursor_index + 1,
|
|
)
|
|
else:
|
|
self.status = "Picked up %d cards from column %d." % (
|
|
moving_count,
|
|
self.cursor_index + 1,
|
|
)
|
|
self.exit_visual_mode(None)
|
|
return
|
|
if self.cursor_index < 4:
|
|
card = self.freecells[self.cursor_index]
|
|
if card is None:
|
|
self.status = "That free cell is empty."
|
|
return
|
|
self.clear_hint()
|
|
self.freecells[self.cursor_index] = None
|
|
self.held = HeldCards([card], "free", self.cursor_index)
|
|
self.status = "Picked up %s from free cell %d." % (
|
|
card.short_name(),
|
|
self.cursor_index + 1,
|
|
)
|
|
return
|
|
self.status = "Cannot pick up directly from foundations."
|
|
|
|
def restore_held(self):
|
|
if self.held is None:
|
|
return
|
|
cards = self.held.cards
|
|
if self.held.source_type == "col":
|
|
self.columns[self.held.source_index].extend(cards)
|
|
elif self.held.source_type == "free":
|
|
self.freecells[self.held.source_index] = cards[0]
|
|
self.held = None
|
|
self.clear_hint()
|
|
|
|
def drop(self):
|
|
if self.held is None:
|
|
self.status = "You are not holding a card."
|
|
return
|
|
cards = self.held.cards
|
|
moving_count = len(cards)
|
|
moving_top = cards[0]
|
|
if self.cursor_zone == "bottom":
|
|
if self.can_place_stack_on_column(cards, self.cursor_index):
|
|
self.clear_hint()
|
|
self.columns[self.cursor_index].extend(cards)
|
|
if moving_count == 1:
|
|
self.status = "Placed %s on column %d." % (
|
|
moving_top.short_name(),
|
|
self.cursor_index + 1,
|
|
)
|
|
else:
|
|
self.status = "Placed %d cards on column %d." % (
|
|
moving_count,
|
|
self.cursor_index + 1,
|
|
)
|
|
self.held = None
|
|
else:
|
|
self.status = "Illegal move for tableau column."
|
|
return
|
|
if self.cursor_index < 4:
|
|
if moving_count != 1:
|
|
self.status = "Free cells accept only one card."
|
|
return
|
|
if self.freecells[self.cursor_index] is None:
|
|
self.clear_hint()
|
|
self.freecells[self.cursor_index] = moving_top
|
|
self.status = "Placed %s in free cell %d." % (
|
|
moving_top.short_name(),
|
|
self.cursor_index + 1,
|
|
)
|
|
self.held = None
|
|
else:
|
|
self.status = "That free cell is occupied."
|
|
return
|
|
if moving_count != 1:
|
|
self.status = "Foundations accept only one card at a time."
|
|
return
|
|
if self.can_move_to_foundation(moving_top):
|
|
self.clear_hint()
|
|
foundation = self.foundation_for_suit(moving_top.suit.name)
|
|
foundation.append(moving_top)
|
|
self.status = "Moved %s to foundation." % moving_top.short_name()
|
|
self.held = None
|
|
else:
|
|
self.status = "Illegal move for foundation."
|
|
|
|
def quick_to_freecell(self):
|
|
if self.held is not None:
|
|
self.status = "Drop the held card first."
|
|
return
|
|
free_idx = self.first_empty_freecell()
|
|
if free_idx is None:
|
|
self.status = "No free freecells available."
|
|
return
|
|
if self.cursor_zone == "bottom":
|
|
col = self.columns[self.cursor_index]
|
|
if not col:
|
|
self.status = "That column is empty."
|
|
return
|
|
if self.visual_mode:
|
|
self.status = "Quick freecell move uses only the bottom card."
|
|
return
|
|
self.clear_hint()
|
|
self.freecells[free_idx] = col.pop()
|
|
self.status = "Moved card to free cell %d." % (free_idx + 1)
|
|
return
|
|
if self.cursor_zone == "top" and self.cursor_index < 4:
|
|
self.status = "Already on a free cell."
|
|
return
|
|
self.status = "Select a tableau card first."
|
|
|
|
def quick_to_foundation(self):
|
|
if self.held is not None:
|
|
self.status = "Drop the held card first."
|
|
return
|
|
card = None
|
|
source = None
|
|
if self.cursor_zone == "bottom":
|
|
col = self.columns[self.cursor_index]
|
|
if col:
|
|
if self.visual_mode:
|
|
self.status = "Quick foundation move uses only the bottom card."
|
|
return
|
|
card = col[-1]
|
|
source = ("col", self.cursor_index)
|
|
elif self.cursor_zone == "top" and self.cursor_index < 4:
|
|
card = self.freecells[self.cursor_index]
|
|
if card is not None:
|
|
source = ("free", self.cursor_index)
|
|
if card is None or source is None:
|
|
self.status = "No movable card selected."
|
|
return
|
|
if not self.can_move_to_foundation(card):
|
|
self.status = "That card cannot go to foundation yet."
|
|
return
|
|
self.clear_hint()
|
|
self.foundation_for_suit(card.suit.name).append(card)
|
|
if source[0] == "col":
|
|
self.columns[source[1]].pop()
|
|
else:
|
|
self.freecells[source[1]] = None
|
|
self.status = "Moved %s to foundation." % card.short_name()
|
|
|
|
def is_won(self):
|
|
total = 0
|
|
for foundation in self.foundations:
|
|
total = total + len(foundation)
|
|
return total == 52
|
|
|
|
|
|
def spread_positions(count, start_x, end_x):
|
|
positions = []
|
|
if count <= 1:
|
|
return [start_x]
|
|
span = end_x - start_x
|
|
if span < 0:
|
|
span = 0
|
|
steps = count - 1
|
|
index = 0
|
|
while index < count:
|
|
positions.append(rounded_position(start_x, span, index, steps))
|
|
index = index + 1
|
|
return positions
|
|
|
|
|
|
def compute_board_layout(max_x):
|
|
left_margin = 2
|
|
right_margin = max_x - CARD_W - 2
|
|
if right_margin < left_margin:
|
|
right_margin = left_margin
|
|
if max_x < 72:
|
|
top_positions = spread_positions(8, left_margin, right_margin)
|
|
freecell_xs = top_positions[:4]
|
|
foundation_xs = top_positions[4:8]
|
|
else:
|
|
center_gap = max_x / 6
|
|
if center_gap < 10:
|
|
center_gap = 10
|
|
if center_gap > 20:
|
|
center_gap = 20
|
|
left_end = (max_x / 2) - (center_gap / 2) - CARD_W
|
|
right_start = (max_x / 2) + (center_gap / 2)
|
|
if left_end < left_margin:
|
|
left_end = left_margin
|
|
if right_start > right_margin:
|
|
right_start = right_margin
|
|
freecell_xs = spread_positions(4, left_margin, left_end)
|
|
foundation_xs = spread_positions(4, right_start, right_margin)
|
|
tableau_xs = spread_positions(8, left_margin, right_margin)
|
|
return BoardLayout(
|
|
freecell_xs,
|
|
foundation_xs,
|
|
tableau_xs,
|
|
freecell_xs[0],
|
|
foundation_xs[0],
|
|
tableau_xs[0],
|
|
)
|
|
|
|
|
|
def init_colors():
|
|
if not hasattr(curses, "start_color"):
|
|
return
|
|
if not hasattr(curses, "init_pair"):
|
|
return
|
|
try:
|
|
curses.start_color()
|
|
except:
|
|
return
|
|
if hasattr(curses, "use_default_colors"):
|
|
try:
|
|
curses.use_default_colors()
|
|
except:
|
|
pass
|
|
if not hasattr(curses, "COLOR_WHITE"):
|
|
return
|
|
try:
|
|
curses.init_pair(1, curses.COLOR_WHITE, -1)
|
|
curses.init_pair(2, curses.COLOR_RED, -1)
|
|
curses.init_pair(3, curses.COLOR_CYAN, -1)
|
|
curses.init_pair(4, curses.COLOR_BLACK, curses.COLOR_YELLOW)
|
|
curses.init_pair(5, curses.COLOR_BLACK, curses.COLOR_GREEN)
|
|
curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_CYAN)
|
|
except:
|
|
pass
|
|
|
|
|
|
def card_color_pair(card):
|
|
if card.color_group() == "red":
|
|
return 2
|
|
return 3
|
|
|
|
|
|
def draw_box(stdscr, y, x, w, label, attr):
|
|
inner = pad_center(label[: w - 2], w - 2)
|
|
text = "[" + inner + "]"
|
|
safe_addnstr(stdscr, y, x, text, w, attr)
|
|
|
|
|
|
def draw_card_label(card):
|
|
if card is None:
|
|
return " "
|
|
return card.short_name()
|
|
|
|
|
|
def draw_top_row(stdscr, game, layout):
|
|
safe_addstr(
|
|
stdscr,
|
|
TOP_Y,
|
|
layout.freecells_label_x,
|
|
"Free Cells",
|
|
pair_attr(1) | text_attr("A_BOLD"),
|
|
)
|
|
safe_addstr(
|
|
stdscr,
|
|
TOP_Y,
|
|
layout.foundations_label_x,
|
|
"Foundations",
|
|
pair_attr(1) | text_attr("A_BOLD"),
|
|
)
|
|
|
|
index = 0
|
|
while index < 4:
|
|
x = layout.freecell_xs[index]
|
|
selected = game.cursor_zone == "top" and game.cursor_index == index
|
|
hinted = game.hinted_freecell(index)
|
|
marker = " "
|
|
if selected:
|
|
attr = pair_attr(4) | highlight_attr()
|
|
marker = ">"
|
|
elif hinted:
|
|
attr = pair_attr(6) | text_attr("A_BOLD")
|
|
marker = "+"
|
|
else:
|
|
attr = pair_attr(1)
|
|
safe_addstr(stdscr, TOP_Y + 1, x - 1, marker, attr)
|
|
card = game.freecells[index]
|
|
draw_box(stdscr, TOP_Y + 1, x, CARD_W, draw_card_label(card), attr)
|
|
if card is not None:
|
|
safe_addnstr(
|
|
stdscr,
|
|
TOP_Y + 1,
|
|
x + 1,
|
|
card.short_name(),
|
|
CARD_W - 2,
|
|
pair_attr(card_color_pair(card)),
|
|
)
|
|
index = index + 1
|
|
|
|
index = 0
|
|
while index < 4:
|
|
x = layout.foundation_xs[index]
|
|
selected = game.cursor_zone == "top" and game.cursor_index == index + 4
|
|
hinted = game.hinted_foundation(index)
|
|
marker = " "
|
|
if selected:
|
|
attr = pair_attr(4) | highlight_attr()
|
|
marker = ">"
|
|
elif hinted:
|
|
attr = pair_attr(6) | text_attr("A_BOLD")
|
|
marker = "+"
|
|
else:
|
|
attr = pair_attr(1)
|
|
safe_addstr(stdscr, TOP_Y + 1, x - 1, marker, attr)
|
|
foundation = game.foundations[index]
|
|
if foundation:
|
|
top_card = foundation[-1]
|
|
label = draw_card_label(top_card)
|
|
else:
|
|
top_card = None
|
|
label = SUITS[index].tag
|
|
draw_box(stdscr, TOP_Y + 1, x, CARD_W, label, attr)
|
|
if top_card is not None:
|
|
safe_addnstr(
|
|
stdscr,
|
|
TOP_Y + 1,
|
|
x + 1,
|
|
top_card.short_name(),
|
|
CARD_W - 2,
|
|
pair_attr(card_color_pair(top_card)),
|
|
)
|
|
else:
|
|
if SUITS[index].color_group == "red":
|
|
dummy_color = 2
|
|
else:
|
|
dummy_color = 3
|
|
safe_addnstr(
|
|
stdscr,
|
|
TOP_Y + 1,
|
|
x + 1,
|
|
SUITS[index].tag,
|
|
CARD_W - 2,
|
|
pair_attr(dummy_color),
|
|
)
|
|
index = index + 1
|
|
|
|
|
|
def draw_columns(stdscr, game, layout):
|
|
safe_addstr(
|
|
stdscr,
|
|
COL_Y - 1,
|
|
layout.tableau_label_x,
|
|
"Tableau",
|
|
pair_attr(1) | text_attr("A_BOLD"),
|
|
)
|
|
max_height = 0
|
|
for column in game.columns:
|
|
if len(column) > max_height:
|
|
max_height = len(column)
|
|
|
|
col_idx = 0
|
|
while col_idx < 8:
|
|
x = layout.tableau_xs[col_idx]
|
|
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)
|
|
if selected:
|
|
header_attr = pair_attr(4) | highlight_attr()
|
|
header_label = ">" + header_label
|
|
elif hinted_dest:
|
|
header_attr = pair_attr(6) | text_attr("A_BOLD")
|
|
header_label = "+" + header_label
|
|
else:
|
|
header_attr = pair_attr(1)
|
|
draw_box(stdscr, COL_Y, x, CARD_W, header_label, header_attr)
|
|
|
|
column = game.columns[col_idx]
|
|
if not column:
|
|
safe_addstr(stdscr, COL_Y + 1, x + 2, ".", pair_attr(1))
|
|
col_idx = col_idx + 1
|
|
continue
|
|
|
|
selected_cards = []
|
|
selection_reason = ""
|
|
valid_tail_start = game.valid_tail_start_index(col_idx)
|
|
if valid_tail_start is None:
|
|
valid_tail_start = len(column)
|
|
hint_source_start = game.hinted_column_source_start(col_idx)
|
|
if selected and game.visual_mode:
|
|
selected_cards, selection_reason = game.selected_stack_from_column(col_idx)
|
|
if selected_cards:
|
|
selected_start = len(column) - len(selected_cards)
|
|
else:
|
|
selected_start = len(column)
|
|
|
|
row_idx = 0
|
|
for card in column:
|
|
y = COL_Y + 1 + row_idx
|
|
in_valid_tail = (
|
|
selected and game.visual_mode and row_idx >= valid_tail_start
|
|
)
|
|
in_selected_stack = (
|
|
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
|
|
)
|
|
marker = " "
|
|
if in_selected_stack:
|
|
attr = pair_attr(4) | highlight_attr()
|
|
marker = ">"
|
|
if selection_reason:
|
|
attr = attr | text_attr("A_DIM")
|
|
else:
|
|
attr = attr | text_attr("A_BOLD")
|
|
elif in_hint_source:
|
|
attr = pair_attr(6) | text_attr("A_BOLD")
|
|
marker = "+"
|
|
elif in_valid_tail:
|
|
attr = pair_attr(card_color_pair(card)) | text_attr("A_DIM")
|
|
elif selected and row_idx == len(column) - 1:
|
|
attr = pair_attr(4) | highlight_attr()
|
|
marker = ">"
|
|
else:
|
|
attr = pair_attr(card_color_pair(card))
|
|
safe_addstr(stdscr, y, x - 1, marker, attr)
|
|
safe_addnstr(
|
|
stdscr,
|
|
y,
|
|
x + 1,
|
|
pad_right(card.short_name(), CARD_W - 2),
|
|
CARD_W - 2,
|
|
attr,
|
|
)
|
|
row_idx = row_idx + 1
|
|
|
|
col_idx = col_idx + 1
|
|
|
|
extra_y = COL_Y + 2 + max_height
|
|
while extra_y < COL_Y + 8 + max_height:
|
|
try:
|
|
stdscr.move(extra_y, 0)
|
|
stdscr.clrtoeol()
|
|
except curses.error:
|
|
pass
|
|
extra_y = extra_y + 1
|
|
|
|
|
|
def draw_held(stdscr, game, max_y, max_x):
|
|
y = max_y - 3
|
|
try:
|
|
stdscr.move(y, 0)
|
|
stdscr.clrtoeol()
|
|
except curses.error:
|
|
pass
|
|
if game.held is None:
|
|
safe_addstr(stdscr, y, 2, "Held: (none)", pair_attr(1))
|
|
return
|
|
safe_addstr(stdscr, y, 2, "Held: ", pair_attr(1))
|
|
cards = game.held.cards
|
|
if len(cards) == 1:
|
|
safe_addstr(
|
|
stdscr, y, 8, cards[0].short_name(), pair_attr(5) | text_attr("A_BOLD")
|
|
)
|
|
return
|
|
summary = "%d cards (%s..%s)" % (
|
|
len(cards),
|
|
cards[0].short_name(),
|
|
cards[-1].short_name(),
|
|
)
|
|
safe_addnstr(stdscr, y, 8, summary, max_x - 10, pair_attr(5) | text_attr("A_BOLD"))
|
|
|
|
|
|
def draw_status(stdscr, game, max_y, max_x):
|
|
y = max_y - 2
|
|
try:
|
|
stdscr.move(y, 0)
|
|
stdscr.clrtoeol()
|
|
except curses.error:
|
|
pass
|
|
safe_addnstr(stdscr, y, 2, game.status, max_x - 4, pair_attr(1))
|
|
|
|
|
|
def draw_help(stdscr, max_y, max_x):
|
|
y = max_y - 1
|
|
try:
|
|
stdscr.move(y, 0)
|
|
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"
|
|
safe_addnstr(stdscr, y, 2, help_text, max_x - 4, pair_attr(1))
|
|
|
|
|
|
def render(stdscr, game):
|
|
stdscr.erase()
|
|
max_y, max_x = stdscr.getmaxyx()
|
|
layout = compute_board_layout(max_x)
|
|
title = "mcfreecell 1.5.2"
|
|
if len(title) > max_x - 4:
|
|
title = "mcfreecell"
|
|
title_x = int((max_x - len(title)) / 2)
|
|
if title_x < 2:
|
|
title_x = 2
|
|
safe_addnstr(
|
|
stdscr,
|
|
0,
|
|
title_x,
|
|
title,
|
|
max_x - title_x - 1,
|
|
pair_attr(1) | text_attr("A_BOLD"),
|
|
)
|
|
if game.visual_mode:
|
|
visual_text = "-- VISUAL --"
|
|
visual_x = title_x + len(title) + 3
|
|
if visual_x + len(visual_text) < max_x - 1:
|
|
safe_addstr(
|
|
stdscr, 0, visual_x, visual_text, pair_attr(5) | text_attr("A_BOLD")
|
|
)
|
|
draw_top_row(stdscr, game, layout)
|
|
draw_columns(stdscr, game, layout)
|
|
draw_held(stdscr, game, max_y, max_x)
|
|
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"))
|
|
stdscr.refresh()
|
|
|
|
|
|
def move_cursor(game, dy, dx):
|
|
if dy < 0:
|
|
if game.visual_mode and game.cursor_zone == "bottom":
|
|
game.move_visual_selection(-1)
|
|
return
|
|
game.cursor_zone = "top"
|
|
if game.cursor_index > 7:
|
|
game.cursor_index = 7
|
|
return
|
|
if dy > 0:
|
|
if game.visual_mode and game.cursor_zone == "bottom":
|
|
game.move_visual_selection(1)
|
|
return
|
|
game.cursor_zone = "bottom"
|
|
if game.cursor_index > 7:
|
|
game.cursor_index = 7
|
|
return
|
|
if game.visual_mode and dx != 0:
|
|
game.exit_visual_mode("Visual selection cancelled.")
|
|
return
|
|
game.cursor_index = game.cursor_index + dx
|
|
if game.cursor_index < 0:
|
|
game.cursor_index = 0
|
|
if game.cursor_index > 7:
|
|
game.cursor_index = 7
|
|
|
|
|
|
def handle_key(game, ch):
|
|
if ch == ord("q") or ch == ord("Q"):
|
|
return 0
|
|
if ch == curses.KEY_LEFT or ch == ord("h"):
|
|
move_cursor(game, 0, -1)
|
|
return 1
|
|
if ch == curses.KEY_RIGHT or ch == ord("l"):
|
|
move_cursor(game, 0, 1)
|
|
return 1
|
|
if ch == curses.KEY_UP or ch == ord("k"):
|
|
move_cursor(game, -1, 0)
|
|
return 1
|
|
if ch == curses.KEY_DOWN or ch == ord("j"):
|
|
move_cursor(game, 1, 0)
|
|
return 1
|
|
if ch == ord("v") or ch == ord("V"):
|
|
if game.visual_mode:
|
|
game.exit_visual_mode("Visual selection cancelled.")
|
|
else:
|
|
game.enter_visual_mode()
|
|
return 1
|
|
if ch == curses.KEY_ENTER or ch == 10 or ch == 13 or ch == ord(" "):
|
|
if game.held is None:
|
|
game.pick_up()
|
|
else:
|
|
game.drop()
|
|
return 1
|
|
if ch == ord("y") or ch == ord("Y"):
|
|
if game.held is not None:
|
|
game.status = "You are already holding cards."
|
|
else:
|
|
game.pick_up()
|
|
return 1
|
|
if ch == ord("p") or ch == ord("P"):
|
|
if game.held is None:
|
|
game.status = "You are not holding any cards."
|
|
else:
|
|
game.drop()
|
|
return 1
|
|
if ch == ord("?"):
|
|
game.show_hint()
|
|
return 1
|
|
if ch == ord("f") or ch == ord("F"):
|
|
game.quick_to_freecell()
|
|
return 1
|
|
if ch == ord("d") or ch == ord("D"):
|
|
game.quick_to_foundation()
|
|
return 1
|
|
if ch == 27:
|
|
if game.visual_mode:
|
|
game.exit_visual_mode("Visual selection cancelled.")
|
|
elif game.held is not None:
|
|
game.restore_held()
|
|
game.status = "Cancelled move."
|
|
return 1
|
|
return 1
|
|
|
|
|
|
def curses_main(stdscr):
|
|
if hasattr(curses, "curs_set"):
|
|
try:
|
|
curses.curs_set(0)
|
|
except:
|
|
pass
|
|
if hasattr(stdscr, "keypad"):
|
|
try:
|
|
stdscr.keypad(1)
|
|
except:
|
|
pass
|
|
init_colors()
|
|
game = FreeCellGame()
|
|
while 1:
|
|
render(stdscr, game)
|
|
ch = stdscr.getch()
|
|
if not handle_key(game, ch):
|
|
break
|
|
|
|
|
|
def run_curses(main_func):
|
|
if hasattr(curses, "wrapper"):
|
|
curses.wrapper(main_func)
|
|
return
|
|
stdscr = curses.initscr()
|
|
if hasattr(curses, "noecho"):
|
|
try:
|
|
curses.noecho()
|
|
except:
|
|
pass
|
|
if hasattr(curses, "cbreak"):
|
|
try:
|
|
curses.cbreak()
|
|
except:
|
|
pass
|
|
try:
|
|
main_func(stdscr)
|
|
finally:
|
|
if hasattr(curses, "nocbreak"):
|
|
try:
|
|
curses.nocbreak()
|
|
except:
|
|
pass
|
|
if hasattr(stdscr, "keypad"):
|
|
try:
|
|
stdscr.keypad(not 1)
|
|
except:
|
|
pass
|
|
if hasattr(curses, "echo"):
|
|
try:
|
|
curses.echo()
|
|
except:
|
|
pass
|
|
if hasattr(curses, "endwin"):
|
|
try:
|
|
curses.endwin()
|
|
except:
|
|
pass
|
|
|
|
|
|
def main():
|
|
run_curses(curses_main)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|