"""Python port of O'BLOCK's single-pair deal (src/deal.rs, Deal::new_lot_at + Deal::offer), shipping code read Oct 24 2026.
Leaves out world modifiers that scale limit and ask together (weather, taste, angles, memory) and the rival bid.
Value normalised to 100. Street band = value*(1 -/+ spread); spread 0.36 for a new player (world.rs band_spread)."""
import json, random, csv, sys
from collections import defaultdict
# The 121 people from O'BLOCK src/deal.rs people() (outlets left out)
P = [{"name":"LIL REECE","tier":"Block","patience":3,"greed":0.7,"sharp":0.3,"picky":0.2},{"name":"TADONE","tier":"Block","patience":2,"greed":0.5,"sharp":0.2,"picky":0.1},{"name":"BAILOUT","tier":"Block","patience":4,"greed":0.4,"sharp":0.4,"picky":0.3},{"name":"HD","tier":"Block","patience":3,"greed":0.6,"sharp":0.5,"picky":0.4},{"name":"CAPPO","tier":"Block","patience":2,"greed":0.8,"sharp":0.3,"picky":0.2},{"name":"LIL JAYE","tier":"Block","patience":3,"greed":0.9,"sharp":0.2,"picky":0.1},{"name":"EDDIE AI","tier":"Block","patience":4,"greed":0.3,"sharp":0.5,"picky":0.3},{"name":"LIL MICE","tier":"Block","patience":3,"greed":0.6,"sharp":0.1,"picky":0.1},{"name":"KATIE GOT BANDS","tier":"Block","patience":3,"greed":0.5,"sharp":0.6,"picky":0.5},{"name":"SASHA GO SOFT","tier":"Block","patience":4,"greed":0.4,"sharp":0.4,"picky":0.4},{"name":"THE PLUG","tier":"Block","patience":2,"greed":0.5,"sharp":0.9,"picky":0.2},{"name":"THE PLUG'S BOSS","tier":"Block","patience":2,"greed":0.6,"sharp":1.0,"picky":0.5},{"name":"THE BOX MAN","tier":"Block","patience":3,"greed":0.45,"sharp":0.8,"picky":0.0},{"name":"AUNTIE","tier":"Block","patience":5,"greed":0.1,"sharp":0.05,"picky":0.0},{"name":"KANG LOUIE","tier":"City","patience":3,"greed":0.5,"sharp":0.6,"picky":0.5},{"name":"LIL BIPPY","tier":"City","patience":4,"greed":0.5,"sharp":0.5,"picky":0.4},{"name":"G HERBAL","tier":"City","patience":3,"greed":0.6,"sharp":0.7,"picky":0.5},{"name":"MONTANA OF 3000","tier":"City","patience":5,"greed":0.3,"sharp":0.8,"picky":0.6},{"name":"YOUNG CHIP","tier":"City","patience":3,"greed":0.4,"sharp":0.6,"picky":0.3},{"name":"POLO H","tier":"City","patience":3,"greed":0.5,"sharp":0.7,"picky":0.6},{"name":"DREEZE","tier":"City","patience":4,"greed":0.4,"sharp":0.7,"picky":0.5},{"name":"TINC","tier":"City","patience":3,"greed":0.5,"sharp":0.6,"picky":0.6},{"name":"CHANTS THE RAPPER","tier":"City","patience":5,"greed":0.2,"sharp":0.5,"picky":0.7},{"name":"VIC MENSAH","tier":"City","patience":3,"greed":0.5,"sharp":0.8,"picky":0.7},{"name":"TWISTER","tier":"City","patience":2,"greed":0.6,"sharp":0.9,"picky":0.5},{"name":"LOOPY FIASCO","tier":"City","patience":4,"greed":0.3,"sharp":0.9,"picky":0.8},{"name":"UNCOMMON","tier":"City","patience":4,"greed":0.2,"sharp":0.7,"picky":0.9},{"name":"CHIEF REEF","tier":"Fame","patience":2,"greed":0.4,"sharp":0.5,"picky":0.8},{"name":"LIL DURT","tier":"Fame","patience":3,"greed":0.5,"sharp":0.7,"picky":0.7},{"name":"FREDO FONTANA","tier":"Fame","patience":2,"greed":0.6,"sharp":0.4,"picky":0.6},{"name":"DRIZZY","tier":"Fame","patience":4,"greed":0.3,"sharp":0.6,"picky":0.9},{"name":"KAN YAY","tier":"Fame","patience":1,"greed":0.2,"sharp":0.9,"picky":1.0},{"name":"JAY-ZED","tier":"Fame","patience":3,"greed":0.4,"sharp":0.95,"picky":0.9},{"name":"RICK BOSS","tier":"Fame","patience":3,"greed":0.5,"sharp":0.5,"picky":0.6},{"name":"VIRGIL ABLOW","tier":"Fame","patience":4,"greed":0.3,"sharp":0.9,"picky":1.0},{"name":"TRAVIS SCOTCH","tier":"Fame","patience":2,"greed":0.4,"sharp":0.8,"picky":1.0},{"name":"3 CHAINZ","tier":"Fame","patience":3,"greed":0.5,"sharp":0.6,"picky":0.7},{"name":"DJ SHALLOT","tier":"Fame","patience":2,"greed":0.3,"sharp":0.4,"picky":0.9},{"name":"NIKKI MIRAGE","tier":"Fame","patience":3,"greed":0.6,"sharp":0.8,"picky":1.0},{"name":"GUCCI MAIN","tier":"Fame","patience":3,"greed":0.5,"sharp":0.5,"picky":0.6},{"name":"WACKA FLOCKA","tier":"Fame","patience":2,"greed":0.7,"sharp":0.3,"picky":0.4},{"name":"SOULJA BOI","tier":"Fame","patience":4,"greed":0.6,"sharp":0.6,"picky":0.5},{"name":"ONSET","tier":"Fame","patience":3,"greed":0.5,"sharp":0.6,"picky":0.8},{"name":"ELON TUSK","tier":"Billionaire","patience":1,"greed":0.1,"sharp":0.4,"picky":1.0},{"name":"JEFF BOZOS","tier":"Billionaire","patience":2,"greed":0.2,"sharp":0.9,"picky":1.0},{"name":"MARK ZUCKERBIRD","tier":"Billionaire","patience":2,"greed":0.3,"sharp":0.8,"picky":0.9},{"name":"WARREN BUFFET","tier":"Billionaire","patience":5,"greed":0.6,"sharp":1.0,"picky":0.8},{"name":"BILL GAITS","tier":"Billionaire","patience":3,"greed":0.3,"sharp":0.9,"picky":0.9},{"name":"OKRA","tier":"Billionaire","patience":3,"greed":0.1,"sharp":0.7,"picky":1.0},{"name":"MICHAEL JORDYN","tier":"Billionaire","patience":1,"greed":0.0,"sharp":1.0,"picky":1.0},{"name":"LIL SLEEPY","tier":"Block","patience":5,"greed":0.3,"sharp":0.1,"picky":0.1},{"name":"PEEZY","tier":"Block","patience":2,"greed":0.9,"sharp":0.3,"picky":0.2},{"name":"BIG DRE","tier":"Block","patience":4,"greed":0.4,"sharp":0.5,"picky":0.3},{"name":"COACH MO","tier":"Block","patience":4,"greed":0.2,"sharp":0.6,"picky":0.5},{"name":"MS PAT","tier":"Block","patience":5,"greed":0.3,"sharp":0.3,"picky":0.6},{"name":"THE BARBER","tier":"Block","patience":3,"greed":0.5,"sharp":0.7,"picky":0.4},{"name":"DELIVERY DAN","tier":"Block","patience":2,"greed":0.6,"sharp":0.2,"picky":0.1},{"name":"COUSIN","tier":"Block","patience":6,"greed":0.2,"sharp":0.1,"picky":0.0},{"name":"SABBA","tier":"City","patience":4,"greed":0.3,"sharp":0.7,"picky":0.7},{"name":"NO NAME","tier":"City","patience":5,"greed":0.2,"sharp":0.8,"picky":0.8},{"name":"SMEENO","tier":"City","patience":3,"greed":0.5,"sharp":0.6,"picky":0.5},{"name":"JOEY PURPLE","tier":"City","patience":3,"greed":0.6,"sharp":0.5,"picky":0.4},{"name":"ROCKY FRESH","tier":"City","patience":3,"greed":0.5,"sharp":0.6,"picky":0.7},{"name":"BJ THE CHI KID","tier":"City","patience":4,"greed":0.3,"sharp":0.6,"picky":0.6},{"name":"CUPCAKE","tier":"City","patience":2,"greed":0.8,"sharp":0.5,"picky":0.3},{"name":"TOWKYO","tier":"City","patience":3,"greed":0.5,"sharp":0.5,"picky":0.5},{"name":"SUPA BWAY","tier":"City","patience":3,"greed":0.7,"sharp":0.4,"picky":0.3},{"name":"THE PROMOTER","tier":"City","patience":2,"greed":0.9,"sharp":0.7,"picky":0.3},{"name":"THE SNEAKER YOUTUBER","tier":"City","patience":4,"greed":0.4,"sharp":0.9,"picky":0.9},{"name":"MEEK HILL","tier":"Fame","patience":2,"greed":0.6,"sharp":0.6,"picky":0.7},{"name":"PAST","tier":"Fame","patience":2,"greed":0.5,"sharp":0.5,"picky":0.8},{"name":"MIGGOS","tier":"Fame","patience":3,"greed":0.6,"sharp":0.5,"picky":0.7},{"name":"LIL OOZY","tier":"Fame","patience":2,"greed":0.4,"sharp":0.4,"picky":0.9},{"name":"CARDY B","tier":"Fame","patience":3,"greed":0.7,"sharp":0.6,"picky":0.8},{"name":"TYLA THE CREATOR","tier":"Fame","patience":4,"greed":0.3,"sharp":0.9,"picky":1.0},{"name":"ASAP ROCKIE","tier":"Fame","patience":3,"greed":0.4,"sharp":0.8,"picky":1.0},{"name":"J COAL","tier":"Fame","patience":5,"greed":0.2,"sharp":0.9,"picky":0.7},{"name":"KENDRIQUE","tier":"Fame","patience":3,"greed":0.3,"sharp":1.0,"picky":1.0},{"name":"LIL BABEE","tier":"Fame","patience":2,"greed":0.6,"sharp":0.5,"picky":0.6},{"name":"GUNNER","tier":"Fame","patience":3,"greed":0.5,"sharp":0.5,"picky":0.9},{"name":"22 SAVAGE","tier":"Fame","patience":2,"greed":0.7,"sharp":0.5,"picky":0.5},{"name":"PLAYBOY CARTY","tier":"Fame","patience":1,"greed":0.5,"sharp":0.3,"picky":1.0},{"name":"DABABEE","tier":"Fame","patience":2,"greed":0.6,"sharp":0.4,"picky":0.5},{"name":"NAZ","tier":"Fame","patience":4,"greed":0.3,"sharp":1.0,"picky":0.9},{"name":"LIL YACHT","tier":"Fame","patience":3,"greed":0.5,"sharp":0.4,"picky":0.6},{"name":"MEG THEE","tier":"Fame","patience":3,"greed":0.5,"sharp":0.7,"picky":0.8},{"name":"RIRI-ANNA","tier":"Billionaire","patience":3,"greed":0.2,"sharp":0.8,"picky":1.0},{"name":"TIM APPLE","tier":"Billionaire","patience":3,"greed":0.3,"sharp":0.9,"picky":1.0},{"name":"BERNARD ARMANI","tier":"Billionaire","patience":4,"greed":0.4,"sharp":1.0,"picky":1.0},{"name":"LE BRON","tier":"Billionaire","patience":2,"greed":0.2,"sharp":0.7,"picky":1.0},{"name":"TIGER WOODY","tier":"Billionaire","patience":3,"greed":0.3,"sharp":0.8,"picky":0.9},{"name":"MESSY","tier":"Billionaire","patience":2,"greed":0.1,"sharp":0.5,"picky":1.0},{"name":"MUKESH AMBANEE","tier":"Billionaire","patience":3,"greed":0.3,"sharp":1.0,"picky":1.0},{"name":"THE SHEIKH","tier":"Legend","patience":2,"greed":0.0,"sharp":0.6,"picky":1.0},{"name":"THE MUSEUM","tier":"Legend","patience":4,"greed":0.3,"sharp":1.0,"picky":1.0},{"name":"THE FUND","tier":"Legend","patience":3,"greed":0.5,"sharp":1.0,"picky":0.9},{"name":"THE PRINCE","tier":"Legend","patience":1,"greed":0.0,"sharp":0.3,"picky":1.0},{"name":"THE LEAGUE","tier":"Legend","patience":3,"greed":0.3,"sharp":0.9,"picky":1.0},{"name":"THE NETWORK","tier":"Legend","patience":2,"greed":0.4,"sharp":0.8,"picky":0.9},{"name":"THE CATHEDRAL","tier":"Legend","patience":5,"greed":0.1,"sharp":0.7,"picky":1.0},{"name":"THE SOVEREIGN FUND","tier":"Legend","patience":4,"greed":0.5,"sharp":1.0,"picky":1.0},{"name":"DEACON","tier":"Block","patience":4,"greed":0.3,"sharp":0.8,"picky":0.4},{"name":"MS. HOLLOWAY","tier":"Block","patience":3,"greed":0.6,"sharp":0.9,"picky":0.7},{"name":"OLD HECTOR","tier":"Block","patience":5,"greed":0.2,"sharp":1.0,"picky":0.9},{"name":"JUNIOR","tier":"Block","patience":4,"greed":0.3,"sharp":0.1,"picky":0.1},{"name":"NINA ON 64TH","tier":"Block","patience":3,"greed":0.6,"sharp":0.5,"picky":0.3},{"name":"BIG TONE","tier":"Block","patience":4,"greed":0.4,"sharp":0.6,"picky":0.3},{"name":"JUNE","tier":"City","patience":4,"greed":0.3,"sharp":0.7,"picky":0.5},{"name":"MAMA DEE","tier":"City","patience":5,"greed":0.2,"sharp":0.6,"picky":0.6},{"name":"SILK","tier":"City","patience":3,"greed":0.4,"sharp":0.8,"picky":1.0},{"name":"THE ADJUSTER","tier":"City","patience":3,"greed":0.7,"sharp":0.9,"picky":0.5},{"name":"THE ACCOUNTANT","tier":"City","patience":4,"greed":0.5,"sharp":1.0,"picky":0.4},{"name":"THE BROKER","tier":"Fame","patience":3,"greed":0.6,"sharp":0.9,"picky":0.6},{"name":"VIOLET","tier":"Fame","patience":3,"greed":0.5,"sharp":0.9,"picky":0.9},{"name":"FAITH","tier":"Fame","patience":4,"greed":0.3,"sharp":0.8,"picky":1.0},{"name":"DOC","tier":"Fame","patience":3,"greed":0.4,"sharp":1.0,"picky":0.9},{"name":"PELL","tier":"Billionaire","patience":3,"greed":0.4,"sharp":0.9,"picky":0.8},{"name":"MADAME ORO","tier":"Billionaire","patience":3,"greed":0.5,"sharp":0.8,"picky":1.0},{"name":"DR. OKAFOR","tier":"Billionaire","patience":4,"greed":0.2,"sharp":1.0,"picky":1.0},{"name":"THE HEIR","tier":"Billionaire","patience":3,"greed":0.3,"sharp":0.4,"picky":0.8},{"name":"MR. VANTA","tier":"Legend","patience":2,"greed":0.1,"sharp":1.0,"picky":1.0}]
TIER = {'Block': 0, 'City': 1, 'Fame': 2, 'Billionaire': 3, 'Legend': 4}
V = 100.0


