I am writing a text-based freecell game in python 3. I am looking for a way to generate hints. A hint should simply be all the possible moves that you can make, excluding moving a card to a free cell (because you can always do that). It should also handle moving stacks of cards (stacks are lists of cards where each card is of one higher rank and opposite color than the one below it) to other columns. Unfortunately, because there are so many cases to handle that are very similar but slightly different, (moving cards from the tableau to foundation, free cell to foundation, free cell to tableau, tableau to tableau) I feel like my code is being repeated a lot, and I find myself copying and pasting a few lines of codes from time to time. Here is my current code:

class SimpleHint:
"""A simple class for generating the possible moves you could make. Note that it does not consider moves that
involve moving a card *into* a free cell (though it does show moves of free cells coming out)."""
def __init__(self, board):
self.board = board

def get_board_hints(self):
"""Get hints just from the tableau, moving cards from one pile to another"""
free_cells_num = sum(map(lambda x: x is None, self.board.freecells))
# We have to check every combination of stacks with every other position on the board
# Note: stack = cards of increasing rank and alternating color which can be moved
for ind1, pile1 in enumerate(self.board.piles):
# This is the maximum possible length a stack could be
max_stack_len = min(free_cells_num+1, len(pile1))
card_ind1 = CardIndex(self.board, ind1, CardIndex.TYPE_MAIN)
for bottom_stack in range(max_stack_len):
card1 = pile1[-(bottom_stack+1)]
# If the current card is not one higher in rank and of opposite color, then the stack is broken
if bottom_stack > 0 and (pile1[-bottom_stack].rank + 1 != card1.rank
or pile1[-bottom_stack].color == card1.color):
break
# Now, we check every possible column to put it in
for ind2, pile2 in enumerate(self.board.piles):
if pile1 is pile2:
continue

card_ind2 = CardIndex(self.board, ind2, CardIndex.TYPE_MAIN)
# We know the stack starting at card1 can be placed at card_ind2
# Now we just have to compile a list of moves that would correctly move the stack
available_cells = list(filter(lambda x: self.board.freecells[x] is None, range(NUM_FREECELLS)))
indices = []
for c in range(bottom_stack):
free_cell = CardIndex(self.board, available_cells[c], CardIndex.TYPE_FREECELL)
indices.append((card_ind1, free_cell))
indices.append((card_ind1, card_ind2))
for c in range(bottom_stack):
free_cell = CardIndex(self.board, available_cells[bottom_stack - c - 1], CardIndex.TYPE_FREECELL)
indices.append((free_cell, card_ind2))
yield MovePoss(indices)

def get_other_hints(self):
"""Gets hints from moving from a free cell to the tableau, the tableau to a foundation,
or a free cell to a foundation"""
card_ind1 = CardIndex(self.board, free_ind, CardIndex.TYPE_FREECELL)
card1 = card_ind1.peek_card()
if card1 is None:
continue
for foundation_ind in range(NUM_FOUNDATIONS):
card_ind2 = CardIndex(self.board, foundation_ind, CardIndex.TYPE_FOUNDATION)
yield MovePoss([(card_ind1, card_ind2)])
if card1.rank == 0:
break
for ind, pile in enumerate(self.board.piles):
if len(pile) == 0:
continue
card_ind1 = CardIndex(self.board, ind, CardIndex.TYPE_MAIN)
card1 = card_ind1.peek_card()
card_ind2 = CardIndex(self.board, free_ind, CardIndex.TYPE_FREECELL)
yield MovePoss([(card_ind2, card_ind1)])
for foundation_ind in range(NUM_FOUNDATIONS):
card_ind2 = CardIndex(self.board, foundation_ind, CardIndex.TYPE_FOUNDATION)
yield MovePoss([(card_ind1, card_ind2)])
# If the card is an ace, it can go in any foundation, but we save space by only displaying it once
if card1.rank == 0:
break

def get_hints(self):
for hint in self.get_other_hints():
yield hint
for hint in self.get_board_hints():
yield hint


There are a few custom classes here. CardIndex is a class that keeps a reference to the location of a card (there may or may not be any actual cards there). In addition, board.piles[n], represents the nth column and contains a list of Cards, each of which have a suit, rank, and color. This is the full code in the file:

import random
CARD_TEXT_LEN = 10
NUM_FREECELLS = 4
NUM_FOUNDATIONS = 4

help_text = (
"""This is freecell, a card game. The objective is to fill up the foundations. This is accomplished by stacking the main
board by alternating color (white/black) and in descending order (King through Ace). You can also use the free cells to
hold cards which are blocking columns and bring them back later.

Your input should be two numbers separated by a space like this:

>>> Choose some cards to move: 1 7

This would move the last card from the 1st column to the 7th column (everything is 1-indexed).
To move a card to a free cell, use the suffix c:

>>> Choose some cards to move: 1 4c

would move the card from the 1st column to free cell 4.
Similarly, to move a card to a foundation, use the suffix f.
You can use semicolons to send multiple statements. For example, to move the bottom three cards
from column 1 to column two, you could do

>>> Choose some cards to move: 1 1c; 1 2c; 1 2; 2c 2; 1c 2

You can also type sh or "simple hint" to get a hint""")

