feat: add CS Major autochess tournament
check / local-gate (push) Failing after 2m1s

This commit is contained in:
JWeBot Contributors
2026-10-15 15:17:32 +08:00
parent f07ae653c7
commit e713ff369a
26 changed files with 3901 additions and 20 deletions
+4 -4
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project]
name = "jwebot-kernel"
version = "0.6.3a0+1015"
version = "0.6.4a0+1015"
description = "Stable routing and extension authority for JWeBot"
requires-python = ">=3.9"
dependencies = [
"jwebot-config==0.6.3a0+1015",
"jwebot-sdk==0.6.3a0+1015",
"jwebot-official-extensions==0.6.3a0+1015",
"jwebot-config==0.6.4a0+1015",
"jwebot-sdk==0.6.4a0+1015",
"jwebot-official-extensions==0.6.4a0+1015",
"tomli>=2; python_version < '3.11'",
]
@@ -107,4 +107,4 @@ __all__ = [
"verify_installed_release_signature",
]
__version__ = "0.6.3a0+1015"
__version__ = "0.6.4a0+1015"
@@ -1070,6 +1070,71 @@ class Ledger:
ON group_lottery_entries(
group_fingerprint, round_number, created_at, person_key
);
CREATE TABLE IF NOT EXISTS autochess_runs (
group_fingerprint TEXT NOT NULL,
run_id TEXT NOT NULL,
season_number INTEGER NOT NULL,
state TEXT NOT NULL,
host_person_key TEXT NOT NULL,
state_json TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (group_fingerprint, run_id)
);
CREATE UNIQUE INDEX IF NOT EXISTS autochess_one_active_run
ON autochess_runs(group_fingerprint)
WHERE state IN ('registration', 'active');
CREATE TABLE IF NOT EXISTS autochess_action_replay (
group_fingerprint TEXT NOT NULL,
event_id TEXT NOT NULL,
run_id TEXT NOT NULL,
person_key TEXT NOT NULL,
command TEXT NOT NULL,
response_text TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (group_fingerprint, event_id)
);
CREATE TABLE IF NOT EXISTS autochess_story_logs (
group_fingerprint TEXT NOT NULL,
run_id TEXT NOT NULL,
story_id TEXT NOT NULL,
round_number INTEGER NOT NULL,
phase TEXT NOT NULL,
story_type TEXT NOT NULL,
story_text TEXT NOT NULL,
facts_json TEXT NOT NULL,
source_event_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (group_fingerprint, run_id, story_id),
FOREIGN KEY (group_fingerprint, run_id)
REFERENCES autochess_runs(group_fingerprint, run_id)
ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS autochess_battles (
group_fingerprint TEXT NOT NULL,
run_id TEXT NOT NULL,
battle_id TEXT NOT NULL,
round_number INTEGER NOT NULL,
phase TEXT NOT NULL,
battle_type TEXT NOT NULL,
player_a TEXT NOT NULL,
player_b TEXT NOT NULL,
result_json TEXT NOT NULL,
source_event_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (group_fingerprint, run_id, battle_id),
FOREIGN KEY (group_fingerprint, run_id)
REFERENCES autochess_runs(group_fingerprint, run_id)
ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS autochess_story_run_round
ON autochess_story_logs(
group_fingerprint, run_id, round_number, story_id
);
CREATE INDEX IF NOT EXISTS autochess_battle_run_round
ON autochess_battles(
group_fingerprint, run_id, round_number, battle_id
);
CREATE INDEX IF NOT EXISTS events_group_created
ON events(group_fingerprint, created_at);
CREATE INDEX IF NOT EXISTS outbox_due
@@ -3634,6 +3699,250 @@ class Ledger:
self.connection.execute("ROLLBACK")
raise
def _autochess_action_replay(
self,
group: str,
event_id: str,
person_key: str,
command: str,
) -> str | None:
previous = self.connection.execute(
"""
SELECT person_key, command, response_text
FROM autochess_action_replay
WHERE group_fingerprint = ? AND event_id = ?
""",
(group, event_id),
).fetchone()
if previous is None:
return None
if previous["person_key"] != person_key or previous["command"] != command:
raise LocalCommandError("同一事件不能变更自走棋玩家或指令")
return str(previous["response_text"])
def _load_autochess_run(
self,
group: str,
) -> tuple[sqlite3.Row | None, dict[str, Any] | None]:
row = self.connection.execute(
"""
SELECT * FROM autochess_runs
WHERE group_fingerprint = ?
ORDER BY
CASE WHEN state IN ('registration', 'active') THEN 0 ELSE 1 END,
season_number DESC
LIMIT 1
""",
(group,),
).fetchone()
if row is None:
return None, None
try:
state = json.loads(str(row["state_json"]))
except json.JSONDecodeError as error:
raise LocalCommandError("自走棋状态无效") from error
if not isinstance(state, dict):
raise LocalCommandError("自走棋状态无效")
return row, state
@staticmethod
def _validate_autochess_state(state: dict[str, Any]) -> None:
status = state.get("status")
if status not in {"registration", "active", "completed", "cancelled"}:
raise LocalCommandError("自走棋赛事状态无效")
if (
not isinstance(state.get("run_id"), str)
or not state["run_id"]
or type(state.get("season_number")) is not int
or int(state["season_number"]) < 1
or not isinstance(state.get("host_person_key"), str)
or not state["host_person_key"]
or not isinstance(state.get("players"), dict)
):
raise LocalCommandError("自走棋赛事结构无效")
if status in {"active", "completed"}:
players = state["players"].values()
if any(
not isinstance(player, dict)
or not isinstance(player.get("board"), list)
or len(player["board"]) != 5
for player in players
):
raise LocalCommandError("自走棋比赛必须保持五人首发")
def apply_autochess_action(
self,
group: str,
sender: str,
event_id: str,
command: str,
action: Callable[
[dict[str, Any] | None, dict[str, str], int],
dict[str, Any],
],
) -> str:
"""Commit autochess state, replay, stories and battles atomically."""
person = self._player_identity(group, sender)
person_key = str(person["person_key"])
with self.lock:
self.connection.execute("BEGIN IMMEDIATE")
try:
replay = self._autochess_action_replay(
group,
event_id,
person_key,
command,
)
if replay is not None:
self.connection.execute("COMMIT")
return replay
stored, state = self._load_autochess_run(group)
next_season = int(
self.connection.execute(
"""
SELECT COALESCE(MAX(season_number), 0) + 1
FROM autochess_runs
WHERE group_fingerprint = ?
""",
(group,),
).fetchone()[0]
)
result = action(state, person, next_season)
if not isinstance(result, dict):
raise LocalCommandError("自走棋动作结果无效")
response = result.get("response")
new_state = result.get("state")
stories = result.get("stories", [])
battles = result.get("battles", [])
if not isinstance(response, str) or not response.strip():
raise LocalCommandError("自走棋响应无效")
if not isinstance(stories, list) or not isinstance(battles, list):
raise LocalCommandError("自走棋归档无效")
timestamp = utc_now()
run_id = str(stored["run_id"]) if stored is not None else ""
if new_state is not None:
if not isinstance(new_state, dict):
raise LocalCommandError("自走棋状态无效")
self._validate_autochess_state(new_state)
run_id = str(new_state["run_id"])
season_number = int(new_state["season_number"])
existing = self.connection.execute(
"""
SELECT created_at FROM autochess_runs
WHERE group_fingerprint = ? AND run_id = ?
""",
(group, run_id),
).fetchone()
created_at = (
str(existing["created_at"])
if existing is not None
else timestamp
)
self.connection.execute(
"""
INSERT INTO autochess_runs (
group_fingerprint, run_id, season_number, state,
host_person_key, state_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(group_fingerprint, run_id) DO UPDATE SET
season_number = excluded.season_number,
state = excluded.state,
host_person_key = excluded.host_person_key,
state_json = excluded.state_json,
updated_at = excluded.updated_at
""",
(
group,
run_id,
season_number,
str(new_state["status"]),
str(new_state["host_person_key"]),
canonical_json(new_state),
created_at,
timestamp,
),
)
for item in stories:
if (
not isinstance(item, dict)
or not isinstance(item.get("story_id"), str)
or not isinstance(item.get("text"), str)
or not isinstance(item.get("facts", {}), dict)
):
raise LocalCommandError("自走棋剧情归档无效")
self.connection.execute(
"""
INSERT INTO autochess_story_logs (
group_fingerprint, run_id, story_id,
round_number, phase, story_type, story_text,
facts_json, source_event_id, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
group,
run_id,
str(item["story_id"]),
int(item.get("round_number", 0)),
str(item.get("phase", "")),
str(item.get("story_type", "")),
str(item["text"]),
canonical_json(item.get("facts", {})),
event_id,
timestamp,
),
)
for item in battles:
if (
not isinstance(item, dict)
or not isinstance(item.get("battle_id"), str)
or not isinstance(item.get("result"), dict)
):
raise LocalCommandError("自走棋战斗归档无效")
self.connection.execute(
"""
INSERT INTO autochess_battles (
group_fingerprint, run_id, battle_id,
round_number, phase, battle_type, player_a,
player_b, result_json, source_event_id, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
group,
run_id,
str(item["battle_id"]),
int(item.get("round_number", 0)),
str(item.get("phase", "")),
str(item.get("battle_type", "")),
str(item.get("player_a", "")),
str(item.get("player_b", "")),
canonical_json(item["result"]),
event_id,
timestamp,
),
)
self.connection.execute(
"""
INSERT INTO autochess_action_replay (
group_fingerprint, event_id, run_id, person_key,
command, response_text, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
group,
event_id,
run_id,
person_key,
command,
response,
timestamp,
),
)
self.connection.execute("COMMIT")
return response
except BaseException:
self.connection.execute("ROLLBACK")
raise
def status(self) -> dict[str, Any]:
with self.lock:
events = self.connection.execute("SELECT COUNT(*) FROM events").fetchone()[0]