class Deal:
    def __init__(s, who, side, rng, rule=None, patience=None):
        s.side = side; s.rng = rng; s.rule = rule
        early = TIER[who['tier']] <= 1
        mood = rng.random() * 2 - 1
        if early:
            mood = max(mood, -0.2)
        s.mood = mood
        noise = (rng.random() * 2 - 1) * (1 - who['sharp']) * 0.45
        tv = V * (1 + noise)
        want = mood * 0.04
        bg, sg = 0.08 + who['greed'] * 0.30, 0.04 + who['greed'] * 0.16
        if side == 'buy':   # they sell to you; limit = their floor
            s.limit = tv * min(max(1 - bg + want * 0.5, 0.3), 0.98)
            s.ask = s.limit * (1.15 + who['greed'] * 0.35)
        else:               # they buy from you; limit = their ceiling
            s.limit = tv * min(max(1 - sg + want, 0.3), 1.02)
            s.ask = s.limit * (0.8 + who['greed'] * 0.15)
        s.full_limit = s.limit; s.drift_home = s.limit
        s.patience = patience if patience else who['patience']
        s.n = 0; s.done = None; s.standing = None; s.last_heat = None

    def good(s, price):
        line = s.limit
        if s.standing is not None:
            line = min(s.limit, s.standing) if s.side == 'buy' else max(s.limit, s.standing)
        return price >= line if s.side == 'buy' else price <= line

    def dist(s, price):
        return (s.limit - price) / s.limit if s.side == 'buy' else (price - s.limit) / s.limit

    def offer(s, price):
        s.n += 1
        if s.good(price):
            s.done = ('closed', price); r = ('accept', None)
        else:
            d = s.dist(price)
            pat = round(s.patience + max(s.mood, 0) * 1.5)
            if s.n >= max(pat, 1):
                s.done = ('walked', None); r = ('walk', None)
            elif s.rule is None and d < 0.12 and s.rng.random() < 0.55:
                c = s.limit + (price - s.limit) * 0.35
                if s.standing is not None:
                    c = min(c, s.standing) if s.side == 'buy' else max(c, s.standing)
                c = round(c, 2); s.standing = c; r = ('counter', c)
            elif s.rule == 'silent':
                r = ('tell', 0.5)
            elif s.rule == 'mirror':
                h = min(max(1 - d / 0.5, 0), 1); shown = s.last_heat; s.last_heat = h
                r = ('tell', shown if shown is not None else 0.5)
            elif s.rule == 'fog':
                h = min(max(1 - d / 0.5, 0), 1); r = ('tell', 0.85 if h > 0.75 else 0.5 if h > 0.4 else 0.15)
            else:
                if s.rule == 'liar':
                    d = max(0.5 - min(max(d, 0), 0.5), 0)
                r = ('tell', min(max(1 - d / 0.5, 0), 1))
        if s.rule == 'haggler' and s.done is None:
            s.limit *= 1.05 if s.side == 'buy' else 0.95
        if s.rule == 'drifter' and s.done is None:
            s.drift_home = s.drift_home * 0.85 + max(price, 1) * 0.15
            z = s.rng.gauss(0, 1)
            s.limit = min(max(s.limit + 0.35 * (s.drift_home - s.limit) + 0.07 * s.limit * z, s.full_limit * 0.7), s.full_limit * 1.3)
        return r

    def take_counter(s):
        s.done = ('closed', s.standing)


