0
\$\begingroup\$

I have written a solitaire card game for the windows console.

It's the Klondike version with 4 cards in each of the 7 piles (see https://en.wikipedia.org/wiki/Klondike_(solitaire)).

The piles must be filled with cards of the same suit in descending order (K, Q, J, ..., 2). The goal is to move all cards to the 4 foundations (spades, hearts, diamonds, clubs) in ascending order (A, 2, 3, ..., K).

From the stock cards can be drawn one by one to the waste. Cards can be moved

  • from the waste to any of the piles
  • from the waste the corresponding foundation
  • from a pile to another pile
  • an ace on top of the waste will be moved to the foundation automatically

Please review my code, each and any comment/advice is much appreciated.

"""Klondike console game"""
import json
import random
import time

def shuffle() -> tuple[list[int], list[int]]:
    """Return a shuffled deck and a copy thereof."""
    deck = random.sample(range(DECK_SIZE),DECK_SIZE)
    # sav = deck[:]
    return deck, deck[:]

def setup_board(deck: list[int]) -> list[int]:
    """Prepare board.

    b[0] = hidden cards (stock)
    b[1] = card to play (waste)
    b[2]  [0]-[3] = heaps_done: done cards (foundation)
    b[3]  [0]-[6] = heaps_todo: cards to process (pile)
    """
    tmp = [deck[:], [], [[],[],[],[],], [[],[],[],[],[],[],[],]]
    #           |   |    |               |
    #           |   |    |               + todo (pile)
    #           |   |    + done (foundation)
    #           |   + open (waste)
    #           + hidden (stock)
    _ = draw_next(tmp)

    # move 4 cards to each of the 7 piles,
    # aces "on top" will be moved to their corresponding foundation automatically
    for heap in range(len(tmp[TODO])):
        for _ in range(SHOW_TODO):
            tmp[TODO][heap].append(tmp[HIDDEN].pop())
        while is_ace(tmp[TODO][heap][TOP]):
            suit, _ = divmod(tmp[TODO][heap][TOP], CPS)
            tmp[DONE][suit].append(tmp[TODO][heap].pop())
    board = tmp
    return board

def show_board(board: list) -> None:
    """Display board on console."""
    cards_count = 0
    cards_count += len(board[HIDDEN])
    cards_count += len(board[OPEN])
    for heap in board[DONE]:
        cards_count += len(heap)
    for heap in board[TODO]:
        cards_count += len(heap)
    if not cards_count == DECK_SIZE:
        print(f'*** ERROR *** cards_count: {cards_count} DECK_SIZE: {DECK_SIZE}')
        dump_deck(board)
    print()
    print(' ' * SHIFT, end='')
    header = '.'.join(f'.{i+1}' for i in range(len(board[TODO])))
    print(header, end='')
    print()
    show_heaps_todo(board)
    print()
    print(' ' * SHIFT, end='')
    show_heaps_hnd(board)

def show_heaps_hnd(board: list) -> None:
    """Display heaps HIDDEN, NEXT, DONE (stock, waste, foundation)."""
    show_heap_hidden(board)
    show_heap_next(board)
    pprint('    ')
    show_heaps_done(board)

def check_win(board: list) -> bool:
    """Check if game is won (all cards in DONE heaps)."""
    cnt = 0
    for heap in board[DONE]:
        cnt += len(heap)
    return cnt == DECK_SIZE

def pprint(card: str|int) -> None:  # param is a string or index of card
    """Print card suit/value, no newline."""
    if isinstance(card, str):  # if type(card) == str:
        print(card, end='')
    else:
        suit, rank = divmod(card, CPS)
        print(CARDS[suit * CPS + rank], end='')

def show_heap_hidden(board: list) -> None:
    """Show hidden heap (stock) status."""
    # print(f'board <{board}>')
    card = '-- ' if not board[HIDDEN] else '?? '
    pprint(card)

def show_heap_next(board: list) -> None:
    """Show next new card (waste)."""
    # print(f'board <{board}>')
    if not board[OPEN]:
        draw_next(board)
    card = '--' if not board[OPEN] else board[OPEN][TOP]
    pprint(card)

