diff --git a/PLAN.md b/PLAN.md index 3dc41a9..5a9493b 100644 --- a/PLAN.md +++ b/PLAN.md @@ -416,12 +416,12 @@ Integrated into the fight viewer: - [x] API docs page ### Phase 4: Go Live -- [ ] Docker containerization +- [x] Docker containerization - [ ] Anonymous VPS deployment - [ ] Tor hidden service (optional) -- [ ] Rate limiting + abuse prevention -- [ ] Example bot implementations (at least 2) -- [ ] "How to Build a Fighter" tutorial +- [x] Rate limiting + abuse prevention +- [x] Example bot implementations (at least 2) +- [x] "How to Build a Fighter" tutorial ### Phase 5: Betting (Future) - [ ] Cashu mint integration diff --git a/examples/python-bot/bot.py b/examples/python-bot/bot.py new file mode 100644 index 0000000..93dae59 --- /dev/null +++ b/examples/python-bot/bot.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""Minimal BOTFIGHTS webhook bot in Python. Zero dependencies beyond stdlib. + +Usage: + python bot.py # listens on port 3000 + PORT=8080 python bot.py # custom port + +Register at /register with webhook URL pointing to this server. +""" + +import json +import os +import re +from http.server import HTTPServer, BaseHTTPRequestHandler + +PORT = int(os.environ.get("PORT", 3000)) + +# Simple knowledge base for factual challenges +FACTS = { + "capital of france": "Paris", + "capital of australia": "Canberra", + "capital of japan": "Tokyo", + "largest planet": "Jupiter", + "speed of light": "299792458", + "pi to 5 digits": "3.14159", + "h2o": "water", +} + + +def solve_math(prompt: str) -> str | None: + """Try to extract and evaluate a math expression.""" + patterns = [ + r"what is (.+?)\??$", + r"calculate (.+?)\??$", + r"(\d[\d\s\+\-\*\/\.\(\)]+\d)", + ] + for pat in patterns: + m = re.search(pat, prompt, re.IGNORECASE) + if m: + expr = m.group(1).strip() + expr = expr.replace("^", "**") + if re.match(r"^[\d\s\+\-\*\/\.\(\)]+$", expr): + try: + result = eval(expr) # safe: only digits and operators + if isinstance(result, float) and result == int(result): + return str(int(result)) + return str(result) + except Exception: + pass + return None + + +def handle_challenge(data: dict) -> dict: + """Process a fight challenge and return an answer.""" + ctype = data.get("type", "") + challenge = data.get("challenge", "") + opponent = data.get("opponent", {}).get("name", "opponent") + lower = challenge.lower() + + # Webhook test + if ctype == "webhook_test": + return {"answer": "pong"} + + # Math + if ctype == "math_blitz": + result = solve_math(challenge) + if result: + return {"answer": result, "trash_talk": "Math is easy."} + + # True/false + if ctype == "hallucination_check": + # Conservative: default to false for tricky claims + if any(w in lower for w in ["visible from space", "invented the internet"]): + return {"answer": "false", "trash_talk": "Nice try."} + return {"answer": "true"} + + # Roast battle + if ctype == "roast_battle": + return { + "answer": f"{opponent} is the kind of bot that fails a CAPTCHA on purpose.", + "trash_talk": "Too easy.", + } + + # Creative writing + if ctype in ("creative_writing", "meme_war", "wrestling_match"): + return { + "answer": f"In the arena of ideas, {opponent} brought a spoon to a sword fight. " + "The crowd gasps, not in awe, but in pity.", + "trash_talk": "Poetry in motion.", + } + + # Code golf + if ctype == "code_golf": + return {"answer": "print(42)", "trash_talk": "Minimalism."} + + # Factual lookup + for key, val in FACTS.items(): + if key in lower: + return {"answer": val} + + # Default: try math, then echo back a short response + math_result = solve_math(challenge) + if math_result: + return {"answer": math_result} + + # Last resort: pick the shortest plausible answer from the prompt + return {"answer": challenge.split("?")[0].split(".")[-1].strip()[:100]} + + +class BotHandler(BaseHTTPRequestHandler): + def do_POST(self): + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) + try: + data = json.loads(body) + response = handle_challenge(data) + except Exception: + response = {"answer": "error"} + + payload = json.dumps(response).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def log_message(self, fmt, *args): + print(f"[bot] {args[0]}") + + +if __name__ == "__main__": + server = HTTPServer(("0.0.0.0", PORT), BotHandler) + print(f"BOTFIGHTS Python bot listening on port {PORT}") + server.serve_forever()