Theme: Programming Fundamentals
Topic: OO Design
Keywords: objects, classes, design
Presenter | James Powell james@dutc.io |
Date | Friday, December 18, 2020 |
Time | 11:00 PM PST |
print("Let's go!")
The game “Rock, Paper, Scissors” is played as follows:
Task: write a function to evaluate the rules of the game.
# NOTE: for naming & design purposes, you may assume the players are directional
# i.e., `a` is the Player
# `b` is the Challenger
# e.g., `rules` could return "player wins" or "player loses"
# or it could "player wins" vs "challenger wins"
# QUESTION: how do you represent ties?
def rules(a, b):
''' return who wins, given shapes played by two players a and b '''
pass
Task: write a framework that can evaluate a strategy and play the game for 10,000 rounds given a pairing of strategies.
from random import choice
def random_strategy():
''' randomly select a shape '''
return choice(['rps'])
# other sample strategies…
# QUESTION: how do we track "history" here?
def beat_previous_play():
''' select the shape that would beat the opponent's previous play '''
pass
def most_common_play(n=3):
''' select the most common shape from the opponent's previous N plays '''
pass
games = [(random_strategy(), random_strategy()) for _ in range(10_000)]
results = [rules(a, b) for a, b in games]
We have two datafiles in “Portable Game Notation” representing a series of chess games played in recent years.
We will use a third-party toolkit python-chess to load these and generate simpler files for our consumption.
NOTE: you do not need to install python-chess
for this exercise!
from chess import WHITE, BLACK, PAWN, ROOK, KNIGHT, BISHOP, QUEEN, KING
from chess.pgn import read_game
from itertools import product
from collections import namedtuple
from enum import Enum
from pathlib import Path
class Colors(Enum):
Black = BLACK
White = WHITE
class Pieces(Enum):
Pawn = PAWN
Rook = ROOK
Knight = KNIGHT
Bishop = BISHOP
Queen = QUEEN
King = KING
if __name__ == '__main__':
for input_filename in (Path(x) for x in {'carlsen.pgn', 'croatian.pgn'}):
output_filename = input_filename.with_suffix('.csv')
games = []
with open(input_filename) as f:
while (g := read_game(f)):
games.append(g)
class Row(namedtuple('RowBase', 'game move color piece positions')):
def to_csv(self):
return f'{self.game}, {self.move}, {self.color.name}, {self.piece.name}, {", ".join(f"{x}" for x in self.positions)}\n'
rows = []
for game_no, g in enumerate(games):
b = g.board()
for move_no, m in enumerate(g.mainline_moves()):
b.push(m)
for col, pc in product(Colors, Pieces):
if b.pieces(pc.value, col.value):
r = Row(game_no, move_no, col, pc, [*b.pieces(pc.value, col.value)])
rows.append(r)
with open(output_filename, 'w') as f:
for r in rows:
if r.positions:
f.write(r.to_csv())
These files contain information about these games in the following CSV format (with no header):
[0, 64)
where position 63 is the rightmost Black Rook and position 0 is the leftmost White Rook)Read in the above data using pandas
, numpy
, or xarray
(or some combination thereof) and determine the following things:
from pandas import read_csv
df = read_csv('carlsen.csv',
header=None,
names='game move side piece pos0 pos1 pos2 pos3 pos4 pos5 pos6 pos7'.split())
print(df.sample(3))
Here are the general rules for a real game of Blackjack:
The solution codes provided in module are intended to demonstrate the use of Python language features that we have covered at this point our journey together.
There may be other language or library features that we will cover at a later stage in this course that may be able to further refine these solutions.
Part (i):
# TASK: using `random.randrange`, write a programme that loops
# until the sum of the cards in your hand is ≥ 21
# - use a `while`-loop
# - use a single variable `hand_total` to represent the total
# value of your hand
# - use *simplified* rules (where Ace is strictly worth 11)
# - at the end, print out whether you won or went bust
from random import randrange
hand = 0
# HELPER: the `while` loop in Python loops until some predicate is no longer true
x = 10
while x > 0:
print(f'{x = }')
x -= 1
print('-' * 20)
x = 10
while True: # do-while
print(f'{x = }')
x -= 1
if x < 0:
break
# HELPER: `random.randrange` returns a number between [start, stop)—i.e., from
# some starting number `start` up to (but not including) some stopping
# number `stop`
# e.g., the below gives you numbers on the interval [1, 10)
from random import randrange
for _ in range(3):
print(f'{randrange(1, 10) = }') # NOTE: you will never see the number `10`
Part (ii):
# TASK: repeat the above simulation 100 times and compute the percentage of
# trial runs in which you go bust
Part (i):
from random import randrange
hand = 0
while hand <= 21:
card = randrange(1, 11 + 1)
hand += card
print(f'Got card worth: {card}')
if hand == 21:
print(f'Blackjack!')
elif hand > 21:
print(f'Bust! @ {hand}')
else:
print(f'Under 21! @ {hand}')
Part (ii):
from random import randrange
num_bust = 0
NUM_TRIALS = 150
for _ in range(NUM_TRIALS):
hand = 0
while hand < 21:
card = randrange(1, 11 + 1)
hand += card
if hand > 21:
num_bust += 1
print(f'You went bust {num_bust / NUM_TRIALS * 100}% of the time')
# TASK: model a deck of 52 cards; use `random.shuffle` to shuffle the deck,
# dealing cards to yourself until your card value is ≥ 21
# - use *simplified* rules (where Ace is strictly worth 11)
from random import shuffle
suits = ...
faces = ...
deck = [...]
shuffle(deck)
from random import shuffle
suits = 'C D H S'.split()
faces = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
values = [2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 11]
deck = []
for s in suits:
for f in faces:
card = s, f
deck.append(card)
shuffle(deck)
hand = []
while True:
card = deck.pop()
hand.append(card)
total_value = 0
for _, f in hand:
value = values[faces.index(f)]
total_value += value
if total_value >= 21:
break
print(f'{hand = }')
total_value = 0
for _, f in hand:
value = values[faces.index(f)]
total_value += value
if total_value == 21:
print(f'Blackjack! {hand = }')
elif total_value > 21:
print(f'Bust! @ {total_value} {hand = }')
else:
print(f'Under… @ {total_value} {hand = }')
# TASK: given your deck of cards, run your simplified game of Blackjack 100
# times, and compute the probability of going bust
from random import shuffle
suits = 'C D H S'.split()
faces = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
values = [2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 11]
TOTAL_GAMES = 100
games = []
for _ in range(TOTAL_GAMES):
deck = []
for s in suits:
for f in faces:
card = s, f
deck.append(card)
shuffle(deck)
hand = []
while True:
card = deck.pop()
hand.append(card)
total_value = 0
for _, f in hand:
value = values[faces.index(f)]
total_value += value
if total_value >= 21:
break
games.append(hand)
# print(f'{games = }')
results = []
for hand in games:
total_value = 0
for _, f in hand:
value = values[faces.index(f)]
total_value += value
if total_value > 21:
res = hand, 'bust'
elif total_value == 21:
res = hand, 'blackjack'
elif total_value < 21:
res = hand, 'miss'
results.append(res)
# print(f'{results = }')
num_bust = 0
for _, res in results:
if res == 'bust':
num_bust += 1
# print(f'{num_bust = }')
print(f'{num_bust / len(results) * 100 = :.2f}%')
# TASK: Repeat Problem III above, but allow Ace to be worth EITHER 1 or 11.
# TASK: Repeat Problem III above, but follow dealer's rules, allow the player
# to stop after ≥17.
# TASK: Repeat Problem III above, but implement multiple strategies:
# - strategy (i): stop at 21
# - strategy (ii): stop after ≥17
# - strategy (iii): stop if the probability of going bust is >50%
# (*without* keeping track of the cards that have been dealt)
# - strategy (iv): stop if the probability of going bust is >50%
# (*with* keeping track of the cards that have been dealt)
# Compute the probability of going bust for each strategy.
from collections import namedtuple
from enum import Enum
from dataclasses import dataclass
from itertools import product, zip_longest
from random import shuffle
from enum import Enum, auto
from collections import namedtuple, Counter, defaultdict
class Values(Enum):
Two = 2
Three = auto()
Four = auto()
Five = auto()
Six = auto()
Seven = auto()
Eight = auto()
Nine = auto()
Ten = auto()
Jack = 10
Queen = 10
King = 10
Ace = 11
def __add__(self, other):
if isinstance(other, Card):
other = other.value
return self.value + other
def __str__(self):
return dict(zip(Values, '2 3 4 5 6 7 8 9 10 J Q K A'.split()))[self]
class Suits(Enum):
Diamonds = 0
Clubs = auto()
Hearts = auto()
Spades = auto()
def __str__(self):
return '\N{black diamond suit} \N{black club suit} \N{black heart suit} \N{black spade suit}'.split()[self.value]
class Card(namedtuple('CardBase', 'value suit')):
def __add__(self, other):
if isinstance(other, Card):
other = other.value
return self.value + other
__radd__ = __add__
def __str__(self):
return f'{self.value}{self.suit}'
class Hand(namedtuple('Hand', 'cards')):
@classmethod
def from_cards(cls, *cards):
return cls([*cards])
def __str__(self):
return ' '.join(f'{c}' for c in self.cards)
@property
def total(self):
return sum(self.cards)
Strategy = namedtuple('Strategy', 'func name author')
STRATEGIES = {}
def strategy(name, author):
def dec(func):
STRATEGIES[func] = Strategy(func, name, author)
return func
return dec
@strategy(
name = 'Hit Until 21',
author = 'James',
)
def hit_until_21(hand, deck):
while True:
if hand.total >= 21:
break
hand.cards.append(deck.pop())
yield
@strategy(
name = 'Hit Until 17',
author = 'Jiyuan',
)
def hit_until_17(hand, deck):
while True:
if hand.total >= 17:
break
hand.cards.append(deck.pop())
yield
@strategy(
name = 'Hit Until Prob of Bust > 50%',
author = 'Mahmut',
)
def hit_until_prob(hand, deck):
while True:
prob = sum(Hand.from_cards(*hand.cards, c).total > 21 for c in deck) / len(deck)
if prob > .50:
break
hand.cards.append(deck.pop())
yield
if __name__ == '__main__':
wins = defaultdict(Counter)
for m_strat, jy_strat in product(STRATEGIES.values(), STRATEGIES.values()):
for _ in range(1_000):
deck = [Card(v, s) for v, s in product(Values, Suits)]
shuffle(deck)
m_hand = Hand.from_cards(deck.pop(), deck.pop())
jy_hand = Hand.from_cards(deck.pop(), deck.pop())
for _ in zip_longest(m_strat.func(m_hand, deck), jy_strat.func(jy_hand, deck)):
if m_hand.total == 21 or jy_hand.total > 21:
wins[m_strat, jy_strat]['ms'] += 1
break
elif m_hand.total > 21 or jy_hand.total == 21:
wins[m_strat, jy_strat]['jy'] += 1
break
else:
if m_hand.total > jy_hand.total:
wins[m_strat, jy_strat]['ms'] += 1
else:
wins[m_strat, jy_strat]['jy'] += 1
for (a, b), cnt in wins.items():
print(f'ms: {a.name}\njy: {b.name}')
for play, count in cnt.most_common():
print(f' {play}: {count}')
print()
print(f'{m_hand = !s}')
print(f'{m_hand.total = !s}')
hit_until_21(m_hand, deck)
print(f'{m_hand = !s}')
print(f'{m_hand.total = !s}')
print('-' * 10)
print(f'{jy_hand = !s}')
print(f'{jy_hand.total = !s}')
hit_until_17(jy_hand, deck)
print(f'{jy_hand = !s}')
print(f'{jy_hand.total = !s}')