class Card:
"""A card part of a standard deck of 52 cards. Has a suit and rank."""
def __init__(self, rank, suit):
self.rank = rank
# For suit ordering: 0=white heart, 1=black clubs, 2=white diamonds, 3=black spades
self.suit = suit
self.color = self.suit % 2 == 0

def to_str(self, force_to=CARD_TEXT_LEN):
# Note that because rank is 0-indexed but when displaying, it should be 1-indexed, it is necessary
# to add one. Thus, 12 actually represents King (not Queen) and 0 represents Ace.
suitname = ["♡", "♣", "♢", "♠"][self.suit]
if self.rank >= 10:
rankname = ["J", "Q", "K"][self.rank - 10]
elif self.rank == 0:
rankname = "A"
else:
rankname = str(self.rank + 1)
spaces = " " * (force_to - len(suitname) - len(rankname) - 1) if force_to != -1 else ""
return rankname + " " + suitname + spaces

class CardIndex:
"""This class is for referencing a particular card in either the main board,
the free cell area, or the foundations."""

TYPE_MAIN = 0
TYPE_FREECELL = 1
TYPE_FOUNDATION = 2

def __init__(self, board, index, type):
self.board = board
self.index = index
self.type = type

def peek_card(self):
"""Returns the card's value without modifying anything. May return None if there is no card where the index
is currently referencing."""
if self.type == self.TYPE_MAIN:
pile = self.board.piles[self.index]
return None if len(pile) == 0 else pile[-1]
elif self.type == self.TYPE_FREECELL:
return self.board.freecells[self.index]
elif self.type == self.TYPE_FOUNDATION:
return self.board.foundations[self.index]

def pop_card(self):
"""Returns the card and removes it from its previous location."""
if self.type == self.TYPE_MAIN:
return self.board.piles[self.index].pop()
elif self.type == self.TYPE_FREECELL:
card = self.board.freecells[self.index]
self.board.freecells[self.index] = None
return card
elif self.type == self.TYPE_FOUNDATION:
raise ValueError("Cannot remove cards from foundations")

"""Determines if it is possible to move 'card' into the location described by this index."""
if card is None:
return False
prev_card = self.peek_card()
if self.type == self.TYPE_MAIN:
return prev_card is None or (prev_card.rank - 1 == card.rank and prev_card.color != card.color)
elif self.type == self.TYPE_FREECELL:
return prev_card is None
elif self.type == self.TYPE_FOUNDATION:
# If the card is an ace or one rank higher and same suit as previous card
return (prev_card is None and card.rank == 0) \
or (prev_card is not None and prev_card.rank + 1 == card.rank and prev_card.suit == card.suit)

"""Adds the card 'card' into this index. This assumes that checks have already been done to make sure
that this is legal. No further checks are done here."""
if self.type == self.TYPE_MAIN:
self.board.piles[self.index].append(card)
elif self.type == self.TYPE_FREECELL:
self.board.freecells[self.index] = card
elif self.type == self.TYPE_FOUNDATION:
self.board.foundations[self.index] = card

def __str__(self):
suffix = ["", "c", "f"][self.type]
return str(self.index + 1) + suffix

class Board:
def __init__(self, board_copy=None):
if board_copy is None:
cards = []
for rank in range(13):
for suit in range(4):
cards.append(Card(rank, suit))
random.shuffle(cards)
# Columns of cards
self.piles = []
# Add 4 piles of 7 cards
for i in range(0, 28, 7):
self.piles.append(cards[i:i+7])
# Add 4 piles of 6 cards
for i in range(28, 52, 6):
self.piles.append(cards[i:i+6])
self.freecells = [None] * NUM_FREECELLS
self.foundations = [None] * NUM_FOUNDATIONS
else:
self.piles = board_copy.piles[:]
self.freecells = board_copy.freecells[:]
self.foundations = board_copy.foundations[:]

def move_card(self, origin, dest):
raise ValueError("Cannot move cards")

def __str__(self):
def concatenate_list_cards(card_list):
return (" "*4).join(map(lambda x: str(x) if x is None else x.to_str(force_to=-1), card_list))
free_cell_str = concatenate_list_cards(self.freecells)
foundations_str = concatenate_list_cards(self.foundations)
max_len = max(map(len, self.piles))
transposed = [[" " * CARD_TEXT_LEN] * len(self.piles) for _ in range(max_len)]
for pile_num, pile in enumerate(self.piles):
for index, card in enumerate(pile):
transposed[index][pile_num] = card.to_str()
main_board = "\n".join(map("".join, transposed))
return "Free cells: " + free_cell_str + " "*20 + "Foundations: " + foundations_str + "\n\n" + main_board

class MovePoss:
"""Move possibility"""
def __init__(self, indices):
self.indices = indices

def perform_on_board(self):
for index in self.indices:

def __str__(self):
return "; ".join(map(lambda x: str(x[0])+" "+str(x[1]), self.indices))

