Skip to content

Instantly share code, notes, and snippets.

@dutc
Last active January 2, 2024 22:11
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dutc/88d7f2d30527be9842b2c06b3c14cf77 to your computer and use it in GitHub Desktop.
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/)
#!/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}')
#!/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
#!/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)())
#!/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=' '
)
@dutc
Copy link
Author

dutc commented Feb 18, 2023

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/

@epogrebnyak
Copy link

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