# Bingo Card Maker Free: simulation of calls needed per 75-ball bingo pattern.
# Method: https://bingocardmakerfree.com/blog/bingo-patterns#method
# Requires Python 3 and NumPy. Run: python3 bingo-pattern-simulation.py

import numpy as np, json, math
rng = np.random.default_rng(20260916)
GAMES = 20000
PLAYERS = [1, 10, 25, 50]
idx = lambda r,c: r*5+c
L=range(5)
lines=[[idx(r,c) for c in L] for r in L]+[[idx(r,c) for r in L] for c in L]+[[idx(i,i) for i in L],[idx(i,4-i) for i in L]]
patterns = {
 "Any line": lines,
 "Hardway line (no free space)": [l for l in lines if 12 not in l],
 "Four corners": [[0,4,20,24]],
 "Postage stamp (any corner)": [[0,1,5,6],[3,4,8,9],[15,16,20,21],[18,19,23,24]],
 "X (both diagonals)": [sorted(set([idx(i,i) for i in L]+[idx(i,4-i) for i in L]))],
 "Outside frame": [[idx(r,c) for r in L for c in L if r in (0,4) or c in (0,4)]],
 "Blackout (coverall)": [list(range(25))],
}
def make_cards(n):
    cards=np.zeros((n,25),dtype=np.int16)
    for k in range(n):
        cols=[]
        for c in range(5):
            cnt=4 if c==2 else 5
            cols.append(rng.choice(np.arange(c*15+1,c*15+16),cnt,replace=False))
        g=[]
        for r in range(5):
            for c in range(5):
                if r==2 and c==2: g.append(0)
                else:
                    col=cols[c]; i = r if not (c==2 and r>2) else r-1
                    g.append(col[i])
        cards[k]=g
    return cards
results={name:{p:[] for p in PLAYERS} for name in patterns}
maxp=max(PLAYERS)
for game in range(GAMES):
    order=rng.permutation(75)+1
    pos=np.zeros(76,dtype=np.int16); pos[order]=np.arange(1,76)  # call index of each number
    cards=make_cards(maxp)
    t=pos[cards]; t[:,12]=0  # free space
    for name,alts in patterns.items():
        best=np.min(np.stack([t[:,a].max(axis=1) for a in alts],axis=1),axis=1)  # per card
        for p in PLAYERS:
            results[name][p].append(int(best[:p].min()))
out={}
for name in patterns:
    out[name]={}
    for p in PLAYERS:
        arr=np.array(results[name][p])
        out[name][p]={"median":int(np.median(arr)),"mean":round(float(arr.mean()),1),"p10":int(np.percentile(arr,10)),"p90":int(np.percentile(arr,90))}
json.dump({"games":GAMES,"seed":20260916,"results":out},open("pattern_calls.json","w"),indent=1)
for name in patterns:
    print(name, {p:(out[name][p]["median"],out[name][p]["p10"],out[name][p]["p90"]) for p in PLAYERS})
# card counts
perm=lambda n,k: math.perm(n,k); comb=math.comb
print("75-ball cards (order matters):", perm(15,5)**4*perm(15,4))
print("75-ball cards (sets per column):", comb(15,5)**4*comb(15,4))