SPREAD = 0.36


def band():
    return V * (1 - SPREAD), V * (1 + SPREAD)

# Strategies get only what the screen shows: the ask, the street band, replies (tell bar, counter).


def st_ask(d, rng):
    d.offer(d.ask)


def st_mid(d, rng):
    d.offer(V)


def st_split(d, rng):  # open at your end of the band, then split the difference with their number each round
    lo, hi = band(); p = lo if d.side == 'buy' else hi
    for _ in range(12):
        if d.done:
            return
        r = d.offer(round(p, 2))
        if r[0] == 'counter':
            d.take_counter(); return
        if r[0] in ('accept', 'walk'):
            return
        p = (p + d.ask) / 2


def st_ackerman(d, rng, target=None):  # Voss: 65/85/95/100% of target (mirrored for sellers: 135/115/105/100)
    T = target if target else V
    steps = [0.65, 0.85, 0.95, 1.0] if d.side == 'buy' else [1.35, 1.15, 1.05, 1.0]
    for i, f in enumerate(steps):
        if d.done:
            return
        p = T * f
        if i == 3:
            p = p - 0.37 if d.side == 'buy' else p + 0.37   # the precise, non-round last number
        r = d.offer(round(p, 2))
        if r[0] == 'counter':
            c = r[1]
            if (d.side == 'buy' and c <= T) or (d.side == 'sell' and c >= T):
                d.take_counter(); return
        if r[0] in ('accept', 'walk'):
            return


