Something went wrong. Try again.
bayes for days
Something went wrong. Try again.
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207#!/usr/bin/env python3"""The floor: walk somewhere legal, more or less at random.
Deliberately stupid, and useful for exactly that. Every later bot is measuredagainst two ends of a range - Princess at the top and this at the bottom - and achange that cannot beat this one is not an improvement whatever its win rateagainst Princess happens to be that evening.
It does the least work that keeps its orders legal. It walks forward hex by hexuntil the next one would be off the map, under water, or occupied, with no ideawhere the enemy is. It shoots everything it has at whichever enemy it is mostlikely to hit, and never thinks about heat. It deploys wherever the first legalhex is, pointed roughly at the middle of the map.
None of that is good play, and none of it is borrowed. Every order it gives isone of these thirty lines, which is the property that makes a match log worthreading.
Seeded from SDS_SEED so a match can be repeated.
It never chooses which of its units acts: it answers about the host's `actor`and lets the host's own first-eligible pick stand. That is the floor for turnorder as well, and it is what keeps this bot a check that a host offering aneligible set still works with a bot that ignores one."""
import jsonimport osimport randomimport sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from hexes import distance, translated # noqa: E402
# Terrain a walking Mek should not stroll into. Not a rules implementation -# MegaMek decides legality and the host checks it - just enough to keep the# illegal-path count low so the metric measures decisions rather than syntax.IMPASSABLE_DEPTH = 1
class Bot: def __init__(self, rng: random.Random) -> None: self.rng = rng self.width = 0 self.height = 0 self.hexes: dict[tuple[int, int], dict] = {}
def on_board(self, message: dict) -> None: self.width = message["width"] self.height = message["height"] self.hexes = {(h["x"], h["y"]): h for h in message["hexes"]}
def passable(self, x: int, y: int, occupied: set[tuple[int, int]]) -> bool: if not (0 <= x < self.width and 0 <= y < self.height): return False if (x, y) in occupied: return False terrain = self.hexes.get((x, y), {}).get("terrain", {}) return terrain.get("water", 0) <= IMPASSABLE_DEPTH
def cost(self, fx: int, fy: int, tx: int, ty: int) -> int: """What entering a hex costs, near enough.
Counting one point per hex is what a first version does, and it is wrong often enough to matter: woods, rough and a climb all cost more, so the bot walks past its allowance and the server rejects the path. The first run of this harness spent 7 of 74 decisions that way.
Deliberately an approximation, not TW movement. The host checks legality and Princess covers the rejects, so an occasional over-run is a counted event rather than a broken match - and the number in the `illegal` column is the honest measure of how good this guess is. """ source = self.hexes.get((fx, fy), {}) target = self.hexes.get((tx, ty), {}) terrain = target.get("terrain", {}) points = 1 points += terrain.get("woods", 0) points += terrain.get("jungle", 0) points += 1 if terrain.get("rough", 0) else 0 points += 1 if terrain.get("rubble", 0) else 0 points += terrain.get("water", 0) points += abs(target.get("level", 0) - source.get("level", 0)) return points
def on_deployment(self, message: dict) -> dict: hexes = message.get("deployHexes", []) if not hexes: return {"kind": "pass"} spot = self.rng.choice(hexes) return { "kind": "deploy", "x": spot["x"], "y": spot["y"], "facing": self.facing_toward_centre(spot["x"], spot["y"]), }
def facing_toward_centre(self, x: int, y: int) -> int: """The facing whose next hex is closest to the middle of the board.
Six candidates, one distance each. Cheaper to write than an angle, and it cannot disagree with the hex geometry the moves use. """ cx, cy = self.width // 2, self.height // 2 best, best_distance = 0, None for facing in range(6): nx, ny = translated(x, y, facing) d = distance(nx, ny, cx, cy) if best_distance is None or d < best_distance: best, best_distance = facing, d return best
def on_firing(self, message: dict) -> dict: """Everything at one target: the one we are most likely to hit.
No heat management at all, so this bot will shut itself down on a hot machine. Left in deliberately - the floor should lose for reasons that are easy to name. """ shots = message.get("shots", []) if not shots: return {"kind": "fire", "attacks": []} best = {} for shot in shots: target = shot["target"] current = best.get(target) if current is None or shot["toHit"] < current: best[target] = shot["toHit"] target = min(best, key=lambda t: best[t]) attacks = [ {"weapon": s["weapon"], "target": target} for s in shots if s["target"] == target ] return {"kind": "fire", "attacks": attacks}
def on_observation(self, message: dict) -> dict: actor_id = message.get("actor") units = {u["id"]: u for u in message["units"]} actor = units.get(actor_id) if actor is None: return {"kind": "pass"}
occupied = {(u["x"], u["y"]) for u in message["units"] if u["id"] != actor_id}
steps: list[str] = [] facing = actor["facing"] # One turn at most, so the walk that follows goes somewhere rather than # spending its whole allowance rotating on the spot. turn = self.rng.choice([0, 0, 0, -1, 1]) if turn == -1: steps.append("TURN_LEFT") facing = (facing - 1) % 6 elif turn == 1: steps.append("TURN_RIGHT") facing = (facing + 1) % 6
# Walk, not run: running costs a to-hit penalty this bot has no way to # value, and the point of the floor is that it is honest about what it # does not know. budget = max(0, actor.get("walkMp", 0) - len(steps)) wanted = self.rng.randint(0, budget) x, y = actor["x"], actor["y"] spent = 0 for _ in range(wanted): nx, ny = translated(x, y, facing) if not self.passable(nx, ny, occupied): break step_cost = self.cost(x, y, nx, ny) if spent + step_cost > budget: break steps.append("FORWARDS") spent += step_cost x, y = nx, ny
return {"kind": "move", "steps": steps}
def main() -> None: rng = random.Random(int(os.environ.get("SDS_SEED", "0"))) bot = Bot(rng) for line in sys.stdin: line = line.strip() if not line: continue message = json.loads(line) kind = message.get("type") if kind == "board": bot.on_board(message) continue if kind != "observation": continue phase = message.get("phase", "") if phase == "DEPLOYMENT": reply = bot.on_deployment(message) elif phase == "FIRING": reply = bot.on_firing(message) else: reply = bot.on_observation(message) reply["type"] = "action" reply["seq"] = message["seq"] sys.stdout.write(json.dumps(reply) + "\n") sys.stdout.flush()
if __name__ == "__main__": main()