Last active
January 2, 2024 22:11
-
-
Save dutc/88d7f2d30527be9842b2c06b3c14cf77 to your computer and use it in GitHub Desktop.
Spot the bug in this Python code! (Fri Feb 24 @ https://www.eventbrite.com/e/548056811677/)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from random import Random | |
from itertools import combinations, chain | |
from collections import defaultdict, Counter | |
from statistics import mean | |
letters = {1: 'aeilnorstu', 2: 'dg', 3: 'bcmp', 4: 'fhvwy', 5: 'k', 8: 'jx', 10: 'qz'} | |
tiles = {v: k for k, vs in letters.items() for v in vs} | |
if __name__ == '__main__': | |
rnd = Random(0) | |
with open('/usr/share/dict/words') as f: | |
words = {w for ln in f if not {*(w := ln.strip())} - tiles.keys()} | |
freqs = Counter(chain.from_iterable(words)) | |
playable_words = {w for w in words if len(w) <= 7} | |
scores = {w: sum(tiles[x] for x in w) for w in playable_words} | |
by_positions = defaultdict(set) | |
for w in playable_words: | |
for pos in chain.from_iterable(combinations(enumerate(w), r=r) for r in range(1, len(w))): | |
by_positions[pos].add(w) | |
patterns = {r: [*combinations(range(7), r=r)] for r in range(1, 6+1)} | |
samples = { | |
sz: [( | |
prob := (*zip(rnd.choice(pats), rnd.choices([*freqs], k=sz, weights=[*freqs.values()])),), | |
solns := by_positions[prob], | |
) for _ in range(10_000)] | |
for sz, pats in patterns.items() | |
} | |
stats = { | |
sz: ( | |
1 - mean(not solns for _, solns in samp), | |
mean(scores[sol] for _, solns in samp for sol in solns), | |
) for sz, samp in samples.items() | |
} | |
print(f'{"blanks":^5} {"solvable":^9} {"avg. score":^11}') | |
for size, (solvable, avg_score) in stats.items(): | |
print(f'{7 - size:^5} {f"{solvable * 100:5.2f}%":^9} {f"{avg_score:5.2f}":^11}') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from itertools import tee, islice | |
from typing import Literal | |
from hypothesis import given | |
from hypothesis.strategies import lists, integers | |
# or `itertools.pairwise`… | |
nwise = lambda g, *, n=2: zip(*(islice(g, i, None) for i, g in enumerate(tee(g, n)))) | |
def sort(xs : list, kind : Literal['bubble', 'selection'] = 'bubble'): | |
idxs = range(len(xs)) | |
match kind: | |
case 'bubble': | |
swaps = [ | |
(lft, rgt) | |
for rnd in (reversed(idxs[low:]) for low in idxs) | |
for lft, rgt in nwise(rnd) if xs[lft] < xs[rgt] | |
] | |
case 'selection': | |
swaps = [(idx, min(idxs[idx:], key=xs.__getitem__)) for idx in idxs] | |
case _: | |
raise ValueError(f'unknown {kind = }') | |
for a, b in swaps: | |
xs[a], xs[b] = xs[b], xs[a] | |
@given(xs=lists(integers())) | |
def test_sort(xs): | |
ys, zs = xs.copy(), xs.copy() | |
xs.sort() | |
sort(ys, kind='bubble') | |
sort(zs, kind='selection') | |
assert xs == ys == zs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from argparse import ArgumentParser | |
from asyncio import run, TaskGroup, start_server, sleep as aio_sleep | |
from collections import Counter | |
from collections.abc import Callable | |
from dataclasses import dataclass, field | |
from decimal import Decimal | |
from logging import getLogger, basicConfig, INFO | |
from math import ceil, floor | |
from weakref import WeakSet, WeakKeyDictionary | |
logger = getLogger(__name__) | |
@dataclass | |
class Player: | |
name : str | |
credit_card : str | |
factor : Decimal = Decimal('1') | |
def __hash__(self): | |
return hash((self.name, self.credit_card)) | |
async def tick(self): | |
self.factor *= Decimal('.985') | |
@dataclass(frozen=True) | |
class Game: | |
host : str | |
port : int | |
callbacks : WeakSet[Callable] = field(default_factory=WeakSet) | |
scores : WeakKeyDictionary[Player, Decimal] = field(default_factory=WeakKeyDictionary) | |
async def connection(self, reader, writer): | |
writer.write('Welcome to Microtransactions, The Game®!\n'.encode()) | |
writer.write(' (must be 12 or under to play…)\n'.encode()) | |
writer.write('Name: '.encode()) | |
name = (await reader.readline()).decode().strip() | |
writer.write("Parent's Credit Card #: ".encode()) | |
credit_card = (await reader.readline()).decode().strip() | |
await writer.drain() | |
player = Player(name, credit_card) | |
self.scores[player] = Decimal('0') | |
self.callbacks.add(player.tick) # XXX ← add callback ← XXX | |
while True: | |
try: | |
writer.write('Payment: '.encode()) | |
if not (ln := (await reader.readline()).decode().strip()): | |
break | |
payment = ceil(Decimal(ln)) | |
points = floor(payment * player.factor) | |
if payment < 0: | |
raise ValueError("that's not how this works") | |
self.scores[player] += points | |
if payment >= self.scores[player]: | |
player.factor = Decimal('1') | |
writer.write(f'You scored {points} pts (total: {self.scores[player]})!\n'.encode()) | |
await writer.drain() | |
except Exception as e: break | |
writer.close() | |
await writer.wait_closed() | |
async def tick(self): | |
while True: | |
logger.info('Callbacks: %r', self.callbacks) | |
logger.info('Active players: %r', [*self.scores]) | |
logger.info('Leaderboard: %s', ' → '.join( | |
f'{player.name} ({score})' for player, score in Counter(self.scores).most_common(3) | |
)) | |
for cb in self.callbacks: # XXX ← callback never called ← XXX | |
await cb() | |
await aio_sleep(1) | |
async def __call__(self): | |
s = await start_server(self.connection, self.host, self.port) | |
async with s, TaskGroup() as tg: | |
tg.create_task(s.serve_forever()) | |
tg.create_task(self.tick()) | |
parser = ArgumentParser() | |
parser.add_argument('host', help='bind to host address', default='0.0.0.0') | |
parser.add_argument('port', help='bind to port', default=23, type=int) | |
parser.add_argument('-v', '--verbose', action='store_true', default=False) | |
if __name__ == '__main__': | |
args = parser.parse_args() | |
if args.verbose: | |
basicConfig(level=INFO) | |
run(Game(args.host, args.port)()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
from dataclasses import dataclass | |
from collections import namedtuple | |
from functools import wraps | |
from random import Random | |
from statistics import mean | |
from itertools import groupby | |
pumped = lambda coro: wraps(coro)(lambda *a, **kw: [ci := coro(*a, **kw), next(ci)][0]) | |
def registrar(): | |
def dec(coro): | |
dec.entries.add(coro := pumped(coro)) | |
return coro | |
dec.entries = {*()} | |
return dec | |
game, solver = registrar(), registrar() | |
@dataclass | |
class Response: | |
value : str | |
_SUBTYPES = {} | |
def __init_subclass__(cls, predicate): | |
cls._SUBTYPES[predicate] = cls | |
cls.__call__ = lambda self, g, guess: predicate(self.value, g, guess) | |
@classmethod | |
def from_guess(cls, guess, answer): | |
for g, a in zip(guess, answer, strict=True): | |
for pred, subcls in cls._SUBTYPES.items(): | |
if pred(g, a, answer): | |
yield subcls(g) | |
break | |
class Correct (Response, predicate=lambda g, a, _: g == a ): pass | |
class Present (Response, predicate=lambda g, _, ans: g in ans ): pass | |
class NotPresent(Response, predicate=lambda g, _, ans: g not in ans): pass | |
@game | |
def wordle(answer, *, max_tries=5): | |
guess = yield | |
for _ in range(max_tries): | |
if guess == answer: | |
return True | |
guess = yield [*Response.from_guess(guess, answer)] | |
return False | |
@solver | |
def random_solver(wordlist, *, rnd=None): | |
rnd = Random() if rnd is None else rnd | |
guesslist = sorted(wordlist) | |
rnd.shuffle(guesslist) | |
while guesslist: | |
_ = yield guesslist.pop() | |
@solver | |
def smarter_solver(wordlist, *, rnd=None): | |
rnd = Random() if rnd is None else rnd | |
guesslist = sorted(wordlist) | |
response = None | |
while True: | |
guesslist = [ | |
word for word in guesslist if response is None or all( | |
r(w, word) for r, w in zip(response, word) | |
) | |
] | |
response = yield rnd.choice(guesslist) | |
if __name__ == '__main__': | |
with open('/usr/share/dict/words') as f: | |
wordlist = sorted({w for ln in f if len(w := ln.strip()) == 5}) | |
Puzzle = namedtuple('Puzzle', 'game solver target max_tries') | |
rnd = Random(0) | |
puzzles = { | |
Puzzle( | |
game=g(tgt, max_tries=mt), | |
solver=s(wordlist, rnd=Random(1)), | |
target=tgt, max_tries=mt, | |
) | |
for tgt in rnd.choices(wordlist, k=100) | |
for s in solver.entries | |
for g in game.entries | |
for mt in {5, 10, 100, 1_000} | |
} | |
results = {} | |
for puzz in puzzles: | |
resp = None | |
while True: | |
guess = puzz.solver.send(resp) | |
try: | |
resp = puzz.game.send(guess) | |
except StopIteration as e: | |
break | |
results[puzz] = e.value | |
key = lambda kv: (kv[0].game.__name__, kv[0].solver.__name__, kv[0].max_tries) | |
summary = groupby(sorted(results.items(), key=key), key=key) | |
for (game_name, solver_name, max_tries), results in summary: | |
successes = [r for _, r in results] | |
print( | |
f'{game_name:<10} {solver_name:<16} within {max_tries:>5,} tries', | |
f'{mean(successes)*100:>5.1f}% solved', | |
sep=' ' | |
) |
Order of items in set not guaranteed - could this be reason for instability?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For a full-discussion of the answer, solution, and how to debug this code, join us for our seminar on Fri Feb 24, 2023: https://www.eventbrite.com/e/548056811677/