+
    L<j                        R t ^ RIHt ^ RIt^ RItR tR tR tR tR]3R lt	R t
R	 t]R
8X  d   . t]! ^4       F>  t]P!                  RR] 2RRR]! ^^4       U u. uF	  p RV RR/NK  	  up R^RRRR/4       K@  	  Rt]	! ]]4      t]! R]R,          4       ]! R]R,          4       ]! R]R,          R]R ,          R!]R",          4       R# R# u up i )#u  
HEAD OR TAIL — payout engine (pure functions, no DB, no I/O).

This is the heart of the margin guarantee. Everything here is deterministic
given its inputs, so it can be unit-tested in isolation.

Resolution order inside a single simulated outcome:
  1. Each bet -> WIN (exact tier), NEAR (5/6-coin, miss by exactly one), or LOSS.
  2. Group bets by (session, round). A group of >= CASHBACK_MIN_TICKETS that is a
     CLEAN SWEEP (every bet strictly LOSS, no WIN and no NEAR) earns cashback.
     (Near-miss and cashback are therefore mutually exclusive within a group.)
  3. Total payout = sum(wins) + sum(near-miss) + sum(cashback).

The chooser simulates all 2^NUM_COINS outcomes, keeps only those whose total
payout <= ceiling (PAYOUT_RATE * pool), and picks one favouring the house.
If NO outcome is safe (oversubscribed round), it picks the least-bad outcome
and returns a scale factor < 1 so that, after scaling every payout, the total
equals exactly the ceiling -> the house always keeps its reserve.
)productNc                      R# )HEADS)r   TAILS r       /opt/headortail/engine.py_facesr	      s    r   c                >    \        \        \        4       V R7      4      # )z=All 2^num_coins possible boards as tuples of 'HEADS'/'TAILS'.)repeat)listr   r	   )	num_coinss   &r   all_outcomesr      s    344r   c                   a  \        V 3R lV 4       4      pWR8X  d   RW4,          3# V\        P                  9   d'   WR^,
          8X  d   RV\        P                  ,          3# R# )z
Return (status, payout) for one bet against one outcome.
status in {'win','near_miss','loss'}.
outcome is a tuple indexed 0..num_coins-1.
picks is [{'coin':1..6,'pick':'HEADS'/'TAILS'}, ...].
c              3   r   <"   T F,  pSVR ,          ^,
          ,          VR,          8X  g   K(  ^x  K.  	  R# 5i)coinpickNr   ).0pkoutcomes   & r   	<genexpr>resolve_bet.<locals>.<genexpr>*   s+     Mewr&zA~'>"V*'L!!es   %7
7win	near_miss)loss        )sumconfigNEAR_MISS_SIZESNEAR_MISS_MULT)r   pickssizestake
multipliercorrects   f&&&& r   resolve_betr%   #   s[     MeMMGe(((v%%%'AX*=EF$9$9999r   c           	       a / o/ p/ pV F  p\        WR,          VR,          VR,          VR,          4      w  rVWV3SVR,          &   VR,          pVP                  V. 4      P                  VR,          4       VP                  VR4      VR,          ,           W7&   K  	  / pVP	                  4        F  w  ry\        V	4      \        P                  8  g   K%  \        ;QJ d    V3R lV	 4       F  '       d   K   R	M	  R
M! V3R lV	 4       4      '       g   Kg  W7,          \        P                  ,          W&   K  	  \        R SP                  4        4       4      \        VP                  4       4      ,           p
V
SV3# )z
Evaluate one outcome against all bets.
bets: list of dicts with keys: ticket_id, session_id, picks, size, stake, multiplier
Returns: (total_payout, results, cashback)
  results: {ticket_id: (status, payout)}
  cashback: {session_id: amount}
