# Windows console based card solitaire (Klondike)

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

"""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()
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'

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

"""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]
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:
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:
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:
while action not in VALID_ACTIONS:
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 = "".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()
$$$$
`