def st_ackerman_low(d, rng):
    lo, hi = band()
    return st_ackerman(d, rng, target=(lo + V) / 2 if d.side == 'buy' else (hi + V) / 2)


def st_hold(d, rng):  # lowball and hold
    p = V * 0.5 if d.side == 'buy' else V * 1.5
    for _ in range(12):
        if d.done:
            return
        d.offer(p)


def st_meet(d, rng):  # Beat-the-haggle move 2: open low, come up, take the number he names
    steps = [0.64, 0.82, 0.95, 1.05] if d.side == 'buy' else [1.36, 1.18, 1.05, 0.95]
    for f in steps:
        if d.done:
            return
        r = d.offer(round(V * f, 2))
        if r[0] == 'counter':
            d.take_counter(); return


def st_listen(d, rng, noise=0.04):  # read the tell bar, solve for their number, say it
    lo, hi = band(); p = lo if d.side == 'buy' else hi
    for _ in range(12):
        if d.done:
            return
        r = d.offer(round(p, 2))
        if r[0] == 'counter':
            d.take_counter(); return
        if r[0] in ('accept', 'walk'):
            return
        heat = min(max(r[1] + rng.uniform(-noise, noise), 0), 1)
        if heat <= 0.02:   # off the bar: too far to read, move 15%
            p = p * 1.15 if d.side == 'buy' else p / 1.15
            continue
        dd = (1 - heat) * 0.5
        est = p / (1 - dd) if d.side == 'buy' else p / (1 + dd)
        p = est * 1.01 if d.side == 'buy' else est * 0.99


