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)                    if card_ind2.can_add_card(card1):                        # 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        for free_ind in range(NUM_FREECELLS):            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)                if card_ind2.can_add_card(card1):                    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()            for free_ind in range(NUM_FREECELLS):                card_ind2 = CardIndex(self.board, free_ind, CardIndex.TYPE_FREECELL)                if card_ind1.can_add_card(card_ind2.peek_card()):                    yield MovePoss([(card_ind2, card_ind1)])            for foundation_ind in range(NUM_FOUNDATIONS):                card_ind2 = CardIndex(self.board, foundation_ind, CardIndex.TYPE_FOUNDATION)                if card_ind2.can_add_card(card1):                    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 hintThere 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 randomCARD_TEXT_LEN = 10NUM_FREECELLS = 4NUM_FOUNDATIONS = 4help_text = (This is freecell, a card game. The objective is to fill up the foundations. This is accomplished by stacking the mainboard by alternating color (white/black) and in descending order (King through Ace). You can also use the free cells tohold 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 7This 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 4cwould 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 cardsfrom column 1 to column two, you could do    >>> Choose some cards to move: 1 1c; 1 2c; 1 2; 2c 2; 1c 2You 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 + spacesclass 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)    def can_add_card(self, card):        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)    def add_card(self, card):        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) + suffixclass 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):        if not dest.can_add_card(origin.peek_card()):            raise ValueError(Cannot move cards)        dest.add_card(origin.pop_card())    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_boardclass MovePoss:    Move possibility    def __init__(self, indices):        self.indices = indices    def perform_on_board(self):        for index in self.indices:            index[1].add_card(index[0])    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)                    if card_ind2.can_add_card(card1):                        # 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        for free_ind in range(NUM_FREECELLS):            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)                if card_ind2.can_add_card(card1):                    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()            for free_ind in range(NUM_FREECELLS):                card_ind2 = CardIndex(self.board, free_ind, CardIndex.TYPE_FREECELL)                if card_ind1.can_add_card(card_ind2.peek_card()):                    yield MovePoss([(card_ind2, card_ind1)])            for foundation_ind in range(NUM_FOUNDATIONS):                card_ind2 = CardIndex(self.board, foundation_ind, CardIndex.TYPE_FOUNDATION)                if card_ind2.can_add_card(card1):                    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 hintdef 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 0def 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 = Trueif __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.
Hint generation code for freecell game
python;python 3.x;playing cards