def show_heaps_done(board: list) -> None:
    """Show heaps DONE 1 to 4 (foundations)."""
    heaps = board[DONE]
    for heap in heaps:
        card = '**' if not heap else heap[TOP]
        pprint(card)
        print(' ', end='')
    print()

def show_heaps_todo(board: list) -> None:
    """Show heaps TODO 1 to 7 (piles)."""
    max_heap_len = 0
    for heap in board[TODO]:
        max_heap_len = max(max_heap_len, len(heap))
    for i in range(max_heap_len):
        print(' ' * SHIFT, end='')
        for j in range(len(board[TODO])):
            card = '  '
            heap = board[TODO][j]
            if heap:
                if i < len(heap):
                    card = heap[i]
            else:
                card = '--' if i == 0 else '  '
            pprint(card)
            print(' ', end='')
        print()

def ask_for_action(previous: str, buffer: str) -> tuple[str, str]:
    """Retrieve next command either from input or from input buffer.
    
    If input buffer is empty, read prompted input() into buffer.
    Return 1st character from buffer and the remaining buffer.
    """
    if not buffer:
        print(' ' * SHIFT, end='')
        prompt = ''.join(['Action (', previous, '): ', buffer])
        while True:
            try:
                buffer = input(prompt)
            except (KeyboardInterrupt, EOFError):
                continue
            break
    inp = buffer[:1].strip().lower()
    return inp, buffer[1:]

def confirm(action: str, prompt: str) -> bool:
    """Return True if input in 'YyJj'."""
    _ = action
    inp = ''
    while not inp or inp not in 'YyjJNn':
        try:
            inp = input(prompt).strip()
        except (KeyboardInterrupt, EOFError):
            continue
    return inp != '' and inp[:1] in 'YyJj'

def ask_for_input(prompt: str) -> str:
    """Ask for prompted input."""
    while True:
        try:
            inp = input(prompt).strip()
        except (KeyboardInterrupt, EOFError):
            continue
        break
    return inp

def undo_last(board: list) -> None:
    """Undo last move."""
    if hist:
        # print(hist)
        tup = hist.pop()
        # print(tup)
        src, dst = tup[0], tup[1]
        # print(src, dst)
        if src[0] in (HIDDEN, OPEN):
            if dst[0] == OPEN:
                board[src[0]].append(board[dst[0]].pop())
            else:
                board[src[0]].append(board[dst[0]][dst[1]].pop())
        else:  # src == TODO, check number of cards to be moved
            if len(src) == 2:
                board[src[0]][src[1]].append(board[dst[0]][dst[1]].pop())
            else:  # more than 1 card
                number_of_cards = src[2]
                board[src[0]][src[1]].extend(board[dst[0]][dst[1]][-number_of_cards:])
                del board[dst[0]][dst[1]][-number_of_cards:]

def draw_next(board: list) -> bool:
    """Draw next card from HIDDEN stack, move it to OPEN stack.
    
    Return False if HIDDEN is empty (no screen refresh required),
    move card to DONE if it is an ace & repeat.
    """
    board_has_changed = False
    while board[HIDDEN]:
        card = board[HIDDEN].pop()
        board_has_changed = True
        if is_ace(card):  # move to [DONE][suit], continue drawing
            dst, _ = divmod(card, CPS)  # dst = suit
            board[DONE][dst].append(card)
            hist.append(((HIDDEN, ), (DONE, dst)))
        else:  # move to OPEN, stop drawing cards
            board[OPEN].append(card)
            hist.append(((HIDDEN, ), (OPEN, )))
            break
    return board_has_changed

def is_ace(card: int) -> bool:
    """Return True if card is an Ace."""
    return divmod(card, CPS)[1] == ACE

def move_to_done(board: list, action: str) -> bool:
    """Move a card to one of the DONE heaps (action = dest. heap).

    Returns True if any change occured to trigger screen update.
    """
    dst = 'qwer'.index(action)
    value_to_find = ACE  # default=search for ace (remainder=0)
    if board[DONE][dst]:
        if board[DONE][dst][TOP] % CPS == KING:  # king? heap is done already
            return False
        value_to_find = board[DONE][dst][TOP] % CPS + 1  # next card
    card_to_find = dst * CPS + value_to_find
    src = OPEN
    if board[src]:
        candidate = board[src][TOP]
        # print(f'candidate {candidate} to_find {card_to_find} dst (2 dst) {board[2][dst]}')
        if candidate == card_to_find:
            board[DONE][dst].append(board[src].pop())
            hist.append(((src, ), (DONE, dst)))
            return True
    for i, heap in enumerate(board[TODO]):
        if heap:
            # print(f'cand to_find i heap {heap[TOP]} {card_to_find} {board[3][i][TOP]} {i} {heap}')
            if heap[TOP] == card_to_find:
                board[DONE][dst].append(board[TODO][i].pop())
                hist.append(((TODO, i), (DONE, dst)))
                return True
    return False