class SimpleHint:
"""A simple class for generating the possible moves you could make. Note that it does not consider moves that
involve moving a card *into* a free cell (though it does show moves of free cells coming out)."""
def __init__(self, board):
self.board = board

def get_board_hints(self):
"""Get hints just from the tableau, moving cards from one pile to another"""
free_cells_num = sum(map(lambda x: x is None, self.board.freecells))
# We have to check every combination of stacks with every other position on the board
# Note: stack = cards of increasing rank and alternating color which can be moved
for ind1, pile1 in enumerate(self.board.piles):
# This is the maximum possible length a stack could be
max_stack_len = min(free_cells_num+1, len(pile1))
card_ind1 = CardIndex(self.board, ind1, CardIndex.TYPE_MAIN)
for bottom_stack in range(max_stack_len):
card1 = pile1[-(bottom_stack+1)]
# If the current card is not one higher in rank and of opposite color, then the stack is broken
if bottom_stack > 0 and (pile1[-bottom_stack].rank + 1 != card1.rank
or pile1[-bottom_stack].color == card1.color):
break
# Now, we check every possible column to put it in
for ind2, pile2 in enumerate(self.board.piles):
if pile1 is pile2:
continue

card_ind2 = CardIndex(self.board, ind2, CardIndex.TYPE_MAIN)
# We know the stack starting at card1 can be placed at card_ind2
# Now we just have to compile a list of moves that would correctly move the stack
available_cells = list(filter(lambda x: self.board.freecells[x] is None, range(NUM_FREECELLS)))
indices = []
for c in range(bottom_stack):
free_cell = CardIndex(self.board, available_cells[c], CardIndex.TYPE_FREECELL)
indices.append((card_ind1, free_cell))
indices.append((card_ind1, card_ind2))
for c in range(bottom_stack):
free_cell = CardIndex(self.board, available_cells[bottom_stack - c - 1], CardIndex.TYPE_FREECELL)
indices.append((free_cell, card_ind2))
yield MovePoss(indices)

def get_other_hints(self):
"""Gets hints from moving from a free cell to the tableau, the tableau to a foundation,
or a free cell to a foundation"""
card_ind1 = CardIndex(self.board, free_ind, CardIndex.TYPE_FREECELL)
card1 = card_ind1.peek_card()
if card1 is None:
continue
for foundation_ind in range(NUM_FOUNDATIONS):
card_ind2 = CardIndex(self.board, foundation_ind, CardIndex.TYPE_FOUNDATION)
yield MovePoss([(card_ind1, card_ind2)])
if card1.rank == 0:
break
for ind, pile in enumerate(self.board.piles):
if len(pile) == 0:
continue
card_ind1 = CardIndex(self.board, ind, CardIndex.TYPE_MAIN)
card1 = card_ind1.peek_card()
card_ind2 = CardIndex(self.board, free_ind, CardIndex.TYPE_FREECELL)
yield MovePoss([(card_ind2, card_ind1)])
for foundation_ind in range(NUM_FOUNDATIONS):
card_ind2 = CardIndex(self.board, foundation_ind, CardIndex.TYPE_FOUNDATION)
yield MovePoss([(card_ind1, card_ind2)])
# If the card is an ace, it can go in any foundation, but we save space by only displaying it once
if card1.rank == 0:
break

def get_hints(self):
for hint in self.get_other_hints():
yield hint
for hint in self.get_board_hints():
yield hint

def run_input_on_board(board, inp):
"""Processes some text input. Returns -1 if quit; otherwise, returns 1 if the board does need to be refreshed (e.g.
help text was displayed or move was invalid) and 0 if the board needs to be refreshed."""
if inp in ["q", "quit"]:
return -1
if inp in ["h", "help"]:
print(help_text)
return 1
if inp in ["sh", "simple hint"]:
hints = SimpleHint(board).get_hints()
hints_available = False
for hint in hints:
print("Try typing", hint)
hints_available = True
if not hints_available:
print("No hints available")
return 1

def parse_card(card_str):
if card_str.endswith("c"):
index = int(card_str.rstrip("c"))
type = CardIndex.TYPE_FREECELL
elif card_str.endswith("f"):
index = int(card_str.rstrip("f"))
type = CardIndex.TYPE_FOUNDATION
else:
index = int(card_str)
type = CardIndex.TYPE_MAIN
return CardIndex(board, index - 1, type)
try:
parts = list(map(parse_card, inp.split(" ")))
board.move_card(parts[0], parts[1])
except ValueError as e:
print("That is not a valid move. Type h or help for help. (Error message: {})".format(e))
return 1
return 0

def main():
b = Board()
changed = True
while True:
if changed:
print(b)
changed = False
moves = input("Choose some cards to move: ").split(";")
for move in moves:
ret = run_input_on_board(b, move.strip())
if ret == 1:
break  # There was an error so interrupt the sequence of moves
elif ret == -1:
return
else:
changed = True

if __name__ == "__main__":
main()


I am mainly looking for ways to shorten the SimpleHint class. Commentary on the rest of the code would certainly be helpful as well, however. In particular, the CardIndex class may need to be refactored to make SimpleHint shorter.