#!/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 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 d : Quick move selected card to foundation Esc : Cancel held move q : Quit """ import curses import time 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 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 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 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, 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.move_history = [] self.visual_mode = 0 self.visual_row = 0 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 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: 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." 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: 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.request_full_repaint() 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, ) self.request_full_repaint() 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() self.request_full_repaint() 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) 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(), self.cursor_index + 1, ) else: self.status = "Placed %d cards on column %d." % ( moving_count, self.cursor_index + 1, ) self.held = None self.request_full_repaint() 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.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, ) self.held = None self.request_full_repaint() 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.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() 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() 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 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.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() 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] 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 = " " 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] 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 = " " 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] 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) 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) 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)) 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 u undo ? 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) elapsed_text = "Time: %s" % format_elapsed_time(game.elapsed_time_seconds()) 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"), ) 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 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(): 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() 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("u") or ch == ord("U"): game.undo_last_move() 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): idle_ticks = 0 poll_delay = 0 if hasattr(curses, "curs_set"): try: curses.curs_set(0) except: pass if hasattr(stdscr, "keypad"): try: 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 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()