def ask_which_king(kings: str) -> list[int]:
    """Ask which specific king to move."""
    prompt = f'which king ({kings})? '
    inp = kings if len(kings) == 1 else ''
    while not inp or inp not in kings:  # 'shdc':
        try:
            inp = input(prompt).strip().lower()
        except (KeyboardInterrupt, EOFError):
            continue
    val = 'shdc'.index(inp)
    return [val * CPS + KING]

def search_open_kings(board: list, dst: int) -> str:
    """Determine kings available to be moved to empty TODO heap."""
    res = ''
    for i_king, king in enumerate(KINGS):
        for i, heap in enumerate(board[TODO]):
            if not heap or i == dst:
                continue  # skip search in empty heap; dont move heap to itself

            if king in heap or (board[OPEN] and board[OPEN][TOP] == king):
                found = 'shdc'[i_king]
                if not found in res:  # ???
                    res += found
    return res

def move_to_todo(board: list, action: str) -> bool:
    """Move one ore more cards to the TODO heap specified.

    Return True if at least one card was moved and display must be refreshed.
    """
    dst = '1234567'.index(action)
    if board[TODO][dst]:
        if board[TODO][dst][TOP] % CPS in (ACE, DEUCE):  # deuce or ace? cannot put a card there
            return False
    if not board[TODO][dst]:
        # To-Do suggest available kings only
        kings_open = search_open_kings(board, dst)
        if kings_open:
            cards_to_find = ask_which_king(kings_open)
        else:
            cards_to_find = []
    else:
        cards_to_find = [board[TODO][dst][TOP] - 1]

    src = OPEN
    if board[src] and board[src][TOP] in cards_to_find:
        board[TODO][dst].append(board[src].pop())
        hist.append(((src, ), (TODO, dst)))
        return True

    for card_to_find in cards_to_find:
        for i, heap in enumerate(board[TODO]):
            if not heap or i == dst:
                continue  # skip search in empty heap; dont move heap to itself
            if card_to_find in heap:
                start_idx = heap.index(card_to_find)
                number_of_cards = len(heap[start_idx:])
                board[TODO][dst].extend(heap[start_idx:])
                del heap[start_idx:]
                hist.append(((TODO, i, number_of_cards), (TODO, dst)))
                return True
            else:
                # print('NOT found')
                pass
    return False

def dump_deck(board: list) -> None:
    """Dump raw game data to screen."""
    print(hist)
    print(key_log)
    for heap in board:
        print(heap)

def save_game(saved_deck: list, deck: list, solved_flag=False, comment='') -> None:
    """Save game to file."""
    with open(FILE_KLONDIKE, 'a') as file:
        solved = ' *' if solved_flag else ''
        file.write(f'{time.strftime("%Y%m%d-%H%M%S", time.localtime())}{solved}{comment}\n')
        file.write(f'{json.dumps(saved_deck)}\n')
        file.write(f'{json.dumps(deck)}\n')
        file.write(f'{json.dumps(hist)}\n')
        file.write(f'{json.dumps(key_log)}\n')
        file.write('=====\n')

def game_stats(board: list) -> None:
    """Show game status / statistics."""
    print(f'len heap_hidden : {len(board[HIDDEN])}')
    print(f'len heap_open   : {len(board[OPEN])}')
    print(f'len hist        : {len(hist)}')

FILE_KLONDIKE = 'klondike.log'
VALID_ACTIONS = list('0#,+1234567qweruanxdsg?h')