r    r!   r"   r#   	ticket_id
session_idr   c              3   J   <"   T F  pSV,          ^ ,          R8H  x  K  	  R# 5i)    r   Nr   )r   tidresultss   & r   r   #evaluate_outcome.<locals>.<genexpr>L   s     @73<?f,s    #FTc              3   *   "   T F	  w  rVx  K  	  R # 5i)Nr   )r   _ps   &  r   r   r-   O   s     1 0fq 0s   )r%   
setdefaultappendgetitemslenr   CASHBACK_MIN_TICKETSallCASHBACK_RATEr   values)r   betsgroup_membersgroup_stakebstatuspayoutsidcashbackmemberstotalr,   s   &&         @r   evaluate_outcomerD   2   s5    GMK$wZ6AgJ,
 $*"2+o  b)00;@&??34qzA  H%++-w<6666s@@sss@@@@ + 063G3G G	 . 1 011C8I4JJE'8##r   c                r   Vf   \         P                  pV\         P                  ,          p. p\        V4       F$  p\	        W`4      w  rxp	VP                  WvW34       K&  	  V U
u. uF  q^ ,          VR,           8:  g   K  V
NK  	  pp
V'       dW   VP                  R R7       VP                  4       \         P                  8  d   V^ ,          pMVP                  V4      pRpRpM8VP                  R R7       V^ ,          pV^ ,          pV^ 8  d	   WO,          MRpRpVw  pprRVR	VR
V	RVRVRVRV/# u up
i )a  
Pick the board to reveal.

Returns a dict:
  outcome        : tuple of faces to reveal
  results        : {ticket_id: (status, payout)}  (pre-scale)
  cashback       : {session_id: amount}           (pre-scale)
  raw_payout     : total payout before scaling
  scale          : multiply every payout by this (<1 only when oversubscribed)
  ceiling        : PAYOUT_RATE * pool
  oversubscribed : bool
g&.>c                     V ^ ,          # r*   r   es   &r   <lambda> choose_outcome.<locals>.<lambda>l   s    !r   )key      ?Fc                     V ^ ,          # rG   r   rH   s   &r   rJ   rK   t   s    QqTr   Tr   r,   rA   
raw_payoutscaleceilingoversubscribed)
r   	NUM_COINSPAYOUT_RATEr   rD   r2   sortrandom
HOUSE_BIASchoice)r:   poolr   rngrQ   	evaluatedorC   r,   rA   rI   safechosenrP   rR   rawr   s   &&&&             r   choose_outcomer`   S   s8    $$	V'''GI)$#3A#< %G67 % !;y!aDGdN$:AAyD;		n	%::<&+++!WFZZ%F>*1Qi#&7(.%E7G77He7. % <s   %D4 D4c           
        V \         P                  ,          p/ p\        ^\         P                  ^,           4       F  p\         P                  V,          pVP                  VR4      pV^ 8:  d   WSV&   K9  Wb8  dF   Wb,          p\        R\        VRV\         P                  ,          ,
          ,          ^4      4      W4&   K  RW4&   K  	  V# )z
Live, displayed multipliers that shrink as liability for a tier grows,
so the displayed odds already lean toward keeping the round safe.
Returns {size: multiplier}.
r   g?rM   )	r   rT   rangerS   BASE_MULTIPLIERSr3   maxroundLIABILITY_DAMP)
total_poolliability_by_sizerQ   outr!   baselibratios   &&      r   recalc_live_multipliersrm      s     6---G
Ca))A-.&&t,##D#.a<I]MED%ef>S>S6S0S(TVW"XYCICI / Jr   c                0    \        \        V 4      4      ^8H  # )z2All coins the same face -> jackpot-eligible board.)r5   set)r   s   &r   is_perfect_boardrp      s    s7|!!r   __main__r'   Tr(   P1r    r   r   r   r!   r"   g      Y@r#   g      N@g     @@zCeiling:rQ   zChosen board:r   zRaw payout:rO   zscale:rP   zoversubscribed:rR   )__doc__	itertoolsr   rV   r   r	   r   r%   rD   r`   rm   rp   __name__samplerb   ir2   rY   r   print)cs   0r   <module>r{      s  (   5
$B *.6 0f*" z F1X1QC$E!QKHKqvq&'2KHAU$
 	  D&$'D	*d9o&	/4	?+	-l+XtG}
T"235%  Is   C