seminars.fb

Grab Bag → “Stocking Stuffers”

Seminar (Fri, Dec 11, 2020; 12 PM PST)

Theme: Grab Bag

Topic: Stocking Stuffers

Keywords: simulation design

Presenter James Powell james@dutc.io
Date Friday, December 11, 2020
Time 12:00 PM PST
print("Let's go!")

Game: Fizzbuzz

Rules

The game ”fizzbuzz” is played as follows:

A sample game may look like this:

Task: Write a function that gives the first N values from such a game.

divisors = {
    3: 'fizz',
    5: 'buzz',
}
def fizzbuzz(n, divisors=divisors):
    rv = []
    for x in range(1, n+1):
        terms = []
        for div, term in divisors.items():
            if x % div == 0:
                terms.append(term)
        rv.append((x, terms))
    return rv

for num, word in fizzbuzz(10, divisors=divisors):
    print(f'{"".join(word).upper() = }')
    print(f'{num                   = :.2f}')

Task: Write a test for the above.

Task: Extend the above to generalise the divisors. e.g., allow the game to be played with ‘fizz’, ‘buzz’, and ‘quux’ for 3, 5, and 7.

Task: Write search below to search within the fizzbuzz sequence for particular patterns.

# NOTE: feel free to redesign the grammar for your pattern.

divisors = {
    3: 'fizz',
    5: 'buzz',
}
def fizzbuzz(n, divisors=divisors):
    rv = []
    for x in range(1, n+1):
        terms = []
        for div, term in divisors.items():
            if x % div == 0 or str(div) in str(x):
                terms.append(term)
        rv.append((x, terms))
    return rv

for num, terms in fizzbuzz(30):
    print(f'{num = :<5} {"".join(terms) = }')

def search(pattern, *, max_value=100_000):
    ''' search for a pattern within the fizzbuzz sequence up to max_value
    
    Use the following (sample) grammar:
    - word: match this exact word (e.g., fizz, buzz, fizzbuzz)
    - word₁|word₂: match word₁ OR word₂
    - *: match any word
    - #: match only a number
    '''
    pass

# look for four sequential plays that are
#   fizz, then fizz OR buzz, then anything, then buzz
pattern = ['fizz', 'fizz|buzz', '*', 'fizzbuzz']
prev_val1 = None
prev_val2 = None
for curr_val in range(10):
    if prev_val1 is None:
        prev_val1, prev_val2 = prev_val2, curr_val
        continue
    if prev_val2 is None:
        prev_val1, prev_val2 = prev_val2, curr_val
        continue
    print(f'{prev_val1, prev_val2, curr_val = }')
    prev_val1, prev_val2 = prev_val2, curr_val
def pairs_of_three(xs):
    return zip(xs, xs[1:], xs[2:])

for prev_val1, prev_val2, curr_val in pairs_of_three(range(10)):
    print(f'{prev_val1, prev_val2, curr_val = }')
for x, y in zip('lion tiger bear'.split(), 'yellow orange brown'.split()):
    print(f'{x, y = }')

xs = [0, 1, 2, 3, 4]
for x, y, z in zip(xs, xs[1:], xs[2:]):
    print(f'{x, y, z = }')
from itertools import count
divisors = {
    3: 'fizz',
    5: 'buzz',
}
def fizzbuzz(divisors=divisors):
    for num in count(1):
        terms = []
        for div, term in divisors.items():
            if num % div == 0 or str(div) in str(num):
                terms.append(term)
        yield num, terms

from itertools import islice, tee
nwise = lambda g, n=2: zip(*(islice(g, i, None) for i, g in enumerate(tee(g, n))))

def search(pattern, *, max_value=100_000):
    ''' search for a pattern within the fizzbuzz sequence up to max_value
    
    Use the following (sample) grammar:
    - word: match this exact word (e.g., fizz, buzz, fizzbuzz)
    - word₁|word₂: match word₁ OR word₂
    - *: match any word
    - #: match only a number
    '''
    for win in nwise(islice(fizzbuzz(), max_value), len(pattern)):
        if all(p == ''.join(t) for p, (_, t) in zip(pattern, win)):
            return win

# look for four sequential plays that are
#   fizz, then fizz OR buzz, then anything, then buzz
pattern = ['buzz', 'buzz', 'fizzbuzz', 'buzz']
print(f'{search(pattern) = }')

Game: Rock, Paper, Scissors

Rules

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]

Game: Poker

# High-Card
# Two-of-a-Kind
# Three-of-a-Kind
# Full-House
# Straight        
# Flush           . . . . .  all same suit
# Straight-Flush  10 9 8 7 6 
# Royal-Flush     A K Q J 10
from enum import Enum, auto
from functools import total_ordering
from collections import namedtuple, Counter
from itertools import product, islice, tee, combinations
from random import shuffle