STRATS = [('Pay the ask', st_ask), ('Offer the street price', st_mid), ('Split the difference', st_split),
          ('Ackerman (Voss), target = street', st_ackerman), ('Ackerman, target = low half', st_ackerman_low),
          ('Lowball and hold', st_hold), ('Open low, take his number', st_meet), ('Read the bar', st_listen)]


def run(N, side, seed):
    rng = random.Random(seed)
    agg = defaultdict(lambda: defaultdict(float))
    for i in range(N):
        who = P[i % len(P)]
        s0 = rng.randrange(1 << 30)
        for name, f in STRATS:
            d = Deal(who, side, random.Random(s0))
            f(d, random.Random(s0 + 1))
            a = agg[(name, who['tier'])]
            a['n'] += 1
            if d.done and d.done[0] == 'closed':
                pr = d.done[1]
                a['closed'] += 1; a['price'] += pr
                a['gain'] += (V - pr) if side == 'buy' else (pr - V)   # vs the street value
                a['left'] += (pr - d.full_limit) if side == 'buy' else (d.full_limit - pr)
                a['offers'] += d.n
    return agg


def rules_run(N, seed):
    rng = random.Random(seed); out = defaultdict(lambda: defaultdict(float))
    for rule in ['none', 'liar', 'mirror', 'fog', 'silent', 'haggler', 'drifter']:
        for i in range(N):
            who = P[i % len(P)]; s0 = rng.randrange(1 << 30)
            for name, f in STRATS:
                d = Deal(who, 'buy', random.Random(s0), rule=None if rule == 'none' else rule, patience=4)
                f(d, random.Random(s0 + 1))
                a = out[(rule, name)]; a['n'] += 1
                if d.done and d.done[0] == 'closed':
                    pr = d.done[1]; a['closed'] += 1; a['gain'] += V - pr
                    if pr <= d.full_limit * 1.08 + 0.5:
                        a['pass'] += 1
    return out


