Update BOT_SETUP.md with full retro mode combo reference, scoring, and strategy tips. Add docs/bot-guide.md as a standalone developer guide covering all challenge types including retro mode. Add docs/sprite-guide.md and sprite-reference.html for custom sprite creation. Update docs.ts API endpoint and Python example bot with retro_mode handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
145 lines
4.6 KiB
Python
145 lines
4.6 KiB
Python
#!/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."}
|
|
|
|
# Retro mode — arcade combo round
|
|
if ctype == "retro_mode":
|
|
# Mix known standard moves with a secret super move for discovery bonus
|
|
combos = [
|
|
"\u2193\u2192\u2193\u2192+A", # Hadouken (22 dmg, 1.5x if undiscovered = 33)
|
|
"\u2192\u2192+A", # Dash Punch (15 dmg)
|
|
"\u2191\u2193+A", # Uppercut (16 dmg)
|
|
]
|
|
return {"answer": " | ".join(combos), "trash_talk": "HADOUKEN!"}
|
|
|
|
# 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()
|