dsh-eval-guard 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -13
- package/assets/commit0/instance.json +30 -0
- package/assets/commit0/workspace.zip +0 -0
- package/assets/featurebench/hidden/test_regression.py +690 -0
- package/assets/featurebench/instance.json +16 -0
- package/assets/featurebench/workspace.zip +0 -0
- package/assets/swe-bench-pro/hidden/test.patch +36 -0
- package/assets/swe-bench-pro/instance.json +10 -0
- package/assets/swe-bench-pro/workspace.zip +0 -0
- package/eval/README.md +18 -0
- package/eval/arena/arena/README.md +21 -0
- package/eval/arena/arena/bt.py +104 -0
- package/eval/arena/arena/clock.py +15 -0
- package/eval/arena/arena/core.py +265 -0
- package/eval/arena/arena/run.cmd +24 -0
- package/eval/arena/arena/run.sh +11 -0
- package/eval/arena/arena/schedule.py +79 -0
- package/eval/arena/arena/tournament.py +183 -0
- package/eval/arena/bomberman/bots/README.md +16 -0
- package/eval/arena/bomberman/bots/deepseek-v4.1-flash.cpp +447 -0
- package/eval/arena/bomberman/bots/doubao-seed-2.1.cpp +542 -0
- package/eval/arena/bomberman/bots/glm-5.3.cpp +406 -0
- package/eval/arena/bomberman/bots/gpt-6-astra.cpp +768 -0
- package/eval/arena/bomberman/bots/gpt-6-sol.cpp +444 -0
- package/eval/arena/bomberman/bots/kimi-k3.cpp +432 -0
- package/eval/arena/bomberman/bots/mimo-v2.6-pro.cpp +529 -0
- package/eval/arena/bomberman/bots/opus-5.5.cpp +548 -0
- package/eval/arena/bomberman/bots/qwen-3.8-max.cpp +836 -0
- package/eval/arena/bomberman/tournament/README.md +11 -0
- package/eval/arena/bomberman/tournament/bt.py +56 -0
- package/eval/arena/bomberman/tournament/evaluate.py +199 -0
- package/eval/arena/bomberman/tournament/run.py +79 -0
- package/eval/arena/bomberman/tournament/schedule-2to10.json.gz +0 -0
- package/eval/arena/bomberman/workspace/MANIFEST.json +13 -0
- package/eval/arena/bomberman/workspace/README.md +29 -0
- package/eval/arena/bomberman/workspace/RULES.md +100 -0
- package/eval/arena/bomberman/workspace/baseline.cpp +240 -0
- package/eval/arena/bomberman/workspace/clock.py +25 -0
- package/eval/arena/bomberman/workspace/engine/arena.py +198 -0
- package/eval/arena/bomberman/workspace/engine/build.py +35 -0
- package/eval/arena/bomberman/workspace/engine/engine.py +256 -0
- package/eval/arena/bomberman/workspace/engine/verify_replay.py +24 -0
- package/eval/arena/bomberman/workspace/run.cmd +28 -0
- package/eval/arena/bomberman/workspace/run.sh +16 -0
- package/eval/arena/bomberman/workspace/viewer/index.html +42 -0
- package/eval/arena/ctf/bots/README.md +18 -0
- package/eval/arena/ctf/bots/deepseek-v4.1-flash.cpp +601 -0
- package/eval/arena/ctf/bots/doubao-seed-2.1-turbo.cpp +320 -0
- package/eval/arena/ctf/bots/fable-5.1.cpp +634 -0
- package/eval/arena/ctf/bots/glm-5.3.cpp +689 -0
- package/eval/arena/ctf/bots/gpt-6-astra.cpp +657 -0
- package/eval/arena/ctf/bots/gpt-6.1-sol.cpp +725 -0
- package/eval/arena/ctf/bots/kimi-k3.cpp +667 -0
- package/eval/arena/ctf/bots/mimo-v2.6-pro.cpp +533 -0
- package/eval/arena/ctf/bots/opus-5.5.cpp +645 -0
- package/eval/arena/ctf/bots/qwen-3.8-max.cpp +726 -0
- package/eval/arena/ctf/bots/sonnet-5.5.cpp +569 -0
- package/eval/arena/ctf/tournament/README.md +15 -0
- package/eval/arena/ctf/tournament/formal-2to8.json +31 -0
- package/eval/arena/ctf/tournament/formal-9to15.json +31 -0
- package/eval/arena/ctf/workspace/README.md +41 -0
- package/eval/arena/ctf/workspace/RULES.md +119 -0
- package/eval/arena/ctf/workspace/arena.py +186 -0
- package/eval/arena/ctf/workspace/baseline.cpp +257 -0
- package/eval/arena/ctf/workspace/clock.py +15 -0
- package/eval/arena/ctf/workspace/core.py +265 -0
- package/eval/arena/ctf/workspace/engine.py +389 -0
- package/eval/arena/ctf/workspace/run.cmd +24 -0
- package/eval/arena/ctf/workspace/run.sh +11 -0
- package/eval/arena/ctf/workspace/viewer.html +71 -0
- package/eval/arena/land/bots/README.md +17 -0
- package/eval/arena/land/bots/deepseek-4.1f@workbuddy#1/bot.cpp +608 -0
- package/eval/arena/land/bots/glm-5.3@workbuddy#1/bot.cpp +521 -0
- package/eval/arena/land/bots/gpt-6-astra@codex#1/bot.cpp +373 -0
- package/eval/arena/land/bots/gpt-6-sol@codex#1/bot.cpp +341 -0
- package/eval/arena/land/bots/kimi-k3@workbuddy#1/bot.cpp +492 -0
- package/eval/arena/land/bots/mimo-2.6-pro@zcode#1/bot.cpp +652 -0
- package/eval/arena/land/bots/mystery-model@zcode#1/bot.cpp +466 -0
- package/eval/arena/land/bots/opus-5.5@claude-code#1/bot.cpp +630 -0
- package/eval/arena/land/bots/sonnet-5@claude-code#1/bot.cpp +619 -0
- package/eval/arena/land/tournament/make_replay_page.py +17 -0
- package/eval/arena/land/tournament/rate.py +290 -0
- package/eval/arena/land/tournament/round_robin.py +172 -0
- package/eval/arena/land/tournament/sparring.cpp +356 -0
- package/eval/arena/land/workspace/RULES.md +114 -0
- package/eval/arena/land/workspace/TASK.md +46 -0
- package/eval/arena/land/workspace/anchors/.build/greedy_f2e6d2dea5ca +0 -0
- package/eval/arena/land/workspace/anchors/greedy.cpp +194 -0
- package/eval/arena/land/workspace/anchors/random.cpp +80 -0
- package/eval/arena/land/workspace/clock.py +64 -0
- package/eval/arena/land/workspace/engine/build.py +33 -0
- package/eval/arena/land/workspace/engine/game.py +273 -0
- package/eval/arena/land/workspace/engine/referee.py +308 -0
- package/eval/arena/land/workspace/starter/.build/bot_be32b45b4699 +0 -0
- package/eval/arena/land/workspace/starter/bot.cpp +82 -0
- package/eval/arena/land/workspace/viewer/viewer.html +210 -0
- package/eval/arena/tank/bots/README.md +24 -0
- package/eval/arena/tank/bots/half1/deepseek-v4.1-flash.cpp +367 -0
- package/eval/arena/tank/bots/half1/doubao-seed-evolving.cpp +402 -0
- package/eval/arena/tank/bots/half1/gemini-4-argon.cpp +1132 -0
- package/eval/arena/tank/bots/half1/glm-5.3.cpp +551 -0
- package/eval/arena/tank/bots/half1/gpt-6-astra.cpp +233 -0
- package/eval/arena/tank/bots/half1/gpt-6.1-sol-ultrafast.cpp +350 -0
- package/eval/arena/tank/bots/half1/gpt-6.1-sol.cpp +78 -0
- package/eval/arena/tank/bots/half1/grok-4.7.cpp +1099 -0
- package/eval/arena/tank/bots/half1/haiku-5.5.cpp +210 -0
- package/eval/arena/tank/bots/half1/kimi-k3.cpp +353 -0
- package/eval/arena/tank/bots/half1/mimo-v2.6-pro.cpp +336 -0
- package/eval/arena/tank/bots/half1/muse-wonder-full.cpp +560 -0
- package/eval/arena/tank/bots/half1/opus-5.5.cpp +435 -0
- package/eval/arena/tank/bots/half1/qwen-3.8-max.cpp +537 -0
- package/eval/arena/tank/bots/half2/deepseek-v4.1-flash.cpp +425 -0
- package/eval/arena/tank/bots/half2/doubao-seed-evolving.cpp +308 -0
- package/eval/arena/tank/bots/half2/gemini-4-argon.cpp +1390 -0
- package/eval/arena/tank/bots/half2/glm-5.3.cpp +651 -0
- package/eval/arena/tank/bots/half2/gpt-6-astra.cpp +241 -0
- package/eval/arena/tank/bots/half2/gpt-6.1-sol-ultrafast.cpp +356 -0
- package/eval/arena/tank/bots/half2/gpt-6.1-sol.cpp +87 -0
- package/eval/arena/tank/bots/half2/grok-4.7.cpp +1205 -0
- package/eval/arena/tank/bots/half2/haiku-5.5.cpp +210 -0
- package/eval/arena/tank/bots/half2/kimi-k3.cpp +487 -0
- package/eval/arena/tank/bots/half2/mimo-v2.6-pro.cpp +336 -0
- package/eval/arena/tank/bots/half2/muse-wonder-full.cpp +560 -0
- package/eval/arena/tank/bots/half2/opus-5.5.cpp +476 -0
- package/eval/arena/tank/bots/half2/qwen-3.8-max.cpp +392 -0
- package/eval/arena/tank/tournament/README.md +24 -0
- package/eval/arena/tank/tournament/bt.py +218 -0
- package/eval/arena/tank/tournament/final.json +202 -0
- package/eval/arena/tank/tournament/halftime.json +146 -0
- package/eval/arena/tank/tournament/progress.json +911 -0
- package/eval/arena/tank/tournament/schedule.py +100 -0
- package/eval/arena/tank/tournament/sparring.cpp +928 -0
- package/eval/arena/tank/tournament/tournament.py +1426 -0
- package/eval/arena/tank/workspace/README.md +169 -0
- package/eval/arena/tank/workspace/RULES.md +443 -0
- package/eval/arena/tank/workspace/arena.py +862 -0
- package/eval/arena/tank/workspace/baseline.cpp +259 -0
- package/eval/arena/tank/workspace/clock.py +103 -0
- package/eval/arena/tank/workspace/core.py +712 -0
- package/eval/arena/tank/workspace/engine.py +573 -0
- package/eval/arena/tank/workspace/history/.lock +0 -0
- package/eval/arena/tank/workspace/history/20261017-193610-08715287f91f-bot.cpp +109 -0
- package/eval/arena/tank/workspace/history/20261017-193619-f00b2d5635fa-gpt-6-astra.cpp +241 -0
- package/eval/arena/tank/workspace/history/20261017-193643-63319e0a2b1a-gpt-6_1-sol-ultrafast.cpp +356 -0
- package/eval/arena/tank/workspace/history/20261017-193647-41c1341d0a8a-gemini-4-argon.cpp +1390 -0
- package/eval/arena/tank/workspace/history/20261017-193652-e34c0f45539a-gpt-6_1-sol.cpp +87 -0
- package/eval/arena/tank/workspace/history/20261017-193654-c026348ce96e-opus-5_5.cpp +476 -0
- package/eval/arena/tank/workspace/history/20261017-193655-4a21366e94d1-muse-wonder-full.cpp +560 -0
- package/eval/arena/tank/workspace/history/20261017-193657-dbc4e9969c52-mimo-v2_6-pro.cpp +336 -0
- package/eval/arena/tank/workspace/history/20261017-193703-723cd036eb31-grok-4_7.cpp +1205 -0
- package/eval/arena/tank/workspace/history/20261017-193707-1ebbb227be68-deepseek-v4_1-flash.cpp +425 -0
- package/eval/arena/tank/workspace/history/20261017-193709-aeb0e7202ad5-doubao-seed-evolving.cpp +308 -0
- package/eval/arena/tank/workspace/history/20261017-193710-b21ebeb9bc76-kimi-k3.cpp +487 -0
- package/eval/arena/tank/workspace/history/20261017-193712-54cd86531905-haiku-5_5.cpp +210 -0
- package/eval/arena/tank/workspace/history/20261017-193714-efdb8e35c0bf-qwen-3_8-max.cpp +392 -0
- package/eval/arena/tank/workspace/history/20261017-193715-681423c23fdb-glm-5_3.cpp +651 -0
- package/eval/arena/tank/workspace/history/20261017-193717-1a8b3c75181d-sparring.cpp +928 -0
- package/eval/arena/tank/workspace/history/20261017-193720-1e0803a541e8-baseline.cpp +259 -0
- package/eval/arena/tank/workspace/history/log.jsonl +103 -0
- package/eval/arena/tank/workspace/judge/cycles.hpp +53 -0
- package/eval/arena/tank/workspace/judge/fast.cpp +337 -0
- package/eval/arena/tank/workspace/judge/referee.cpp +125 -0
- package/eval/arena/tank/workspace/judge/speed.cpp +99 -0
- package/eval/arena/tank/workspace/run.cmd +31 -0
- package/eval/arena/tank/workspace/run.sh +23 -0
- package/eval/arena/tank/workspace/tank.hpp +1762 -0
- package/eval/arena/tank/workspace/tank_main.cpp +215 -0
- package/eval/arena/tank/workspace/viewer.html +1150 -0
- package/eval/arena/warehouse/workspace/SPEC.md +130 -0
- package/eval/arena/warehouse/workspace/TASK.md +71 -0
- package/eval/arena/warehouse/workspace/baseline/common.h +88 -0
- package/eval/arena/warehouse/workspace/baseline/main.cpp +27 -0
- package/eval/arena/warehouse/workspace/baseline/old_stuff.cpp +87 -0
- package/eval/arena/warehouse/workspace/baseline/sim.cpp +427 -0
- package/eval/arena/warehouse/workspace/baseline/util.cpp +220 -0
- package/eval/arena/warehouse/workspace/clock.py +42 -0
- package/eval/arena/warehouse/workspace/src/common.h +88 -0
- package/eval/arena/warehouse/workspace/src/main.cpp +27 -0
- package/eval/arena/warehouse/workspace/src/old_stuff.cpp +87 -0
- package/eval/arena/warehouse/workspace/src/sim.cpp +427 -0
- package/eval/arena/warehouse/workspace/src/util.cpp +220 -0
- package/eval/arena/warehouse/workspace/tools/bench.py +119 -0
- package/eval/arena/warehouse/workspace/tools/check_input.py +161 -0
- package/eval/arena/warehouse/workspace/tools/gen.py +132 -0
- package/eval/arena/warehouse/workspace/tools/measure.py +107 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.in +1420 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.out +9162 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.in +1465 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.out +3439 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_flood.in +2757 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_flood.out +3126 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_small.in +731 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_small.out +2578 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_wide.in +528 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_wide.out +2026 -0
- package/eval/bomberman.py +20 -0
- package/eval/commit0.py +21 -0
- package/eval/common.py +61 -0
- package/eval/ctf.py +20 -0
- package/eval/featurebench.py +25 -0
- package/eval/land.py +20 -0
- package/eval/legacy/bomberman-prices.json +4 -0
- package/eval/legacy/bomberman.py +161 -0
- package/eval/legacy/commit0.py +91 -0
- package/eval/legacy/ctf-prices.json +4 -0
- package/eval/legacy/ctf.py +178 -0
- package/eval/legacy/featurebench.py +103 -0
- package/eval/legacy/land-prices.json +4 -0
- package/eval/legacy/land.py +177 -0
- package/eval/legacy/swe_bench_pro.py +116 -0
- package/eval/legacy/tank-prices.json +3 -0
- package/eval/legacy/tank.py +408 -0
- package/eval/legacy/warehouse-prices.json +4 -0
- package/eval/legacy/warehouse.py +365 -0
- package/eval/swe-bench-pro.py +25 -0
- package/eval/tank.py +20 -0
- package/eval/warehouse.py +20 -0
- package/lib/client/index.js +376 -63
- package/lib/client/index.js.map +1 -1
- package/lib/index.js +684 -27
- package/package.json +2 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Bomb Arena v0.1. Pure deterministic public rules engine; Python 3.10+."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from collections import deque
|
|
4
|
+
from dataclasses import dataclass, asdict
|
|
5
|
+
import copy
|
|
6
|
+
import random
|
|
7
|
+
import math
|
|
8
|
+
from fractions import Fraction
|
|
9
|
+
|
|
10
|
+
VERSION = 'bomb-arena-0.5-public-state'
|
|
11
|
+
DIRS = {'U': (0,-1), 'D': (0,1), 'L': (-1,0), 'R': (1,0), 'S': (0,0)}
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Config:
|
|
15
|
+
size: int = 13
|
|
16
|
+
players: int = 2
|
|
17
|
+
max_turns: int = 300
|
|
18
|
+
shrink_start: int = 150
|
|
19
|
+
shrink_every: int = 20
|
|
20
|
+
fuse: int = 4
|
|
21
|
+
radius: int = 3
|
|
22
|
+
capacity: int = 2
|
|
23
|
+
flame_turns: int = 2
|
|
24
|
+
|
|
25
|
+
def config_for(n):
|
|
26
|
+
if not 2 <= n <= 10: raise ValueError('players must be between 2 and 10')
|
|
27
|
+
if n == 2: return Config()
|
|
28
|
+
if n == 4: return Config(17,4,450,250,25)
|
|
29
|
+
size = 2*math.ceil(math.sqrt(14*n))+1
|
|
30
|
+
start = 150+25*(n-2)
|
|
31
|
+
every = 20 if n <= 3 else 25
|
|
32
|
+
return Config(size,n,start+every*(size//2)+25,start,every)
|
|
33
|
+
|
|
34
|
+
def make_map(seed, cfg):
|
|
35
|
+
n = cfg.size
|
|
36
|
+
board = [['.' for _ in range(n)] for _ in range(n)]
|
|
37
|
+
for y in range(n):
|
|
38
|
+
for x in range(n):
|
|
39
|
+
if x in (0,n-1) or y in (0,n-1) or (x%2 == 0 and y%2 == 0):
|
|
40
|
+
board[y][x] = '#'
|
|
41
|
+
rng = random.Random(seed)
|
|
42
|
+
if cfg.players == 2: spawns = [(1,1),(n-2,n-2)]
|
|
43
|
+
elif cfg.players == 4: spawns = [(1,1),(n-2,1),(n-2,n-2),(1,n-2)]
|
|
44
|
+
else:
|
|
45
|
+
# Equally spaced along the perimeter's odd-coordinate cells. Corner/edge
|
|
46
|
+
# positions need seat rotation across matches; do not claim exact symmetry.
|
|
47
|
+
ring = ([(x,1) for x in range(1,n-2,2)] + [(n-2,y) for y in range(1,n-2,2)]
|
|
48
|
+
+ [(x,n-2) for x in range(n-2,1,-2)] + [(1,y) for y in range(n-2,1,-2)])
|
|
49
|
+
offset = rng.randrange(len(ring))
|
|
50
|
+
spawns = [ring[(offset+(i*len(ring)+cfg.players//2)//cfg.players)%len(ring)] for i in range(cfg.players)]
|
|
51
|
+
safe = {(x,y) for sx,sy in spawns for y in range(n) for x in range(n)
|
|
52
|
+
if abs(x-sx)+abs(y-sy) <= 2}
|
|
53
|
+
used = set()
|
|
54
|
+
for y in range(1,n-1):
|
|
55
|
+
for x in range(1,n-1):
|
|
56
|
+
if (x,y) in used or board[y][x] == '#': continue
|
|
57
|
+
orbit = {(x,y)}
|
|
58
|
+
if cfg.players in (2,4): orbit.add((n-1-x,n-1-y))
|
|
59
|
+
if cfg.players == 4: orbit |= {(n-1-y,x),(y,n-1-x)}
|
|
60
|
+
used |= orbit
|
|
61
|
+
crate = rng.random() < .42
|
|
62
|
+
if crate and not (orbit & safe):
|
|
63
|
+
for xx,yy in orbit: board[yy][xx] = '+'
|
|
64
|
+
return board, spawns
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def resolve_moves(origins, targets, allow_swap=True):
|
|
68
|
+
"""Alive players only, terrain already applied. No player-order priority."""
|
|
69
|
+
if len(set(origins))!=len(origins):raise ValueError('exclusive mode requires unique starting cells')
|
|
70
|
+
result=list(targets)
|
|
71
|
+
counts={p:targets.count(p) for p in targets}
|
|
72
|
+
for i,p in enumerate(targets):
|
|
73
|
+
if counts[p]>1:result[i]=origins[i]
|
|
74
|
+
if not allow_swap:
|
|
75
|
+
for i in range(len(origins)):
|
|
76
|
+
for j in range(i):
|
|
77
|
+
if result[i]==origins[j] and result[j]==origins[i]:result[i],result[j]=origins[i],origins[j]
|
|
78
|
+
while True:
|
|
79
|
+
stopped={origins[i] for i in range(len(origins)) if result[i]==origins[i]}
|
|
80
|
+
updated=[origins[i] if result[i] in stopped else result[i] for i in range(len(origins))]
|
|
81
|
+
if updated==result:break
|
|
82
|
+
result=updated
|
|
83
|
+
assert len(set(result))==len(result)
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
class Game:
|
|
87
|
+
move_mode="swap"
|
|
88
|
+
def __init__(self, seed=0, cfg=None):
|
|
89
|
+
self.cfg = cfg or Config()
|
|
90
|
+
self.seed, self.turn, self.next_bomb = seed, 0, 0
|
|
91
|
+
self.board, spawns = make_map(seed, self.cfg)
|
|
92
|
+
self.players = [dict(x=x,y=y,alive=True,death_turn=None,reason='',bombs=0,crates=0)
|
|
93
|
+
for x,y in spawns]
|
|
94
|
+
self.bombs = [] # id, owner, x, y, explode_at (absolute zero-based action turn)
|
|
95
|
+
self.flames = {} # (x,y) -> last lethal action turn, inclusive
|
|
96
|
+
self.events = []
|
|
97
|
+
self.flame_sources = {}
|
|
98
|
+
self.kill_credit = [Fraction(0) for _ in self.players]
|
|
99
|
+
|
|
100
|
+
def clone(self): return copy.deepcopy(self)
|
|
101
|
+
def over(self):
|
|
102
|
+
return self.turn >= self.cfg.max_turns or sum(p['alive'] for p in self.players) <= 1
|
|
103
|
+
|
|
104
|
+
def _kill(self, i, reason):
|
|
105
|
+
p = self.players[i]
|
|
106
|
+
if p['alive']:
|
|
107
|
+
p.update(alive=False, death_turn=self.turn, reason=reason)
|
|
108
|
+
self.events.append(dict(type='death',player=i,reason=reason,turn=self.turn))
|
|
109
|
+
if reason=='blast':
|
|
110
|
+
owners=sorted(owner for owner,expiry in self.flame_sources.get((p['x'],p['y']),{}).items() if expiry>=self.turn and owner!=i)
|
|
111
|
+
if owners:
|
|
112
|
+
share=Fraction(1,len(owners))
|
|
113
|
+
for owner in owners:self.kill_credit[owner]+=share
|
|
114
|
+
self.events.append(dict(type='kill_credit',victim=i,owners=owners,share=float(share)))
|
|
115
|
+
|
|
116
|
+
def shrink_layer(self, t=None):
|
|
117
|
+
t = self.turn if t is None else t
|
|
118
|
+
return 0 if t < self.cfg.shrink_start else 1+(t-self.cfg.shrink_start)//self.cfg.shrink_every
|
|
119
|
+
|
|
120
|
+
def step(self, actions, forfeits=None):
|
|
121
|
+
if self.over(): raise RuntimeError('game already finished')
|
|
122
|
+
if len(actions) != self.cfg.players: raise ValueError('one action per player')
|
|
123
|
+
self.events = []
|
|
124
|
+
t, n = self.turn, self.cfg.size
|
|
125
|
+
# 1. Expire flames, simultaneous forfeits, then deterministic shrink.
|
|
126
|
+
self.flames = {k:v for k,v in self.flames.items() if v >= t}
|
|
127
|
+
for i in forfeits or []: self._kill(i, 'forfeit')
|
|
128
|
+
layer = self.shrink_layer()
|
|
129
|
+
if t >= self.cfg.shrink_start and (t-self.cfg.shrink_start)%self.cfg.shrink_every == 0:
|
|
130
|
+
for y in range(n):
|
|
131
|
+
for x in range(n):
|
|
132
|
+
if min(x,y,n-1-x,n-1-y) <= layer:
|
|
133
|
+
self.board[y][x] = '#'
|
|
134
|
+
self.flames.pop((x,y), None)
|
|
135
|
+
self.bombs = [b for b in self.bombs if self.board[b['y']][b['x']] != '#']
|
|
136
|
+
for i,p in enumerate(self.players):
|
|
137
|
+
if self.board[p['y']][p['x']] == '#': self._kill(i, 'shrink')
|
|
138
|
+
self.events.append(dict(type='shrink',layer=layer))
|
|
139
|
+
# 2. Place at the pre-move position. Co-located simultaneous requests:
|
|
140
|
+
# exactly one requester succeeds; >=2 requesters all fail (no seat priority).
|
|
141
|
+
occupied = {(b['x'],b['y']) for b in self.bombs}
|
|
142
|
+
requests = {}
|
|
143
|
+
for i,(p,a) in enumerate(zip(self.players, actions)):
|
|
144
|
+
if not p['alive']: continue
|
|
145
|
+
d,place = a
|
|
146
|
+
if d not in DIRS or place not in (0,1): raise ValueError('invalid action')
|
|
147
|
+
cell = (p['x'],p['y'])
|
|
148
|
+
count = sum(b['owner']==i for b in self.bombs)
|
|
149
|
+
if place and cell not in occupied and count < self.cfg.capacity:
|
|
150
|
+
requests.setdefault(cell,[]).append(i)
|
|
151
|
+
for (x,y), ids in sorted(requests.items()):
|
|
152
|
+
if len(ids) != 1: continue
|
|
153
|
+
i = ids[0]
|
|
154
|
+
self.bombs.append(dict(id=self.next_bomb,owner=i,x=x,y=y,explode_at=t+self.cfg.fuse))
|
|
155
|
+
self.next_bomb += 1
|
|
156
|
+
self.players[i]['bombs'] += 1
|
|
157
|
+
self.events.append(dict(type='place',player=i,x=x,y=y))
|
|
158
|
+
occupied.add((x,y))
|
|
159
|
+
# Terrain and all destinations are computed before resolving player conflicts.
|
|
160
|
+
ids=[i for i,p in enumerate(self.players) if p['alive']]
|
|
161
|
+
origins=[(self.players[i]['x'],self.players[i]['y']) for i in ids];targets=[]
|
|
162
|
+
for i,old in zip(ids,origins):
|
|
163
|
+
dx,dy=DIRS[actions[i][0]];x,y=old[0]+dx,old[1]+dy
|
|
164
|
+
targets.append((x,y) if 0<=x<n and 0<=y<n and self.board[y][x]=='.' and ((x,y)==old or (x,y) not in occupied) else old)
|
|
165
|
+
destinations=targets if self.move_mode=='overlap' else resolve_moves(origins,targets,self.move_mode=='swap')
|
|
166
|
+
for i,old,want,dest in zip(ids,origins,targets,destinations):
|
|
167
|
+
self.players[i]['x'],self.players[i]['y']=dest
|
|
168
|
+
if want!=old and dest==old:self.events.append(dict(type='blocked',player=i,target=list(want)))
|
|
169
|
+
# 4. All explosions use ONE pre-explosion crate/bomb snapshot. A hit bomb
|
|
170
|
+
# triggers immediately AND blocks that ray. A hit crate also blocks it.
|
|
171
|
+
at = {(b['x'],b['y']):b for b in self.bombs}
|
|
172
|
+
pending = deque(b for b in self.bombs if b['explode_at']<=t or (b['x'],b['y']) in self.flames)
|
|
173
|
+
exploded, hit, destroyed = set(), set(), {}
|
|
174
|
+
hit_sources = {}
|
|
175
|
+
while pending:
|
|
176
|
+
b = pending.popleft()
|
|
177
|
+
if b['id'] in exploded: continue
|
|
178
|
+
exploded.add(b['id']); hit.add((b['x'],b['y']))
|
|
179
|
+
hit_sources.setdefault((b['x'],b['y']),set()).add(b['owner'])
|
|
180
|
+
self.events.append(dict(type='explode',player=b['owner'],x=b['x'],y=b['y']))
|
|
181
|
+
for dx,dy in list(DIRS.values())[:4]:
|
|
182
|
+
for r in range(1,self.cfg.radius+1):
|
|
183
|
+
x,y = b['x']+dx*r,b['y']+dy*r
|
|
184
|
+
if not (0<=x<n and 0<=y<n) or self.board[y][x]=='#': break
|
|
185
|
+
hit.add((x,y))
|
|
186
|
+
hit_sources.setdefault((x,y),set()).add(b['owner'])
|
|
187
|
+
if self.board[y][x]=='+':
|
|
188
|
+
destroyed.setdefault((x,y),set()).add(b['owner']); break
|
|
189
|
+
if (x,y) in at:
|
|
190
|
+
pending.append(at[x,y]); break
|
|
191
|
+
self.bombs = [b for b in self.bombs if b['id'] not in exploded]
|
|
192
|
+
for (x,y), owners in destroyed.items():
|
|
193
|
+
self.board[y][x]='.'
|
|
194
|
+
# A shared crate is credited to every hitting player, display only.
|
|
195
|
+
for i in owners: self.players[i]['crates'] += 1
|
|
196
|
+
for cell in hit: self.flames[cell] = max(self.flames.get(cell,-1),t+self.cfg.flame_turns-1)
|
|
197
|
+
for cell,owners in hit_sources.items():
|
|
198
|
+
provenance=self.flame_sources.setdefault(cell,{})
|
|
199
|
+
for owner in owners:provenance[owner]=max(provenance.get(owner,-1),t+self.cfg.flame_turns-1)
|
|
200
|
+
# 5. Kill on old or newly created flames. A dead owner's bombs remain.
|
|
201
|
+
for i,p in enumerate(self.players):
|
|
202
|
+
if (p['x'],p['y']) in self.flames: self._kill(i,'blast')
|
|
203
|
+
self.turn += 1
|
|
204
|
+
return self.snapshot()
|
|
205
|
+
|
|
206
|
+
def survival_ranks(self):
|
|
207
|
+
stamps = [self.cfg.max_turns+1 if p['alive'] else p['death_turn'] for p in self.players]
|
|
208
|
+
return [1+sum(v>s for v in stamps) for s in stamps]
|
|
209
|
+
|
|
210
|
+
def score_parts(self):
|
|
211
|
+
ranks = self.survival_ranks()
|
|
212
|
+
survival = [Fraction(self.cfg.players-r)-Fraction(ranks.count(r)-1,2) for r in ranks]
|
|
213
|
+
kills = [k/2 for k in self.kill_credit]
|
|
214
|
+
return survival, kills, [s+k for s,k in zip(survival,kills)]
|
|
215
|
+
|
|
216
|
+
def ranks(self):
|
|
217
|
+
points = self.score_parts()[2]
|
|
218
|
+
return [1+sum(other>p for other in points) for p in points]
|
|
219
|
+
|
|
220
|
+
def snapshot(self):
|
|
221
|
+
return dict(turn=self.turn,board=[''.join(r) for r in self.board],
|
|
222
|
+
players=copy.deepcopy(self.players),bombs=copy.deepcopy(self.bombs),
|
|
223
|
+
flames=[[x,y,e] for (x,y),e in sorted(self.flames.items())],events=copy.deepcopy(self.events))
|
|
224
|
+
|
|
225
|
+
def header(self, me):
|
|
226
|
+
c = self.cfg
|
|
227
|
+
return f'INIT {c.size} {c.players} {me} {c.max_turns} {c.fuse} {c.radius} {c.capacity} {c.flame_turns} {c.shrink_start} {c.shrink_every}\n'
|
|
228
|
+
|
|
229
|
+
def state_text(self):
|
|
230
|
+
lines = [f'TURN {self.turn}','BOARD']+[''.join(r) for r in self.board]
|
|
231
|
+
for i,p in enumerate(self.players):
|
|
232
|
+
lines.append(f"P {i} {p['x']} {p['y']} {int(p['alive'])}")
|
|
233
|
+
lines.append(f'BOMBS {len(self.bombs)}')
|
|
234
|
+
for b in self.bombs: lines.append(f"B {b['owner']} {b['x']} {b['y']} {b['explode_at']}")
|
|
235
|
+
lines.append(f'FLAMES {len(self.flames)}')
|
|
236
|
+
for (x,y),expiry in sorted(self.flames.items()): lines.append(f'F {x} {y} {expiry}')
|
|
237
|
+
lines.append(f'STATUS {len(self.players)}')
|
|
238
|
+
for i,p in enumerate(self.players):
|
|
239
|
+
k=self.kill_credit[i]
|
|
240
|
+
death=-1 if p['alive'] else p['death_turn']
|
|
241
|
+
lines.append(f'S {i} {death} {k.numerator} {k.denominator}')
|
|
242
|
+
sources=[(x,y,owner,expiry) for (x,y),owners in sorted(self.flame_sources.items())
|
|
243
|
+
if (x,y) in self.flames and self.flames[x,y]>=self.turn
|
|
244
|
+
for owner,expiry in sorted(owners.items()) if expiry>=self.turn]
|
|
245
|
+
lines.append(f'SOURCES {len(sources)}')
|
|
246
|
+
for x,y,owner,expiry in sources:lines.append(f'O {x} {y} {owner} {expiry}')
|
|
247
|
+
return '\n'.join(lines+['END',''])
|
|
248
|
+
|
|
249
|
+
def result(self):
|
|
250
|
+
survival, kills, points = self.score_parts()
|
|
251
|
+
return dict(kill_credit=list(map(float,self.kill_credit)),
|
|
252
|
+
survival_ranks=self.survival_ranks(),survival_points=list(map(float,survival)),
|
|
253
|
+
kill_points=list(map(float,kills)),points=list(map(float,points)),
|
|
254
|
+
points_exact=list(map(str,points)),kill_credit_exact=list(map(str,self.kill_credit)),
|
|
255
|
+
seed=self.seed,config=asdict(self.cfg),turns=self.turn,ranks=self.ranks(),
|
|
256
|
+
survivors=[i for i,p in enumerate(self.players) if p['alive']],players=copy.deepcopy(self.players))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Recompute a JSON replay from its seed/actions and compare every frame."""
|
|
2
|
+
import argparse,json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from engine import Game,Config,VERSION
|
|
5
|
+
|
|
6
|
+
def verify(path):
|
|
7
|
+
data=json.loads(Path(path).read_text(encoding='utf-8'))
|
|
8
|
+
if data['version']!=VERSION:raise ValueError('Replay requires its original engine version')
|
|
9
|
+
g=Game(data['result']['seed'],Config(**data['result']['config']))
|
|
10
|
+
assert g.snapshot()==data['frames'][0],'initial frame differs'
|
|
11
|
+
for expected in data['frames'][1:]:
|
|
12
|
+
assert expected['input_text']==g.state_text(),f'turn {g.turn}, input differs'
|
|
13
|
+
observed=g.step(expected['actions'],expected.get('forfeits',[]))
|
|
14
|
+
for key,value in observed.items():assert value==expected[key],f"turn {g.turn}, {key} differs"
|
|
15
|
+
assert g.over(),'replay ends before the game is complete'
|
|
16
|
+
assert g.turn==data['result']['turns'],'turn count differs'
|
|
17
|
+
assert g.ranks()==data['result']['ranks'],'ranks differ'
|
|
18
|
+
assert g.result()['kill_credit']==data['result']['kill_credit'],'kill credit differs'
|
|
19
|
+
for field in ('survival_ranks','survival_points','kill_points','points','points_exact','kill_credit_exact'):
|
|
20
|
+
assert g.result()[field]==data['result'][field],f'{field} differs'
|
|
21
|
+
return dict(path=str(path),frames=len(data['frames']),verified=True)
|
|
22
|
+
if __name__=='__main__':
|
|
23
|
+
ap=argparse.ArgumentParser();ap.add_argument('files',nargs='+');args=ap.parse_args()
|
|
24
|
+
for file in args.files:print(json.dumps(verify(file),ensure_ascii=False))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
setlocal
|
|
3
|
+
set "BOMB_RUNTIME="
|
|
4
|
+
if defined BOMB_PYTHON if exist "%BOMB_PYTHON%" set "BOMB_RUNTIME=%BOMB_PYTHON%"
|
|
5
|
+
if not defined BOMB_RUNTIME if exist "%~dp0.runtime\python.exe" set "BOMB_RUNTIME=%~dp0.runtime\python.exe"
|
|
6
|
+
if not defined BOMB_RUNTIME if exist "%USERPROFILE%\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe" set "BOMB_RUNTIME=%USERPROFILE%\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe"
|
|
7
|
+
if defined BOMB_RUNTIME goto found
|
|
8
|
+
python3 -c "import sys; sys.exit(0 if sys.version_info >= (3,10) else 1)" >nul 2>nul
|
|
9
|
+
if not errorlevel 1 set "BOMB_RUNTIME=python3"
|
|
10
|
+
if defined BOMB_RUNTIME goto found
|
|
11
|
+
python -c "import sys; sys.exit(0 if sys.version_info >= (3,10) else 1)" >nul 2>nul
|
|
12
|
+
if not errorlevel 1 set "BOMB_RUNTIME=python"
|
|
13
|
+
if not defined BOMB_RUNTIME (
|
|
14
|
+
echo Python 3.10+ not found. Set BOMB_PYTHON to the full executable path.
|
|
15
|
+
exit /b 1
|
|
16
|
+
)
|
|
17
|
+
:found
|
|
18
|
+
call "%BOMB_RUNTIME%" -c "import sys; sys.exit(0 if sys.version_info >= (3,10) else 1)"
|
|
19
|
+
if errorlevel 1 exit /b 1
|
|
20
|
+
pushd "%~dp0"
|
|
21
|
+
if "%~1"=="" (
|
|
22
|
+
call "%BOMB_RUNTIME%" -X utf8 clock.py
|
|
23
|
+
) else (
|
|
24
|
+
call "%BOMB_RUNTIME%" -X utf8 %*
|
|
25
|
+
)
|
|
26
|
+
set "BOMB_EXIT=%errorlevel%"
|
|
27
|
+
popd
|
|
28
|
+
exit /b %BOMB_EXIT%
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
cd "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
4
|
+
if [ -n "${BOMB_PYTHON:-}" ]; then
|
|
5
|
+
runtime=$BOMB_PYTHON
|
|
6
|
+
elif [ -x .runtime/bin/python3 ]; then
|
|
7
|
+
runtime=.runtime/bin/python3
|
|
8
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
9
|
+
runtime=$(command -v python3)
|
|
10
|
+
else
|
|
11
|
+
printf '%s\n' 'Python 3.10+ not found. Report the missing runtime to the organizer, or set BOMB_PYTHON to an existing Python path.' >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
"$runtime" -c 'import sys; sys.exit(0 if sys.version_info >= (3,10) else 1)'
|
|
15
|
+
if [ "$#" -eq 0 ]; then set -- clock.py; fi
|
|
16
|
+
exec "$runtime" -X utf8 "$@"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>炸弹人 · BOMB ARENA</title>
|
|
3
|
+
<style>
|
|
4
|
+
:root{color-scheme:dark;--muted:#93a3b9;--line:#293347;--accent:#c6ff61}*{box-sizing:border-box}body{margin:0;background:#0a101b;color:#eef2fb;font:15px 'Segoe UI','Microsoft YaHei',sans-serif}header{display:flex;justify-content:space-between;align-items:center;padding:22px 32px;border-bottom:1px solid var(--line)}h1{font-size:24px;margin:0;letter-spacing:.04em}h1 small{font-size:12px;color:var(--accent);margin-left:14px;letter-spacing:.16em}.meta{color:var(--muted);font-size:13px}.wrap{max-width:1320px;margin:auto;padding:24px 32px;display:grid;grid-template-columns:minmax(400px,1fr) 290px;gap:24px}.arena{background:#101927;border:1px solid var(--line);border-radius:16px;overflow:hidden}.bar{display:flex;justify-content:space-between;align-items:center;padding:17px 20px;border-bottom:1px solid var(--line);font-size:13px;color:var(--muted)}#counter{color:white;font-variant-numeric:tabular-nums}canvas{display:block;width:min(100%,740px);height:auto;margin:10px auto}aside{display:flex;flex-direction:column;gap:14px}.card{background:#121c2b;border:1px solid var(--line);border-radius:12px;padding:16px}.label{font-size:11px;text-transform:uppercase;letter-spacing:.13em;color:var(--muted);margin-bottom:12px}.player{display:grid;grid-template-columns:32px 1fr;gap:11px;align-items:center;padding:13px 0;border-bottom:1px solid #263144}.player:last-child{border-bottom:0;padding-bottom:0}.avatar{width:31px;height:31px;border-radius:9px;display:grid;place-items:center;font-weight:800;color:#0a101b}.name{font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:190px}.stats{font-size:11px;color:var(--muted);margin-top:4px}.dead{opacity:.42}#status{font-size:23px;font-weight:700;line-height:1.5}#statusSub{color:var(--muted);font-size:12px;line-height:1.8;margin-top:7px}.events{height:140px;overflow:auto;font-size:12px;line-height:1.85;color:#c1cada}.controls{display:flex;flex-wrap:wrap;align-items:center;gap:9px;padding:15px 20px;border-top:1px solid var(--line)}button,select,.file{font:inherit;font-size:12px;border:1px solid #3b485d;border-radius:7px;background:#1c293c;color:#edf3fc;padding:8px 12px;cursor:pointer}button:hover,.file:hover{border-color:var(--accent)}#play{background:var(--accent);border-color:var(--accent);color:#18210a;font-weight:700;min-width:68px}#seek{width:100%;accent-color:var(--accent);margin:0 0 5px}.file input{display:none}.hint{color:var(--muted);font-size:11px;line-height:1.9}.legend{display:flex;gap:14px;flex-wrap:wrap;font-size:11px;color:var(--muted);padding:0 20px 17px}.swatch{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px}footer{max-width:1320px;margin:auto;padding:0 32px 22px;color:#62758f;font-size:11px}.film header,.film .controls,.film footer,.film .hint{display:none}.film .wrap{padding:15px;max-width:1500px;grid-template-columns:minmax(400px,1fr) 270px}.film canvas{width:min(100%,850px)}@media(max-width:820px){.wrap{grid-template-columns:1fr;padding:14px}.film .wrap{grid-template-columns:1fr}aside{display:grid;grid-template-columns:1fr 1fr}header{padding:16px}.events{height:85px}}
|
|
5
|
+
.many #players{display:grid;grid-template-columns:1fr 1fr;gap:8px}.many .player{grid-template-columns:24px minmax(0,1fr);gap:6px;padding:8px 0}.many .avatar{width:24px;height:24px;font-size:12px;border-radius:6px}.many .name{font-size:11px;max-width:100%}.many .stats{font-size:9px;line-height:1.5}.many .wrap{grid-template-columns:minmax(400px,1fr) 340px}.many .events{height:110px}@media(max-width:820px){.many .wrap{grid-template-columns:1fr}.many aside{grid-template-columns:1fr}}
|
|
6
|
+
</style></head><body>
|
|
7
|
+
<header><h1>炸弹人 <small>BOMB ARENA / 01</small></h1><span class="meta" id="meta">公开规则 · 确定性回放</span></header>
|
|
8
|
+
<main class="wrap"><section class="arena"><div class="bar"><span id="mode">加载回放</span><span id="counter">TURN 000 / 000</span></div><canvas id="board" width="780" height="780" aria-label="炸弹人对战棋盘"></canvas><div class="legend"><span><i class="swatch" style="background:#b78a57"></i>可破坏箱子</span><span><i class="swatch" style="background:#ffcb58"></i>火焰</span><span>炸弹数字 = 距引爆行动轮数</span></div><div class="controls"><input id="seek" aria-label="回合进度" type="range" min="0" max="1" value="0"><button id="play">播放</button><button id="prev">上一步</button><button id="next">下一步</button><button id="reset">重播</button><select id="speed" aria-label="播放速度"><option value="2">2 回合/秒</option><option value="5" selected>5 回合/秒</option><option value="10">10 回合/秒</option><option value="20">20 回合/秒</option></select><button id="film">录制模式 F</button><button id="full">全屏</button><label class="file">导入 JSON<input id="file" type="file" accept=".json"></label></div></section>
|
|
9
|
+
<aside><div class="card"><div class="label">PLAYERS / 选手</div><div id="players"></div></div><div class="card"><div class="label">MATCH / 战况</div><div id="status">等待开局</div><div id="statusSub"></div></div><div class="card"><div class="label">EVENTS / 回合事件</div><div id="events" class="events"></div></div><div class="card"><div class="label">ACTIONS / 当前步动作</div><div id="actions" class="events"></div></div><div class="hint">空格:播放 / 暂停 ← →:逐步<br>F:切换录制模式 R:重播<br>单局分=生存分+0.5×击杀份额;同分并列。</div></aside></main><footer>无外部依赖 · 回放中不运行选手代码</footer>
|
|
10
|
+
<script>
|
|
11
|
+
let data=/*REPLAY_DATA*/null,index=0,playing=false,last=0,acc=0;
|
|
12
|
+
const $=s=>document.getElementById(s),ctx=$('board').getContext('2d'),colors=['#62d5ff','#ff7b8d','#c7a2ff','#c6ef66','#ffb65c','#69efd1','#f88dda','#eee2a8','#81a3ff','#f6f7fb'];
|
|
13
|
+
function rounded(x,y,w,h,r,fill){ctx.fillStyle=fill;ctx.beginPath();ctx.roundRect(x,y,w,h,r);ctx.fill()}
|
|
14
|
+
function load(d){if(!d||!Array.isArray(d.frames)||!d.frames.length||!d.result)throw Error('不是有效的回放');data=d;document.body.classList.toggle('many',d.result.config.players>4);index=0;playing=false;acc=0;$('seek').max=d.frames.length-1;$('meta').textContent=`${d.version} · SEED ${d.result.seed}`;const requested=Number(new URLSearchParams(location.hash.slice(1)).get('turn'));if(Number.isFinite(requested)&&requested>0){const found=d.frames.findIndex(f=>f.turn>=requested);index=found<0?d.frames.length-1:found;}draw();}
|
|
15
|
+
function draw(){ctx.clearRect(0,0,780,780);if(!data){ctx.fillStyle='#9aa9be';ctx.font='20px sans-serif';ctx.textAlign='center';ctx.fillText('导入回放 JSON 开始观看',390,390);return;}
|
|
16
|
+
const f=data.frames[index],cfg=data.result.config,n=cfg.size,c=720/n,ox=30,oy=30,flame=new Set(f.flames.map(p=>p[0]+','+p[1]));
|
|
17
|
+
for(let y=0;y<n;y++)for(let x=0;x<n;x++){let xx=ox+x*c,yy=oy+y*c,ch=f.board[y][x];rounded(xx+1,yy+1,c-2,c-2,3,ch==='#'?'#344157':(x+y)%2?'#172436':'#1a293d');if(ch==='#'){ctx.fillStyle='#425169';ctx.fillRect(xx+4,yy+4,c-8,3)}if(ch==='+'){rounded(xx+4,yy+4,c-8,c-8,3,'#987347');ctx.strokeStyle='#c39b66';ctx.lineWidth=2;ctx.strokeRect(xx+8,yy+8,c-16,c-16);ctx.beginPath();ctx.moveTo(xx+8,yy+8);ctx.lineTo(xx+c-8,yy+c-8);ctx.stroke();}if(flame.has(x+','+y)){rounded(xx+2,yy+2,c-4,c-4,5,'#d98432');rounded(xx+c*.21,yy+c*.12,c*.58,c*.76,6,'#ffcd61');rounded(xx+c*.36,yy+c*.29,c*.28,c*.42,4,'#fff5b5')}}
|
|
18
|
+
for(const b of f.bombs){let xx=ox+(b.x+.5)*c,yy=oy+(b.y+.5)*c;ctx.beginPath();ctx.arc(xx,yy,c*.34,0,Math.PI*2);ctx.fillStyle='#080d16';ctx.fill();ctx.strokeStyle=colors[b.owner];ctx.lineWidth=3;ctx.stroke();ctx.fillStyle='#fff';ctx.textAlign='center';ctx.textBaseline='middle';ctx.font=`bold ${c*.34}px sans-serif`;ctx.fillText(Math.max(0,b.explode_at-f.turn),xx,yy)}
|
|
19
|
+
f.players.forEach((p,i)=>{if(!p.alive)return;let xx=ox+(p.x+.5)*c,yy=oy+(p.y+.5)*c;let group=f.players.map((q,j)=>q.alive&&q.x===p.x&&q.y===p.y?j:-1).filter(j=>j>=0),shared=group.length>1;if(shared){let angle=2*Math.PI*group.indexOf(i)/group.length;xx+=Math.cos(angle)*c*.24;yy+=Math.sin(angle)*c*.24}let r=shared?c*.18:c*.32;ctx.shadowBlur=10;ctx.shadowColor=colors[i];rounded(xx-r,yy-r,r*2,r*2,Math.max(4,r*.4),colors[i]);ctx.shadowBlur=0;ctx.fillStyle='#142135';ctx.font=`bold ${c*(shared?.23:.34)}px sans-serif`;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(String.fromCharCode(65+i),xx,yy)});
|
|
20
|
+
$('actions').replaceChildren();
|
|
21
|
+
if(index===0){$('actions').textContent='初始状态,尚未执行动作';}
|
|
22
|
+
else {
|
|
23
|
+
const prev=data.frames[index-1], req=f.requested_actions||f.actions||[];
|
|
24
|
+
prev.players.forEach((p,i)=>{if(!p.alive)return;let line=document.createElement('div'),a=req[i],after=f.players[i];
|
|
25
|
+
line.textContent=`${String.fromCharCode(65+i)}:${a?({U:'上',D:'下',L:'左',R:'右',S:'等待'}[a[0]]+(a[1]?'+放弹':'')):'无有效响应'};(${p.x},${p.y}) → (${after.x},${after.y})`;
|
|
26
|
+
$('actions').append(line);});
|
|
27
|
+
for(const d of f.diagnostics||[]){let line=document.createElement('div');line.textContent=`${String.fromCharCode(65+d.player)}:${d.error}`;$('actions').append(line);}
|
|
28
|
+
}
|
|
29
|
+
$('counter').textContent=`TURN ${String(f.turn).padStart(3,'0')} / ${cfg.max_turns}`;$('mode').textContent=`${cfg.players===2?'双人对决':cfg.players+' 人混战'} · ${n} × ${n}`;$('seek').value=index;$('play').textContent=playing?'暂停':'播放';
|
|
30
|
+
$('players').replaceChildren();f.players.forEach((p,i)=>{let div=document.createElement('div');div.className='player'+(p.alive?'':' dead');let av=document.createElement('div');av.className='avatar';av.style.background=colors[i];av.textContent=String.fromCharCode(65+i);let info=document.createElement('div'),name=document.createElement('div'),stat=document.createElement('div');name.className='name';name.textContent=data.result.names[i].split(/[\\/]/).pop();stat.className='stats';let active=f.bombs.filter(b=>b.owner===i).length;stat.textContent=p.alive?`存活 · 炸弹 ${active}/${cfg.capacity} · 破箱 ${p.crates}`:`第 ${p.death_turn} 回合淘汰 · ${p.reason}`;if(index===data.frames.length-1&&data.result.kill_credit){stat.textContent+=` · 击杀份额 ${Number(data.result.kill_credit[i]).toFixed(2)}`;if(data.result.points_exact)stat.textContent+=` · 生存分 ${data.result.survival_points[i]}+击杀分 ${Number(data.result.kill_points[i]).toFixed(2)}=${data.result.points_exact[i]} 分 · 第 ${data.result.ranks[i]} 名`;}info.append(name,stat);div.append(av,info);$('players').append(div)});
|
|
31
|
+
let alive=f.players.filter(p=>p.alive).length,finished=index===data.frames.length-1;
|
|
32
|
+
$('status').textContent=finished?(data.result.ranks.filter(r=>r===1).length===1?'胜负已定':'并列结束'):`${alive} 人存活`;
|
|
33
|
+
if(finished){$('statusSub').textContent=data.result.ranks.map((r,i)=>`${String.fromCharCode(65+i)}:第 ${r} 名${data.result.points_exact?'('+data.result.points_exact[i]+'分)':''}`).join(' ')}else{let next=f.turn<cfg.shrink_start?cfg.shrink_start:cfg.shrink_start+Math.ceil((f.turn-cfg.shrink_start)/cfg.shrink_every)*cfg.shrink_every;$('statusSub').textContent=`下一次缩圈:第 ${next} 回合\n剩余 ${Math.max(0,next-f.turn)} 回合`}
|
|
34
|
+
if(data.result.valid_for_scoring===false){$('status').textContent='无效对局 · 不计成绩';$('statusSub').textContent='存在启动故障,名次仅供诊断。';}
|
|
35
|
+
$('events').replaceChildren();let events=[];for(let k=Math.max(0,index-5);k<=index;k++)for(let e of data.frames[k].events||[])events.push([Math.max(0,data.frames[k].turn-1),e]);for(let [t,e] of events.slice(-10).reverse()){let div=document.createElement('div'),who=e.player===undefined?'':String.fromCharCode(65+e.player);div.textContent=`${t} · `+({place:`${who} 放置炸弹`,explode:`${who} 炸弹引爆`,death:`${who} 淘汰(${e.reason})`,shrink:`边界收缩至第 ${e.layer} 层`,blocked:`${who} 移动受阻,目标 (${(e.target||[]).join(',')})`,kill_credit:`${String.fromCharCode(65+(e.victim||0))} 被击杀;${(e.owners||[]).map(i=>String.fromCharCode(65+i)).join('、')} 各记 ${Number(e.share||0).toFixed(2)}`}[e.type]||e.type);$('events').append(div)}
|
|
36
|
+
}
|
|
37
|
+
function toggle(){if(!data)return;if(index>=data.frames.length-1)index=0;playing=!playing;acc=0;draw()}
|
|
38
|
+
$('play').onclick=toggle;$('prev').onclick=()=>{playing=false;index=Math.max(0,index-1);draw()};$('next').onclick=()=>{playing=false;if(data)index=Math.min(data.frames.length-1,index+1);draw()};$('reset').onclick=()=>{index=0;acc=0;playing=true;draw()};$('seek').oninput=()=>{playing=false;index=+$('seek').value;draw()};$('film').onclick=()=>document.body.classList.toggle('film');$('full').onclick=()=>{if(document.fullscreenElement)document.exitFullscreen();else document.documentElement.requestFullscreen()};$('file').onchange=async e=>{try{load(JSON.parse(await e.target.files[0].text()))}catch(err){$('status').textContent='回放读取失败';$('statusSub').textContent=String(err)}};
|
|
39
|
+
document.addEventListener('keydown',e=>{if(e.target.tagName==='INPUT'||e.target.tagName==='SELECT')return;if(e.code==='Space'){e.preventDefault();toggle()}if(e.key==='ArrowLeft')$('prev').click();if(e.key==='ArrowRight')$('next').click();if(e.key.toLowerCase()==='f')$('film').click();if(e.key.toLowerCase()==='r')$('reset').click()});
|
|
40
|
+
function tick(now){if(playing&&data){acc+=Math.min(now-last,250);let step=1000/+$('speed').value;if(acc>=step){let delta=Math.floor(acc/step);index=Math.min(data.frames.length-1,index+delta);acc%=step;if(index===data.frames.length-1)playing=false;draw()}}last=now;requestAnimationFrame(tick)}
|
|
41
|
+
if(data)load(data);else draw();requestAnimationFrame(tick);
|
|
42
|
+
</script></body></html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 各模型的交卷代码
|
|
2
|
+
|
|
3
|
+
都是模型在比赛中独立写出的原样代码,主办方没有改动。基准在 `../workspace/baseline.cpp`。
|
|
4
|
+
|
|
5
|
+
| 文件 | 模型 | 工具 | 正式赛名次 | 实力值 |
|
|
6
|
+
|---|---|---|---:|---:|
|
|
7
|
+
| `gpt-6-astra.cpp` | GPT-6 Astra | Codex | 1 | 254 |
|
|
8
|
+
| `gpt-6.1-sol.cpp` | GPT-6.1 Sol | Codex | 2 | 252 |
|
|
9
|
+
| `fable-5.1.cpp` | Fable 5.1 | Claude Code | 3 | 213 |
|
|
10
|
+
| `sonnet-5.5.cpp` | Sonnet 5.5 | Claude Code | 4 | 203 |
|
|
11
|
+
| `opus-5.5.cpp` | Opus 5.5 | Claude Code | 5 | 196 |
|
|
12
|
+
| `deepseek-v4.1-flash.cpp` | DeepSeek V4.1 Flash | dsh | 6 | 129 |
|
|
13
|
+
| — | 基准 | — | — | 0 |
|
|
14
|
+
| `qwen-3.8-max.cpp` | Qwen 3.8 Max | Qwen Code | 7 | −21 |
|
|
15
|
+
| `mimo-v2.6-pro.cpp` | MiMo V2.6 Pro | MiMo Code | 8 | −26 |
|
|
16
|
+
| `kimi-k3.cpp` | Kimi K3 | Kimi Code | 9 | −35 |
|
|
17
|
+
| `glm-5.3.cpp` | GLM 5.3 | ZCode | 10 | −92 |
|
|
18
|
+
| `doubao-seed-2.1-turbo.cpp` | 豆包 Seed 2.1 Turbo | trae-agent | 11 | −307 |
|