New /patterns page with 9 analyses: chair win bias, bet rank correlations, hand type distributions, pot size buckets, streaks, hourly patterns, and recent-vs-overall comparison. Also adds a standalone analyze.py CLI script for terminal output.
167 lines
5.3 KiB
Python
Executable File
167 lines
5.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Standalone CLI script for Teen Patti pattern analysis.
|
|
|
|
Usage:
|
|
python analyze.py --host localhost --port 8123
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
|
|
|
|
def fmt_pct(n, total):
|
|
return f"{n/total*100:.1f}%" if total else "0.0%"
|
|
|
|
|
|
def print_table(headers, rows, col_widths=None):
|
|
"""Print a simple formatted table."""
|
|
if not col_widths:
|
|
col_widths = [max(len(str(h)), *(len(str(r[i])) for r in rows))
|
|
for i, h in enumerate(headers)]
|
|
# Header
|
|
hdr = " ".join(str(h).ljust(w) for h, w in zip(headers, col_widths))
|
|
print(hdr)
|
|
print("-" * len(hdr))
|
|
for row in rows:
|
|
print(" ".join(str(c).ljust(w) for c, w in zip(row, col_widths)))
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Teen Patti Pattern Analysis CLI")
|
|
parser.add_argument("--host", default="localhost", help="ClickHouse host")
|
|
parser.add_argument("--port", type=int, default=8123, help="ClickHouse HTTP port")
|
|
args = parser.parse_args()
|
|
|
|
# Set config before importing db
|
|
from app import config
|
|
config.CLICKHOUSE_HOST = args.host
|
|
config.CLICKHOUSE_PORT = args.port
|
|
|
|
from app import db
|
|
|
|
print(f"Connecting to ClickHouse at {args.host}:{args.port}...")
|
|
try:
|
|
data = db.get_pattern_analysis()
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
total = data["chair_bias"]["total_games"]
|
|
print(f"\n{'='*60}")
|
|
print(f" TEEN PATTI PATTERN ANALYSIS ({total:,} games)")
|
|
print(f"{'='*60}\n")
|
|
|
|
# 1. Chair Win Bias
|
|
print("1. CHAIR WIN BIAS (expected 33.3%)")
|
|
cb = data["chair_bias"]
|
|
rows = []
|
|
for ch in ("A", "B", "C"):
|
|
d = cb[ch]
|
|
diff = d["pct"] - 33.3
|
|
sign = "+" if diff >= 0 else ""
|
|
rows.append([ch, f"{d['wins']:,}", f"{d['pct']:.1f}%", f"{sign}{diff:.1f}%"])
|
|
print_table(["Chair", "Wins", "Win %", "vs Expected"], rows)
|
|
|
|
# 2. Bet Rank Analysis
|
|
print("\n2. BET RANK ANALYSIS")
|
|
br = data["bet_rank"]
|
|
br_total = br["high"] + br["mid"] + br["low"]
|
|
rows = []
|
|
for rank in ("high", "mid", "low"):
|
|
rows.append([rank.capitalize(), f"{br[rank]:,}", fmt_pct(br[rank], br_total)])
|
|
print_table(["Rank", "Wins", "Win %"], rows)
|
|
|
|
# 3. Per-Chair Bet Rank
|
|
print("\n3. PER-CHAIR: HIGHEST BET WIN RATE")
|
|
print(" When chair X has the highest bet, how often does X win?")
|
|
pcr = data["per_chair_rank"]
|
|
rows = []
|
|
for ch in ("A", "B", "C"):
|
|
d = pcr.get(ch, {})
|
|
rows.append([ch, f"{d.get('has_highest', 0):,}",
|
|
f"{d.get('wins', 0):,}", f"{d.get('win_pct', 0):.1f}%"])
|
|
print_table(["Chair", "Times Highest", "Wins", "Win %"], rows)
|
|
|
|
# 4. Hand Type Distribution by Chair
|
|
print("\n4. HAND TYPE DISTRIBUTION BY CHAIR")
|
|
htbc = data["hand_types_by_chair"]
|
|
hand_order = ["Trail", "Straight Flush", "Straight", "Flush", "Pair", "High Card"]
|
|
rows = []
|
|
for ht in hand_order:
|
|
a = htbc["A"].get(ht, 0)
|
|
b = htbc["B"].get(ht, 0)
|
|
c = htbc["C"].get(ht, 0)
|
|
if a + b + c == 0:
|
|
continue
|
|
rows.append([ht, f"{a:,}", f"{b:,}", f"{c:,}"])
|
|
print_table(["Hand Type", "Chair A", "Chair B", "Chair C"], rows)
|
|
|
|
# 5. Hand Type Win Rates
|
|
print("\n5. HAND TYPE WIN RATES")
|
|
htw = data["hand_type_wins"]
|
|
htw_total = sum(htw.values())
|
|
rows = []
|
|
for ht in hand_order:
|
|
v = htw.get(ht, 0)
|
|
if v == 0:
|
|
continue
|
|
rows.append([ht, f"{v:,}", fmt_pct(v, htw_total)])
|
|
print_table(["Hand Type", "Wins", "Win %"], rows)
|
|
|
|
# 6. Pot Size Buckets
|
|
print("\n6. WIN RATES BY POT SIZE")
|
|
pb = data["pot_buckets"]
|
|
ranges = pb.get("_ranges", {})
|
|
rows = []
|
|
for bucket in ("small", "medium", "large", "whale"):
|
|
d = pb.get(bucket)
|
|
if not d:
|
|
continue
|
|
t = d["total"] or 1
|
|
rows.append([
|
|
bucket.capitalize(), ranges.get(bucket, ""),
|
|
f"{d['total']:,}",
|
|
f"{d['A']/t*100:.1f}%", f"{d['B']/t*100:.1f}%", f"{d['C']/t*100:.1f}%",
|
|
])
|
|
print_table(["Bucket", "Range", "Games", "A %", "B %", "C %"], rows)
|
|
|
|
# 7. Streaks
|
|
print("\n7. STREAK ANALYSIS")
|
|
streaks = data["streaks"]
|
|
rows = []
|
|
for ch in ("A", "B", "C"):
|
|
s = streaks[ch]
|
|
rows.append([ch, str(s["max_streak"]), str(s["current_streak"])])
|
|
print_table(["Chair", "Max Streak", "Current Streak"], rows)
|
|
|
|
# 8. Hourly Patterns
|
|
print("\n8. HOURLY PATTERNS (win % by hour)")
|
|
hourly = data["hourly"]
|
|
hours = sorted(hourly.keys(), key=lambda h: int(h))
|
|
rows = []
|
|
for h in hours:
|
|
d = hourly[h]
|
|
t = d["total"] or 1
|
|
rows.append([
|
|
f"{int(h):02d}:00", str(d["total"]),
|
|
f"{d['A']/t*100:.1f}%", f"{d['B']/t*100:.1f}%", f"{d['C']/t*100:.1f}%",
|
|
])
|
|
print_table(["Hour", "Games", "A %", "B %", "C %"], rows)
|
|
|
|
# 9. Recent vs Overall
|
|
print("\n9. RECENT (LAST 100) vs ALL-TIME")
|
|
rva = data["recent_vs_all"]
|
|
for label, section in [("All-Time", rva["all"]), ("Last 100", rva["recent"])]:
|
|
t = section["total"] or 1
|
|
d = section["dist"]
|
|
parts = " | ".join(f"{ch}: {d[ch]:>4} ({d[ch]/t*100:.1f}%)" for ch in ("A", "B", "C"))
|
|
print(f" {label:>10} [{t:>5} games] {parts}")
|
|
|
|
print(f"\n{'='*60}")
|
|
print(" Done.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|