if __name__ == '__main__':
    N = int(sys.argv[1]) if len(sys.argv) > 1 else 20000
    rows = []
    for side in ['buy', 'sell']:
        agg = run(N, side, 7 if side == 'buy' else 11)
        for (name, tier), a in agg.items():
            rows.append(dict(side=side, strategy=name, tier=tier, deals=int(a['n']), closed=int(a['closed']),
                             close_rate=round(a['closed'] / a['n'], 4),
                             avg_price_pct_of_street=round(a['price'] / max(a['closed'], 1), 2),
                             avg_gain_per_attempt_pct=round(a['gain'] / a['n'], 2),
                             avg_left_on_table_pct=round(a['left'] / max(a['closed'], 1), 2),
                             avg_offers_to_close=round(a['offers'] / max(a['closed'], 1), 2)))
    with open('strategies_by_tier.csv', 'w', newline='') as f:
        w = csv.DictWriter(f, fieldnames=list(rows[0])); w.writeheader(); w.writerows(rows)
    rr = rules_run(N // 4, 13); rrows = []
    for (rule, name), a in rr.items():
        rrows.append(dict(rule=rule, strategy=name, deals=int(a['n']), close_rate=round(a['closed'] / a['n'], 4),
                          gate_pass_rate=round(a['pass'] / a['n'], 4), avg_gain_per_attempt_pct=round(a['gain'] / a['n'], 2)))
    with open('strategies_by_rule.csv', 'w', newline='') as f:
        w = csv.DictWriter(f, fieldnames=list(rrows[0])); w.writeheader(); w.writerows(rrows)
    print('total haggles', sum(r['deals'] for r in rows) + sum(r['deals'] for r in rrows))
