feat: wire bark battle platform loop
Some checks are pending
CI / verify (pull_request) Waiting to run

This commit is contained in:
2026-05-14 18:20:46 +08:00
parent 8c6ec9e6e4
commit 1d7ef7e4b6
73 changed files with 7933 additions and 107 deletions

View File

@@ -0,0 +1,497 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BarkBattleDifficultyPreset {
Easy,
Normal,
Hard,
}
impl Default for BarkBattleDifficultyPreset {
fn default() -> Self {
Self::Normal
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BarkBattleServerResult {
PlayerWin,
OpponentWin,
Draw,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum BarkBattleFinishStatus {
Accepted,
AcceptedWithFlags,
Rejected,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleConfigEditorPayload {
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub theme_preset: String,
pub player_dog_skin_preset: String,
pub opponent_dog_skin_preset: String,
#[serde(default)]
pub difficulty_preset: BarkBattleDifficultyPreset,
pub leaderboard_enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleDraftCreateRequest {
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub theme_preset: String,
pub player_dog_skin_preset: String,
pub opponent_dog_skin_preset: String,
#[serde(default)]
pub difficulty_preset: BarkBattleDifficultyPreset,
pub leaderboard_enabled: bool,
}
impl From<BarkBattleDraftCreateRequest> for BarkBattleConfigEditorPayload {
fn from(value: BarkBattleDraftCreateRequest) -> Self {
Self {
title: value.title,
description: value.description,
theme_preset: value.theme_preset,
player_dog_skin_preset: value.player_dog_skin_preset,
opponent_dog_skin_preset: value.opponent_dog_skin_preset,
difficulty_preset: value.difficulty_preset,
leaderboard_enabled: value.leaderboard_enabled,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleWorkPublishRequest {
pub draft_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub work_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub published_snapshot: Option<BarkBattleConfigEditorPayload>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleDraftConfig {
pub draft_id: String,
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub theme_preset: String,
pub player_dog_skin_preset: String,
pub opponent_dog_skin_preset: String,
#[serde(default)]
pub difficulty_preset: BarkBattleDifficultyPreset,
pub leaderboard_enabled: bool,
pub updated_at: String,
}
impl Default for BarkBattleDraftConfig {
fn default() -> Self {
Self {
draft_id: String::new(),
title: String::new(),
description: None,
theme_preset: String::new(),
player_dog_skin_preset: String::new(),
opponent_dog_skin_preset: String::new(),
difficulty_preset: BarkBattleDifficultyPreset::Normal,
leaderboard_enabled: true,
updated_at: String::new(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattlePublishedConfig {
pub work_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub draft_id: Option<String>,
pub config_version: u32,
pub ruleset_version: String,
pub play_type_id: String,
pub title: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub theme_preset: String,
pub player_dog_skin_preset: String,
pub opponent_dog_skin_preset: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub leaderboard_enabled: bool,
pub updated_at: String,
pub published_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleRuntimeConfig {
pub work_id: String,
pub config_version: u32,
pub ruleset_version: String,
pub play_type_id: String,
pub duration_ms: u64,
pub energy_min: f32,
pub energy_max: f32,
pub draw_threshold: f32,
pub min_bark_gap_ms: u64,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub theme_preset: String,
pub player_dog_skin_preset: String,
pub opponent_dog_skin_preset: String,
pub leaderboard_enabled: bool,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleRunStartRequest {
pub work_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_version: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_route: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_runtime_version: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleRunStartResponse {
pub run_id: String,
pub run_token: String,
pub work_id: String,
pub config_version: u32,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub runtime_config: BarkBattleRuntimeConfig,
pub server_started_at: String,
pub expires_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleDerivedMetrics {
pub trigger_count: u32,
pub max_volume: f32,
pub average_volume: f32,
pub final_energy: f32,
pub combo_max: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleRunFinishRequest {
pub work_id: String,
pub run_id: String,
pub run_token: String,
pub config_version: u32,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub client_started_at: String,
pub client_finished_at: String,
pub duration_ms: u64,
pub derived_metrics: BarkBattleDerivedMetrics,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_result: Option<BarkBattleServerResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sample_digest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub client_runtime_version: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleScoreSummary {
pub duration_ms: u64,
pub trigger_count: u32,
pub max_volume: f32,
pub average_volume: f32,
pub final_energy: f32,
pub combo_max: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleRunFinishResponse {
pub status: BarkBattleFinishStatus,
pub run_id: String,
pub work_id: String,
pub config_version: u32,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub server_result: BarkBattleServerResult,
pub score_summary: BarkBattleScoreSummary,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub leaderboard_score: Option<u64>,
#[serde(default)]
pub anti_cheat_flags: Vec<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleLeaderboardEntry {
pub rank: u32,
pub run_id: String,
pub work_id: String,
pub config_version: u32,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub display_name: String,
pub server_result: BarkBattleServerResult,
pub score_summary: BarkBattleScoreSummary,
pub leaderboard_score: u64,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleLeaderboardResponse {
pub work_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_version: Option<u32>,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
#[serde(default)]
pub entries: Vec<BarkBattleLeaderboardEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub viewer_best: Option<BarkBattleLeaderboardEntry>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattlePersonalHistoryItem {
pub run_id: String,
pub work_id: String,
pub config_version: u32,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub server_result: BarkBattleServerResult,
pub score_summary: BarkBattleScoreSummary,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub leaderboard_score: Option<u64>,
#[serde(default)]
pub anti_cheat_flags: Vec<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattlePersonalBestSummary {
pub work_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_version: Option<u32>,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_leaderboard_score: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_final_energy: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_trigger_count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_max_volume: Option<f32>,
pub win_count: u64,
pub draw_count: u64,
pub loss_count: u64,
pub finish_count: u64,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattlePersonalHistoryResponse {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub work_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub difficulty_preset: Option<BarkBattleDifficultyPreset>,
#[serde(default)]
pub items: Vec<BarkBattlePersonalHistoryItem>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_summary: Option<BarkBattlePersonalBestSummary>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BarkBattleWorkStats {
pub work_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_version: Option<u32>,
pub ruleset_version: String,
pub difficulty_preset: BarkBattleDifficultyPreset,
pub play_start_count: u64,
pub finish_count: u64,
pub win_count: u64,
pub draw_count: u64,
pub loss_count: u64,
pub flagged_count: u64,
pub leaderboard_entry_count: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_leaderboard_score: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub best_final_energy: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub average_final_energy: Option<f32>,
pub updated_at: String,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn draft_config_defaults_to_normal_difficulty() {
let config = BarkBattleDraftConfig::default();
assert_eq!(config.difficulty_preset, BarkBattleDifficultyPreset::Normal);
}
#[test]
fn run_requests_carry_config_and_ruleset_identity() {
let start = BarkBattleRunStartRequest {
work_id: "work-1".to_string(),
config_version: Some(3),
source_route: Some("gallery".to_string()),
client_runtime_version: Some("runtime-v1".to_string()),
};
let start_payload = serde_json::to_value(start).expect("start request should serialize");
assert_eq!(start_payload["workId"], json!("work-1"));
assert_eq!(start_payload["configVersion"], json!(3));
assert_eq!(start_payload["sourceRoute"], json!("gallery"));
let finish = BarkBattleRunFinishRequest {
work_id: "work-1".to_string(),
run_id: "run-1".to_string(),
run_token: "token-1".to_string(),
config_version: 3,
ruleset_version: "bark-battle-ruleset-v1".to_string(),
difficulty_preset: BarkBattleDifficultyPreset::Hard,
client_started_at: "2026-05-13T11:00:00Z".to_string(),
client_finished_at: "2026-05-13T11:00:30Z".to_string(),
duration_ms: 30_000,
derived_metrics: BarkBattleDerivedMetrics {
trigger_count: 12,
max_volume: 0.95,
average_volume: 0.62,
final_energy: 88.5,
combo_max: 7,
},
client_result: Some(BarkBattleServerResult::PlayerWin),
sample_digest: Some("digest-1".to_string()),
client_runtime_version: None,
};
let finish_payload = serde_json::to_value(finish).expect("finish request should serialize");
assert_eq!(finish_payload["configVersion"], json!(3));
assert_eq!(
finish_payload["rulesetVersion"],
json!("bark-battle-ruleset-v1")
);
assert_eq!(finish_payload["difficultyPreset"], json!("hard"));
}
#[test]
fn optional_fields_are_omitted_when_absent() {
let draft = BarkBattleDraftConfig::default();
let payload = serde_json::to_value(draft).expect("draft should serialize");
assert!(!payload.as_object().unwrap().contains_key("description"));
let response = BarkBattlePersonalHistoryResponse {
work_id: None,
difficulty_preset: None,
items: Vec::new(),
best_summary: None,
updated_at: "2026-05-13T11:00:00Z".to_string(),
};
let payload = serde_json::to_value(response).expect("history response should serialize");
assert!(!payload.as_object().unwrap().contains_key("workId"));
assert!(
!payload
.as_object()
.unwrap()
.contains_key("difficultyPreset")
);
assert!(!payload.as_object().unwrap().contains_key("bestSummary"));
}
#[test]
fn finish_response_serializes_player_win_and_accepted() {
let response = BarkBattleRunFinishResponse {
status: BarkBattleFinishStatus::Accepted,
run_id: "run-1".to_string(),
work_id: "work-1".to_string(),
config_version: 3,
ruleset_version: "bark-battle-ruleset-v1".to_string(),
difficulty_preset: BarkBattleDifficultyPreset::Normal,
server_result: BarkBattleServerResult::PlayerWin,
score_summary: BarkBattleScoreSummary {
duration_ms: 30_000,
trigger_count: 12,
max_volume: 0.95,
average_volume: 0.62,
final_energy: 88.5,
combo_max: 7,
},
leaderboard_score: Some(98_765),
anti_cheat_flags: Vec::new(),
updated_at: "2026-05-13T11:00:00Z".to_string(),
};
let payload = serde_json::to_value(response).expect("finish response should serialize");
assert_eq!(payload["runId"], json!("run-1"));
assert_eq!(payload["status"], json!("accepted"));
assert_eq!(payload["serverResult"], json!("player_win"));
assert_eq!(payload["leaderboardScore"], json!(98_765));
assert_eq!(payload["scoreSummary"]["finalEnergy"], json!(88.5));
}
#[test]
fn work_stats_fields_are_constructible() {
let stats = BarkBattleWorkStats {
work_id: "work-1".to_string(),
config_version: Some(3),
ruleset_version: "bark-battle-ruleset-v1".to_string(),
difficulty_preset: BarkBattleDifficultyPreset::Normal,
play_start_count: 10,
finish_count: 9,
win_count: 5,
draw_count: 2,
loss_count: 2,
flagged_count: 1,
leaderboard_entry_count: 4,
best_leaderboard_score: Some(98_765),
best_final_energy: Some(97.5),
average_final_energy: Some(73.25),
updated_at: "2026-05-13T11:00:00Z".to_string(),
};
assert_eq!(stats.work_id, "work-1");
assert_eq!(stats.play_start_count, 10);
assert_eq!(stats.finish_count, 9);
assert_eq!(stats.win_count, 5);
assert_eq!(stats.draw_count, 2);
assert_eq!(stats.loss_count, 2);
assert_eq!(stats.flagged_count, 1);
assert_eq!(stats.leaderboard_entry_count, 4);
assert_eq!(stats.best_leaderboard_score, Some(98_765));
assert_eq!(stats.best_final_energy, Some(97.5));
assert_eq!(stats.average_final_energy, Some(73.25));
assert_eq!(stats.updated_at, "2026-05-13T11:00:00Z");
}
}

View File

@@ -4,6 +4,7 @@ pub mod api;
#[cfg(feature = "oss-contracts")]
pub mod assets;
pub mod auth;
pub mod bark_battle;
pub mod big_fish;
pub mod big_fish_works;
pub mod creation_agent_document_input;