498 lines
17 KiB
Rust
498 lines
17 KiB
Rust
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");
|
|
}
|
|
}
|