SUIT_SYMBOLS = "♠♥♦♣"
SUITS = len(SUIT_SYMBOLS)
LOWEST = ACE = 0
DEUCE = 1
HIGHEST = KING = 12
CARDS_PER_SUIT = HIGHEST - LOWEST + 1
CPS = CARDS_PER_SUIT  # short form for CARDS_PER_SUIT
DECK_SIZE = CPS * SUITS
KINGS = [(s + 1) * CPS - 1 for s in range(SUITS)]  # [12, 25, 38, 51]

CARDS = ["♠A","♠2","♠3","♠4","♠5","♠6","♠7","♠8","♠9","♠T","♠J","♠Q","♠K",
         "♥A","♥2","♥3","♥4","♥5","♥6","♥7","♥8","♥9","♥T","♥J","♥Q","♥K",
         "♦A","♦2","♦3","♦4","♦5","♦6","♦7","♦8","♦9","♦T","♦J","♦Q","♦K",
         "♣A","♣2","♣3","♣4","♣5","♣6","♣7","♣8","♣9","♣T","♣J","♣Q","♣K",]
assert DECK_SIZE == len(CARDS)

HIDDEN = 0  # stock
OPEN = 1  # waste
DONE = 2  # foundation
TODO = 3  # pile

TOP = -1  # "top" of heap, ~ heap[-1]
SHOW_TODO = 4  # start with 4 cards in each todo heap (pile)

SHIFT = 30  # shift output 30 chars to the right for nicer display

GAME_HELP = """possible actions:
0, #, +, ,,  draw next card from stock
1 - 7        move card(s) to pile       1 - 7 (K, Q, J, ..., 2)
q,w,e,r      move card    to foundation 1 - 4 (A, 2, 3, ..., K) (♠♥♦♣)
u            (u)ndo last move
a            start (a)gain current game
n            (n)ew game
x            e(x)it
d            (d)ump raw data to screen
s            (s)ave current game to """ f'{FILE_KLONDIKE}' """
g            (g)ame status / statistics
?, h         (h)elp"""

hist = []
key_log = []

def main():
    """Game loop."""

    while True:
        playing = True
        deck, saved_board = shuffle()
        deck = setup_board(deck)

        inp_buffer = ''
        action = ''
        previous_action = ''
        hist.clear()
        key_log.clear()
        redraw_board = True  # force displaying board
        save_game(saved_board, deck)

        while playing:
            if redraw_board:
                show_board(deck)
                # redraw_board = False
            else:
                show_heaps_hnd(deck)
            if check_win(deck):
                print()
                print('*** You win! ***')
                save_game(saved_board, deck, True)
                break

            action = ''
            if inp_buffer:
                action, inp_buffer = ask_for_action(previous_action, inp_buffer)
            while action not in VALID_ACTIONS:
                action, inp_buffer = ask_for_action(previous_action, inp_buffer)
                if not action:
                    action = previous_action
            previous_action = action
            key_log.append(action)

            if action and action in '?h':
                print(GAME_HELP)
                redraw_board = True

            if action and action in '0#,+':
                redraw_board = draw_next(deck)

            if action and action in 'qwer':
                redraw_board = move_to_done(deck, action)

            if action and action in '1234567':
                redraw_board = move_to_todo(deck, action)

            if action and action in 'u':  # undo move
                undo_last(deck)
                redraw_board = True

            if action and action in 's':  # save to file
                comment = ask_for_input('Comment: ')
                comment = "".join([' #', comment]) if comment else ''
                save_game(saved_board, deck, False, comment)
                previous_action = ''

            if action and action in 'd':
                dump_deck(deck)
                redraw_board = True

            if action and action in 'g':  # game status / statistics
                game_stats(deck)
                redraw_board = True

            # restart, new game, exit
            if action and action in 'a':  # again
                if not confirm(action, "Restart this game? y/n "):
                    previous_action = ''
                    continue
                save_game(saved_board, deck)
                deck = setup_board(saved_board)
                hist.clear()
                key_log.clear()
                inp_buffer = ''
                previous_action = ''
                redraw_board = True

            if action and action in 'n':  # new game
                if confirm(action, "Start new game? y/n "):
                    break
                previous_action = ''

            if action and action in 'x':  # exit
                if confirm(action, "Exit game? y/n "):
                    return
                previous_action = ''

if __name__ == '__main__':
    print(GAME_HELP)
    main()
```
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Browse other questions tagged or ask your own question.