# One-handed Solitaire Game Choice Generator

I want to test some different endgame scenarios for one-handed Solitaire where the removal of the cards if the suit or value match is optional.

I've written a simple generator which yields at the decision point on every opportunity where it could clear cards and accepts a boolean on whether it will clear those cards.

Any feedback on usability of the generators is appreciated.

I want to be able to attempt different algorithms for end states (<10 cards) and get some data on how optional choices can improve win odds, while also being able to look at the discards, cards on stack and unseen cards to be able to determine if the game is even winnable or if it's optimal to simply remove as many cards as possible.

Given how I need access to the deck, stack and discard should I rearrange these in any way? As is the winnable boolean would have to be a property of OneHandedSolitaire as it is the only area that knows of all 3 lists of value.

"""One handed solitaire with a rule change wherein removing cards is optional.

This choice in the majority of cases won't be an optimal play except for in end game scenarios where an actual victory
is still possible and based on the suits or values available the remaining number of cards can influence decisions.
"""
import copy
import itertools
import operator
import random
from collections import namedtuple, Counter

SUITS = ['♠', '♥', '♦', '♣']
VALUES = list(map(str, range(2, 11))) + ['J', 'Q', 'K', 'A']
Card = namedtuple('Card', ['suit', 'value'])
UNSHUFFLED_DECK = list((Card(*c_v) for c_v in itertools.product(SUITS, VALUES)))

class Deck:
def __init__(self):
self.deck = copy.copy(UNSHUFFLED_DECK)

def shuffle(self):
random.shuffle(self.deck)

def draw(self):
"""Return a card; raises exception on an empty deck"""
try:
return self.deck.pop()
except IndexError:
raise RuntimeError("Deck is empty")

class FourStack:
"""Stack for comparison of cards between the top and fourth from top cards; also saves discarded cards from stack"""
def __init__(self):
self.cards = []

self.cards.append(card)

@property
def suit(self) -> bool:
"""Compare the suit of the stack"""
try:
if self.cards[-1].suit == self.cards[-4].suit:
return True
# If there are <4 cards in the stack cannot match
except IndexError:
pass
return False

def remove_middle(self):
for _ in range(2):

@property
def value(self) -> bool:
"""Compare the value of the stack"""
try:
if self.cards[-1].value == self.cards[-4].value:
return True
# If there are <4 cards in the stack cannot match
except IndexError:
pass
return False

def remove_all(self):
for _ in range(4):

class OneHandedSolitaire:
def __init__(self):
self.new_game()

def new_game(self):
self._setup()
self._start()

def _setup(self):
self.deck = Deck()
self.deck.shuffle()
self.stack = FourStack()

def _start(self):
"""Start the game"""
for _ in range(4):
self.flip()

def flip(self):
"""Flip a card over and put it onto the stack"""

def compare(self):
"""Compare the top card and the 4th from the top and provide options

These are the functional rules of one handed solitaire. Matching values remove all four cards on top of the
stack, matching suits remove the middle two cards from the top four.

returns a method which will clear cards or None
"""
if self.stack.value:
return self.stack.remove_all
if self.stack.suit:
return self.stack.remove_middle

def choice_gen(self):
"""Yield the next decision point of the game"""
while True:
choice = self.compare()
if choice:
yield choice
try:
self.flip()
except RuntimeError:
return

def play_gen(self):
"""Play a game of one handed solitaire.

With this generator send True to clear and False to not clear the cards. You can inspect either the yielded
method to see how many and which cards will be removed.
"""
for choice in self.choice_gen():
clear = yield
if clear:
choice()
score = len(self.stack.cards)
return score

def play_clear(self):
"""Play a game where the user always removes every card they can and return the score."""
gen = self.play_gen()
gen.send(None)
try:
while True:
gen.send(True)
except StopIteration as e:
score = e.value
return score

def winnable(self) -> bool:
"""Determining absolutely if the game is winnable is logically and computationally difficult.

This being True means we can't eliminate victory, but doesn't guarantee a victory is possible.
"""
# Empty deck remaining cards
# Edge case: technically there could be a valid series of ignored choices that could clear the stack
if not self.deck.deck and self.stack.cards:
return False

remaining_cards = self.deck.deck + self.stack.cards

if len(remaining_cards) == 2:
return False

# no matches remaining
matches = itertools.groupby(remaining_cards, key=operator.itemgetter('value'))
for _, cards in matches:
if len(cards) <= 1:
break
# nobreak
else:
return False

# matches remain, but they are all within 3 cards of each other in ordering

return True

def play_clear_until_win():
ohs = OneHandedSolitaire()
games = 1
score = True
while score:
score = ohs.play_clear()
print(f"Score was {score} after {games} games")
ohs.new_game()
games += 1

def play(n):
c = Counter()
ohs = OneHandedSolitaire()
for _ in range(n):
score = ohs.play_clear()
c.update([score])
ohs.new_game()
return c

if __name__ == "__main__":
runs = play(300000)
print(f"Counter data: {runs}")
print(f"Most common score: '{runs.most_common()}'")
win_prct = runs[0] / (sum(runs.values()))
print(f"Winning odds: {win_prct}")


• VALUES seems wrong to me — it has two aces but no tens. – Gareth Rees Sep 17 '18 at 8:19