#!/usr/bin/env python3 """Strategy balance simulation for Lantern Auction (same rules as server.js). python3 tools/balance_sim.py""" import random PRIZES = [-3, -2, -1, 1, 2, 3, 4, 5, 6, 7, 8] def rank_match(hand, prize, pot, left): # play the card whose rank equals the prize's rank among prizes left return sorted(hand)[sorted(left).index(prize)] def counter(hand, prize, pot, left): # rank-match plus one on Lanterns h = sorted(hand); i = sorted(left).index(prize) return h[min(len(h) - 1, i + 1)] if prize > 0 else h[i] def rnd(hand, *a): return random.choice(hand) def house(hand, prize, pot, left): # mirrors botMove() in server.js h = sorted(hand); i = sorted(left).index(prize) if sum(pot) > 0 and prize > 0: i += 1 if random.random() < 0.15: return random.choice(h) i += random.choice([-1, 0, 0, 1]); return h[max(0, min(len(h) - 1, i))] S = {'house': house, 'rank_match': rank_match, 'counter': counter, 'random': rnd} def game(strats): hands = [list(range(1, 12)) for _ in strats]; score = [0] * len(strats); deck = random.sample(PRIZES, len(PRIZES)); pot = [] for k, p in enumerate(deck): left = deck[k:] bids = [S[s](hands[i], p, pot, left) for i, s in enumerate(strats)] for i, b in enumerate(bids): hands[i].remove(b) uniq = [(b, i) for i, b in enumerate(bids) if bids.count(b) == 1] if uniq: w = (max(uniq) if p > 0 else min(uniq))[1]; score[w] += p + sum(pot); pot = [] else: pot.append(p) top = max(score); return [s == top for s in score] if __name__ == '__main__': N = 20000 for pair in [('rank_match', 'random'), ('counter', 'rank_match'), ('house', 'rank_match'), ('house', 'counter'), ('house', 'random')]: w = [0, 0] for _ in range(N): r = game(list(pair)); w[0] += r[0]; w[1] += r[1] print(f'{pair[0]:>10} vs {pair[1]:<10} {w[0]/N:.0%} / {w[1]/N:.0%}') for n in (3, 4): wins = dict.fromkeys(S, 0); games = dict.fromkeys(S, 0) for _ in range(N): st = random.choices(list(S), k=n); r = game(st) for s, x in zip(st, r): wins[s] += x; games[s] += 1 print(f'{n} players, mixed field:', {k: f'{wins[k]/games[k]:.0%}' for k in S})