@total_ordering
class Suits(Enum):
    Clubs    = 0
    Diamonds = auto()
    Hearts   = auto()
    Spades   = auto()
    def __lt__(self, other):
        return self.value < other.value
    def __eq__(self, other):
        return self.value == other.value
    def __hash__(self):
        return hash(self.value)

    def __str__(self):
        return ['\N{black club suit}',
                '\N{black diamond suit}',
                '\N{black spade suit}',
                '\N{black heart suit}',][self.value]

@total_ordering
class Faces(Enum):
    Two   = 0
    Three = auto()
    Four  = auto()
    Five  = auto()
    Six   = auto()
    Seven = auto()
    Eight = auto()
    Nine  = auto()
    Ten   = auto()
    Jack  = auto()
    Queen = auto()
    King  = auto()
    Ace   = auto()
    def __lt__(self, other):
        return self.value < other.value
    def __eq__(self, other):
        return self.value == other.value
    def __hash__(self):
        return hash(self.value)
    def __str__(self):
        return '2 3 4 5 6 7 8 9 10 J Q K A'.split()[self.value]

nwise = lambda g, n=2: zip(*(islice(g, i, None) for i, g in enumerate(tee(g, n))))

class HandType(namedtuple('HandType', 'predicate')):
    __call__ = lambda s, *a, **kw: s.predicate(*a, **kw)
@total_ordering
class Hands(Enum):
    RoyalFlush = HandType(
        lambda cs: Hands.StraightFlush(cs) and max(cs, key=lambda c: c.face).face == Faces.Ace
    )
    StraightFlush = HandType(
        lambda cs: Hands.Straight(cs) and Hands.Flush(cs)
    )
    Flush = HandType(
        lambda cs: Counter(c.suit for c in cs).most_common(1)[0][-1] == 5
    )
    FourOfAKind = HandType(
        lambda cs: Counter(c.face for c in cs).most_common(1)[0][-1] == 4
    )
    Straight = HandType(
        lambda cs: all(y.face.value - x.face.value == 1
                       for x, y in nwise(sorted(cs, key=lambda c: c.face)))
    )
    FullHouse = HandType(
        lambda cs: [c for _, c in Counter(c.face for c in cs).most_common(2)] == [3, 2]
    )
    TwoPair = HandType(
        lambda cs: [c for _, c in Counter(c.face for c in cs).most_common(2)] == [2, 2]
    )
    ThreeOfAKind = HandType(
        lambda cs: Counter(c.face for c in cs).most_common(1)[0][-1] == 3
    )
    TwoOfAKind = HandType(
        lambda cs: Counter(c.face for c in cs).most_common(1)[0][-1] == 2
    )
    HighCard = HandType(
        lambda cs: True
    )

    def __call__(self, *args, **kwargs):
        return self.value(*args, **kwargs)
    def __lt__(self, other):
        return self.value < other.value
    def __eq__(self, other):
        return self.value == other.value
    def __hash__(self):
        return hash(self.value)

class Card(namedtuple('Card', 'face suit')):
    def __str__(self):
        return f'{self.face!s}{self.suit!s}'

class Hand(namedtuple('HandBase', 'cards best_hand')):
    @classmethod
    def from_cards(cls, *cards):
        best_hand = [hand_type for hand_type in Hands if hand_type(cards)]
        return cls(cards, best_hand)

deck = [Card(f, s) for f, s in product(Faces, Suits)]
shuffle(deck)

flop = [deck.pop(), deck.pop(), deck.pop()]
megan_hand  = [deck.pop(), deck.pop()]
lauren_hand = [deck.pop(), deck.pop()]
turn = [deck.pop()]
river = [deck.pop()]

h = [*flop, *lauren_hand, *turn, *river]
print(f'{Hand.from_cards(*flop, *lauren_hand, *turn, *river).best_hand = }')

#  from contextlib import contextmanager
#  from time import perf_counter
#  @contextmanager
#  def timed():
#      try:
#          start = perf_counter()
#          yield
#      finally:
#          stop = perf_counter()
#          print(f'Elapsed (\N{greek capital letter delta}t): {stop - start:.2f}s')

#  with timed():
#      all_possible_hands = Counter(Hand.from_cards(*cs).best_hand
#                                   for cs in islice(combinations(deck, 5), 10_000_000))
#  all_possible_hands = Counter()
#  for idx, cs in islice(combinations(deck, 5)):
#      all_possible_hands[Hand.from_cards(*cs).best_hand] += 1
#  print(f'{all_possible_hands = }')

Game: Queen’s Gambit

Data

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

Actual Data

These files contain information about these games in the following CSV format (with no header):

Play

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))