This commit is contained in:
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "jwebot-kernel"
|
||||
version = "0.6.2a0+1015"
|
||||
version = "0.6.3a0+1015"
|
||||
description = "Stable routing and extension authority for JWeBot"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"jwebot-config==0.6.2a0+1015",
|
||||
"jwebot-sdk==0.6.2a0+1015",
|
||||
"jwebot-official-extensions==0.6.2a0+1015",
|
||||
"jwebot-config==0.6.3a0+1015",
|
||||
"jwebot-sdk==0.6.3a0+1015",
|
||||
"jwebot-official-extensions==0.6.3a0+1015",
|
||||
"tomli>=2; python_version < '3.11'",
|
||||
]
|
||||
|
||||
|
||||
@@ -107,4 +107,4 @@ __all__ = [
|
||||
"verify_installed_release_signature",
|
||||
]
|
||||
|
||||
__version__ = "0.6.2a0+1015"
|
||||
__version__ = "0.6.3a0+1015"
|
||||
|
||||
@@ -68,6 +68,11 @@ from jwebot_official_extensions.steam import (
|
||||
SteamClient,
|
||||
SteamUnavailable,
|
||||
)
|
||||
from jwebot_official_extensions.player_games import (
|
||||
append_game_record,
|
||||
next_game_index,
|
||||
normalize_player_account,
|
||||
)
|
||||
from jwebot_sdk import Event, PlanKind
|
||||
|
||||
|
||||
@@ -1026,6 +1031,45 @@ class Ledger:
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (group_fingerprint, event_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS group_lottery_rounds (
|
||||
group_fingerprint TEXT NOT NULL,
|
||||
round_number INTEGER NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
host_person_key TEXT NOT NULL,
|
||||
host_display_name TEXT NOT NULL,
|
||||
drawn_numbers_json TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (group_fingerprint, round_number)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS group_lottery_entries (
|
||||
group_fingerprint TEXT NOT NULL,
|
||||
round_number INTEGER NOT NULL,
|
||||
person_key TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
player_game_index INTEGER NOT NULL,
|
||||
numbers_json TEXT NOT NULL,
|
||||
stake INTEGER NOT NULL,
|
||||
matches INTEGER NOT NULL DEFAULT 0,
|
||||
payout INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (
|
||||
group_fingerprint, round_number, person_key
|
||||
),
|
||||
FOREIGN KEY (group_fingerprint, round_number)
|
||||
REFERENCES group_lottery_rounds (
|
||||
group_fingerprint, round_number
|
||||
)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS group_lottery_one_open
|
||||
ON group_lottery_rounds(group_fingerprint)
|
||||
WHERE state = 'open';
|
||||
CREATE INDEX IF NOT EXISTS group_lottery_entries_round
|
||||
ON group_lottery_entries(
|
||||
group_fingerprint, round_number, created_at, person_key
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS events_group_created
|
||||
ON events(group_fingerprint, created_at);
|
||||
CREATE INDEX IF NOT EXISTS outbox_due
|
||||
@@ -2983,49 +3027,606 @@ class Ledger:
|
||||
})
|
||||
return list(people.values())
|
||||
|
||||
def _player_identity(self, group: str, sender: str) -> dict[str, str]:
|
||||
if not group or not sender or sender == "unknown":
|
||||
raise LocalCommandError("无法确认本人身份,不能操作金币账户")
|
||||
person = self.resolve_person(group, sender)
|
||||
person["display_name"] = person_display_label(person["display_name"])
|
||||
return person
|
||||
|
||||
def _player_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 player_actions
|
||||
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_player_account(
|
||||
self,
|
||||
group: str,
|
||||
person_key: str,
|
||||
) -> dict[str, Any]:
|
||||
stored = self.connection.execute(
|
||||
"""
|
||||
SELECT state_json FROM player_accounts
|
||||
WHERE group_fingerprint = ? AND person_key = ?
|
||||
""",
|
||||
(group, person_key),
|
||||
).fetchone()
|
||||
if stored is None:
|
||||
return normalize_player_account({})
|
||||
try:
|
||||
account = json.loads(str(stored["state_json"]))
|
||||
except json.JSONDecodeError as error:
|
||||
raise LocalCommandError("金币账户状态无效") from error
|
||||
if not isinstance(account, dict):
|
||||
raise LocalCommandError("金币账户状态无效")
|
||||
return normalize_player_account(account)
|
||||
|
||||
def _save_player_account(
|
||||
self,
|
||||
group: str,
|
||||
person_key: str,
|
||||
account: dict[str, Any],
|
||||
timestamp: str,
|
||||
) -> None:
|
||||
normalize_player_account(account)
|
||||
if (
|
||||
type(account["balance"]) is not int
|
||||
or not 0 <= account["balance"] <= 1_000_000_000
|
||||
):
|
||||
raise LocalCommandError("金币结算超出允许范围")
|
||||
self.connection.execute(
|
||||
"""
|
||||
INSERT INTO player_accounts VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(group_fingerprint, person_key) DO UPDATE
|
||||
SET state_json = excluded.state_json,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(
|
||||
group,
|
||||
person_key,
|
||||
json.dumps(account, ensure_ascii=False),
|
||||
timestamp,
|
||||
),
|
||||
)
|
||||
|
||||
def _store_player_action(
|
||||
self,
|
||||
group: str,
|
||||
event_id: str,
|
||||
person_key: str,
|
||||
command: str,
|
||||
response: str,
|
||||
timestamp: str,
|
||||
) -> None:
|
||||
self.connection.execute(
|
||||
"INSERT INTO player_actions VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(group, event_id, person_key, command, response, timestamp),
|
||||
)
|
||||
|
||||
def _open_group_lottery(self, group: str) -> sqlite3.Row | None:
|
||||
return self.connection.execute(
|
||||
"""
|
||||
SELECT * FROM group_lottery_rounds
|
||||
WHERE group_fingerprint = ? AND state = 'open'
|
||||
LIMIT 1
|
||||
""",
|
||||
(group,),
|
||||
).fetchone()
|
||||
|
||||
def _group_lottery_entries(
|
||||
self,
|
||||
group: str,
|
||||
round_number: int,
|
||||
) -> list[dict[str, Any]]:
|
||||
rows = self.connection.execute(
|
||||
"""
|
||||
SELECT * FROM group_lottery_entries
|
||||
WHERE group_fingerprint = ? AND round_number = ?
|
||||
ORDER BY created_at, person_key
|
||||
""",
|
||||
(group, round_number),
|
||||
).fetchall()
|
||||
entries: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
entry = dict(row)
|
||||
try:
|
||||
numbers = json.loads(str(entry["numbers_json"]))
|
||||
except json.JSONDecodeError:
|
||||
numbers = []
|
||||
entry["numbers"] = [
|
||||
int(value)
|
||||
for value in numbers
|
||||
if type(value) is int and 0 <= value <= 9
|
||||
]
|
||||
entries.append(entry)
|
||||
return entries
|
||||
|
||||
def start_group_lottery(
|
||||
self,
|
||||
group: str,
|
||||
sender: str,
|
||||
event_id: str,
|
||||
command: str,
|
||||
render: Callable[[dict[str, Any]], str],
|
||||
) -> str:
|
||||
person = self._player_identity(group, sender)
|
||||
person_key = str(person["person_key"])
|
||||
with self.lock:
|
||||
self.connection.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
replay = self._player_action_replay(
|
||||
group, event_id, person_key, command
|
||||
)
|
||||
if replay is not None:
|
||||
self.connection.execute("COMMIT")
|
||||
return replay
|
||||
row = self._open_group_lottery(group)
|
||||
status = "already_open"
|
||||
if row is None:
|
||||
round_number = int(
|
||||
self.connection.execute(
|
||||
"""
|
||||
SELECT COALESCE(MAX(round_number), 0) + 1
|
||||
FROM group_lottery_rounds
|
||||
WHERE group_fingerprint = ?
|
||||
""",
|
||||
(group,),
|
||||
).fetchone()[0]
|
||||
)
|
||||
timestamp = utc_now()
|
||||
self.connection.execute(
|
||||
"""
|
||||
INSERT INTO group_lottery_rounds (
|
||||
group_fingerprint, round_number, state,
|
||||
host_person_key, host_display_name,
|
||||
drawn_numbers_json, created_at, updated_at
|
||||
) VALUES (?, ?, 'open', ?, ?, '[]', ?, ?)
|
||||
""",
|
||||
(
|
||||
group,
|
||||
round_number,
|
||||
person_key,
|
||||
person["display_name"],
|
||||
timestamp,
|
||||
timestamp,
|
||||
),
|
||||
)
|
||||
row = self._open_group_lottery(group)
|
||||
status = "started"
|
||||
if row is None:
|
||||
raise LocalCommandError("彩票创建失败")
|
||||
data = dict(row)
|
||||
data["status"] = status
|
||||
response = render(data)
|
||||
timestamp = utc_now()
|
||||
self._store_player_action(
|
||||
group, event_id, person_key, command, response, timestamp
|
||||
)
|
||||
self.connection.execute("COMMIT")
|
||||
return response
|
||||
except BaseException:
|
||||
self.connection.execute("ROLLBACK")
|
||||
raise
|
||||
|
||||
def group_lottery_status(
|
||||
self,
|
||||
group: str,
|
||||
sender: str,
|
||||
event_id: str,
|
||||
command: str,
|
||||
render: Callable[[dict[str, Any]], str],
|
||||
) -> str:
|
||||
person = self._player_identity(group, sender)
|
||||
person_key = str(person["person_key"])
|
||||
with self.lock:
|
||||
self.connection.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
replay = self._player_action_replay(
|
||||
group, event_id, person_key, command
|
||||
)
|
||||
if replay is not None:
|
||||
self.connection.execute("COMMIT")
|
||||
return replay
|
||||
row = self._open_group_lottery(group)
|
||||
if row is None:
|
||||
data: dict[str, Any] = {
|
||||
"status": "no_round",
|
||||
"entries": [],
|
||||
}
|
||||
else:
|
||||
data = dict(row)
|
||||
data["status"] = "open"
|
||||
data["entries"] = self._group_lottery_entries(
|
||||
group, int(row["round_number"])
|
||||
)
|
||||
response = render(data)
|
||||
timestamp = utc_now()
|
||||
self._store_player_action(
|
||||
group, event_id, person_key, command, response, timestamp
|
||||
)
|
||||
self.connection.execute("COMMIT")
|
||||
return response
|
||||
except BaseException:
|
||||
self.connection.execute("ROLLBACK")
|
||||
raise
|
||||
|
||||
def register_group_lottery(
|
||||
self,
|
||||
group: str,
|
||||
sender: str,
|
||||
event_id: str,
|
||||
command: str,
|
||||
numbers: tuple[int, int, int],
|
||||
stake: int,
|
||||
render: Callable[[dict[str, Any]], str],
|
||||
) -> str:
|
||||
person = self._player_identity(group, sender)
|
||||
person_key = str(person["person_key"])
|
||||
with self.lock:
|
||||
self.connection.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
replay = self._player_action_replay(
|
||||
group, event_id, person_key, command
|
||||
)
|
||||
if replay is not None:
|
||||
self.connection.execute("COMMIT")
|
||||
return replay
|
||||
row = self._open_group_lottery(group)
|
||||
if row is None:
|
||||
data: dict[str, Any] = {
|
||||
"status": "no_round",
|
||||
"display_name": person["display_name"],
|
||||
}
|
||||
else:
|
||||
round_number = int(row["round_number"])
|
||||
existing = self.connection.execute(
|
||||
"""
|
||||
SELECT * FROM group_lottery_entries
|
||||
WHERE group_fingerprint = ? AND round_number = ?
|
||||
AND person_key = ?
|
||||
""",
|
||||
(group, round_number, person_key),
|
||||
).fetchone()
|
||||
account = self._load_player_account(group, person_key)
|
||||
old_stake = int(existing["stake"]) if existing else 0
|
||||
delta = stake - old_stake
|
||||
if delta > int(account["balance"]):
|
||||
data = {
|
||||
"status": "insufficient",
|
||||
"display_name": person["display_name"],
|
||||
"balance": int(account["balance"]),
|
||||
}
|
||||
else:
|
||||
account["balance"] -= delta
|
||||
index = (
|
||||
int(existing["player_game_index"])
|
||||
if existing
|
||||
else next_game_index(account, "lottery")
|
||||
)
|
||||
timestamp = utc_now()
|
||||
self._save_player_account(
|
||||
group, person_key, account, timestamp
|
||||
)
|
||||
self.connection.execute(
|
||||
"""
|
||||
INSERT INTO group_lottery_entries (
|
||||
group_fingerprint, round_number, person_key,
|
||||
display_name, player_game_index, numbers_json,
|
||||
stake, matches, payout, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)
|
||||
ON CONFLICT (
|
||||
group_fingerprint, round_number, person_key
|
||||
) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
numbers_json = excluded.numbers_json,
|
||||
stake = excluded.stake,
|
||||
matches = 0,
|
||||
payout = 0,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(
|
||||
group,
|
||||
round_number,
|
||||
person_key,
|
||||
person["display_name"],
|
||||
index,
|
||||
canonical_json(list(numbers)),
|
||||
stake,
|
||||
timestamp,
|
||||
timestamp,
|
||||
),
|
||||
)
|
||||
data = {
|
||||
"status": "updated" if existing else "registered",
|
||||
"display_name": person["display_name"],
|
||||
"player_game_index": index,
|
||||
"round_number": round_number,
|
||||
"numbers": list(numbers),
|
||||
"stake": stake,
|
||||
"balance": int(account["balance"]),
|
||||
}
|
||||
response = render(data)
|
||||
timestamp = utc_now()
|
||||
self._store_player_action(
|
||||
group, event_id, person_key, command, response, timestamp
|
||||
)
|
||||
self.connection.execute("COMMIT")
|
||||
return response
|
||||
except BaseException:
|
||||
self.connection.execute("ROLLBACK")
|
||||
raise
|
||||
|
||||
def draw_group_lottery(
|
||||
self,
|
||||
group: str,
|
||||
sender: str,
|
||||
event_id: str,
|
||||
command: str,
|
||||
drawn_numbers: tuple[int, int, int],
|
||||
render: Callable[[dict[str, Any]], str],
|
||||
) -> str:
|
||||
person = self._player_identity(group, sender)
|
||||
person_key = str(person["person_key"])
|
||||
if (
|
||||
len(drawn_numbers) != 3
|
||||
or len(set(drawn_numbers)) != 3
|
||||
or any(type(value) is not int or not 0 <= value <= 9 for value in drawn_numbers)
|
||||
):
|
||||
raise LocalCommandError("彩票开奖号无效")
|
||||
with self.lock:
|
||||
self.connection.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
replay = self._player_action_replay(
|
||||
group, event_id, person_key, command
|
||||
)
|
||||
if replay is not None:
|
||||
self.connection.execute("COMMIT")
|
||||
return replay
|
||||
row = self._open_group_lottery(group)
|
||||
if row is None:
|
||||
data: dict[str, Any] = {"status": "no_round"}
|
||||
elif str(row["host_person_key"]) != person_key:
|
||||
data = {
|
||||
"status": "not_host",
|
||||
"host_display_name": str(row["host_display_name"]),
|
||||
}
|
||||
else:
|
||||
round_number = int(row["round_number"])
|
||||
entries = self._group_lottery_entries(
|
||||
group, round_number
|
||||
)
|
||||
if not entries:
|
||||
data = {
|
||||
"status": "empty",
|
||||
"round_number": round_number,
|
||||
}
|
||||
else:
|
||||
timestamp = utc_now()
|
||||
settled: list[dict[str, Any]] = []
|
||||
drawn = set(drawn_numbers)
|
||||
rates = {0: 0, 1: 50, 2: 120, 3: 300}
|
||||
for entry in entries:
|
||||
key = str(entry["person_key"])
|
||||
account = self._load_player_account(group, key)
|
||||
matches = len(drawn.intersection(entry["numbers"]))
|
||||
stake = int(entry["stake"])
|
||||
payout = stake * rates[matches] // 100
|
||||
account["balance"] += payout
|
||||
account[
|
||||
"wins" if payout > stake else "losses"
|
||||
] += 1
|
||||
append_game_record(
|
||||
account,
|
||||
kind="lottery",
|
||||
index=int(entry["player_game_index"]),
|
||||
outcome=f"命中{matches}个",
|
||||
stake=stake,
|
||||
payout=payout,
|
||||
summary=(
|
||||
"选号 "
|
||||
+ " ".join(map(str, entry["numbers"]))
|
||||
+ ";开奖号 "
|
||||
+ " ".join(map(str, drawn_numbers))
|
||||
),
|
||||
)
|
||||
self._save_player_account(
|
||||
group, key, account, timestamp
|
||||
)
|
||||
self.connection.execute(
|
||||
"""
|
||||
UPDATE group_lottery_entries
|
||||
SET matches = ?, payout = ?, updated_at = ?
|
||||
WHERE group_fingerprint = ?
|
||||
AND round_number = ? AND person_key = ?
|
||||
""",
|
||||
(
|
||||
matches,
|
||||
payout,
|
||||
timestamp,
|
||||
group,
|
||||
round_number,
|
||||
key,
|
||||
),
|
||||
)
|
||||
result = dict(entry)
|
||||
result.update(
|
||||
{
|
||||
"matches": matches,
|
||||
"payout": payout,
|
||||
"balance": int(account["balance"]),
|
||||
}
|
||||
)
|
||||
settled.append(result)
|
||||
self.connection.execute(
|
||||
"""
|
||||
UPDATE group_lottery_rounds
|
||||
SET state = 'drawn', drawn_numbers_json = ?,
|
||||
updated_at = ?
|
||||
WHERE group_fingerprint = ? AND round_number = ?
|
||||
""",
|
||||
(
|
||||
canonical_json(list(drawn_numbers)),
|
||||
timestamp,
|
||||
group,
|
||||
round_number,
|
||||
),
|
||||
)
|
||||
data = {
|
||||
"status": "drawn",
|
||||
"round_number": round_number,
|
||||
"drawn_numbers": list(drawn_numbers),
|
||||
"entries": settled,
|
||||
}
|
||||
response = render(data)
|
||||
timestamp = utc_now()
|
||||
self._store_player_action(
|
||||
group, event_id, person_key, command, response, timestamp
|
||||
)
|
||||
self.connection.execute("COMMIT")
|
||||
return response
|
||||
except BaseException:
|
||||
self.connection.execute("ROLLBACK")
|
||||
raise
|
||||
|
||||
def cancel_group_lottery(
|
||||
self,
|
||||
group: str,
|
||||
sender: str,
|
||||
event_id: str,
|
||||
command: str,
|
||||
render: Callable[[dict[str, Any]], str],
|
||||
) -> str:
|
||||
person = self._player_identity(group, sender)
|
||||
person_key = str(person["person_key"])
|
||||
with self.lock:
|
||||
self.connection.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
replay = self._player_action_replay(
|
||||
group, event_id, person_key, command
|
||||
)
|
||||
if replay is not None:
|
||||
self.connection.execute("COMMIT")
|
||||
return replay
|
||||
row = self._open_group_lottery(group)
|
||||
if row is None:
|
||||
data: dict[str, Any] = {"status": "no_round"}
|
||||
elif str(row["host_person_key"]) != person_key:
|
||||
data = {
|
||||
"status": "not_host",
|
||||
"host_display_name": str(row["host_display_name"]),
|
||||
}
|
||||
else:
|
||||
round_number = int(row["round_number"])
|
||||
entries = self._group_lottery_entries(
|
||||
group, round_number
|
||||
)
|
||||
timestamp = utc_now()
|
||||
for entry in entries:
|
||||
key = str(entry["person_key"])
|
||||
stake = int(entry["stake"])
|
||||
account = self._load_player_account(group, key)
|
||||
account["balance"] += stake
|
||||
append_game_record(
|
||||
account,
|
||||
kind="lottery",
|
||||
index=int(entry["player_game_index"]),
|
||||
outcome="本轮取消",
|
||||
stake=stake,
|
||||
payout=stake,
|
||||
summary="主持人取消,下注已全额退回",
|
||||
)
|
||||
self._save_player_account(
|
||||
group, key, account, timestamp
|
||||
)
|
||||
self.connection.execute(
|
||||
"""
|
||||
UPDATE group_lottery_rounds
|
||||
SET state = 'cancelled', updated_at = ?
|
||||
WHERE group_fingerprint = ? AND round_number = ?
|
||||
""",
|
||||
(timestamp, group, round_number),
|
||||
)
|
||||
data = {
|
||||
"status": "cancelled",
|
||||
"round_number": round_number,
|
||||
"entries": entries,
|
||||
}
|
||||
response = render(data)
|
||||
timestamp = utc_now()
|
||||
self._store_player_action(
|
||||
group, event_id, person_key, command, response, timestamp
|
||||
)
|
||||
self.connection.execute("COMMIT")
|
||||
return response
|
||||
except BaseException:
|
||||
self.connection.execute("ROLLBACK")
|
||||
raise
|
||||
|
||||
def player_game_history(
|
||||
self,
|
||||
group: str,
|
||||
sender: str,
|
||||
) -> dict[str, Any]:
|
||||
person = self._player_identity(group, sender)
|
||||
with self.lock:
|
||||
account = self._load_player_account(
|
||||
group, str(person["person_key"])
|
||||
)
|
||||
history = sorted(
|
||||
account["game_history"],
|
||||
key=lambda entry: float(entry.get("completed_at", 0)),
|
||||
reverse=True,
|
||||
)
|
||||
return {
|
||||
"display_name": person["display_name"],
|
||||
"history": history,
|
||||
"game_counts": dict(account["game_counts"]),
|
||||
}
|
||||
|
||||
def apply_player_action(
|
||||
self, group: str, sender: str, event_id: str, command: str,
|
||||
action: Callable[[dict[str, Any]], str],
|
||||
) -> str:
|
||||
"""Commit identity-scoped funds, game state and replay response together."""
|
||||
if not group or not event_id or not sender or sender == "unknown":
|
||||
raise LocalCommandError("无法确认本人身份,不能操作金币账户")
|
||||
person = self.resolve_person(group, sender)
|
||||
person = self._player_identity(group, sender)
|
||||
person_key = str(person["person_key"])
|
||||
with self.lock:
|
||||
self.connection.execute("BEGIN IMMEDIATE")
|
||||
try:
|
||||
previous = self.connection.execute(
|
||||
"""SELECT person_key, command, response_text FROM player_actions
|
||||
WHERE group_fingerprint = ? AND event_id = ?""",
|
||||
(group, event_id),
|
||||
).fetchone()
|
||||
if previous is not None:
|
||||
if previous["person_key"] != person_key or previous["command"] != command:
|
||||
raise LocalCommandError("同一事件不能变更账户或游戏指令")
|
||||
self.connection.execute("COMMIT")
|
||||
return str(previous["response_text"])
|
||||
stored = self.connection.execute(
|
||||
"""SELECT state_json FROM player_accounts
|
||||
WHERE group_fingerprint = ? AND person_key = ?""",
|
||||
(group, person_key),
|
||||
).fetchone()
|
||||
account = json.loads(stored["state_json"]) if stored else {
|
||||
"balance": 1000, "game": None, "wins": 0, "losses": 0, "ties": 0,
|
||||
}
|
||||
response = action(account)
|
||||
if type(account["balance"]) is not int or not 0 <= account["balance"] <= 1_000_000_000:
|
||||
raise LocalCommandError("金币结算超出允许范围")
|
||||
timestamp = utc_now()
|
||||
self.connection.execute(
|
||||
"""INSERT INTO player_accounts VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(group_fingerprint, person_key) DO UPDATE
|
||||
SET state_json = excluded.state_json, updated_at = excluded.updated_at""",
|
||||
(group, person_key, json.dumps(account, ensure_ascii=False), timestamp),
|
||||
replay = self._player_action_replay(
|
||||
group, event_id, person_key, command
|
||||
)
|
||||
self.connection.execute(
|
||||
"INSERT INTO player_actions VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(group, event_id, person_key, command, response, timestamp),
|
||||
if replay is not None:
|
||||
self.connection.execute("COMMIT")
|
||||
return replay
|
||||
account = self._load_player_account(group, person_key)
|
||||
response = action(account)
|
||||
timestamp = utc_now()
|
||||
self._save_player_account(
|
||||
group, person_key, account, timestamp
|
||||
)
|
||||
self._store_player_action(
|
||||
group,
|
||||
event_id,
|
||||
person_key,
|
||||
command,
|
||||
response,
|
||||
timestamp,
|
||||
)
|
||||
self.connection.execute("COMMIT")
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user