feat: wire bark battle platform loop
Some checks failed
CI / verify (pull_request) Has been cancelled
Some checks failed
CI / verify (pull_request) Has been cancelled
This commit is contained in:
497
server-rs/crates/shared-contracts/src/bark_battle.rs
Normal file
497
server-rs/crates/shared-contracts/src/bark_battle.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user