feat: add puzzle clear template runtime
This commit is contained in:
14
server-rs/crates/module-puzzle-clear/Cargo.toml
Normal file
14
server-rs/crates/module-puzzle-clear/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "module-puzzle-clear"
|
||||
edition.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
spacetime-types = ["dep:spacetimedb"]
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
shared-kernel = { workspace = true }
|
||||
spacetimedb = { workspace = true, optional = true }
|
||||
1837
server-rs/crates/module-puzzle-clear/src/application.rs
Normal file
1837
server-rs/crates/module-puzzle-clear/src/application.rs
Normal file
File diff suppressed because it is too large
Load Diff
25
server-rs/crates/module-puzzle-clear/src/commands.rs
Normal file
25
server-rs/crates/module-puzzle-clear/src/commands.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use shared_kernel::normalize_required_string;
|
||||
|
||||
use crate::{PuzzleClearOrientation, PuzzleClearShapeKind};
|
||||
|
||||
pub fn parse_puzzle_clear_shape_kind(value: &str) -> PuzzleClearShapeKind {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"1x3" | "one-by-three" => PuzzleClearShapeKind::OneByThree,
|
||||
"2x2" | "two-by-two" => PuzzleClearShapeKind::TwoByTwo,
|
||||
"2x3" | "two-by-three" => PuzzleClearShapeKind::TwoByThree,
|
||||
_ => PuzzleClearShapeKind::OneByTwo,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_puzzle_clear_orientation(value: &str) -> PuzzleClearOrientation {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"vertical" | "纵向" => PuzzleClearOrientation::Vertical,
|
||||
_ => PuzzleClearOrientation::Horizontal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_puzzle_clear_seed(seed: &str, fallback: &str) -> String {
|
||||
normalize_required_string(seed)
|
||||
.or_else(|| normalize_required_string(fallback))
|
||||
.unwrap_or_else(|| "puzzle-clear".to_string())
|
||||
}
|
||||
191
server-rs/crates/module-puzzle-clear/src/domain.rs
Normal file
191
server-rs/crates/module-puzzle-clear/src/domain.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "spacetime-types")]
|
||||
use spacetimedb::SpacetimeType;
|
||||
|
||||
pub const PUZZLE_CLEAR_PLAY_ID: &str = "puzzle-clear";
|
||||
pub const PUZZLE_CLEAR_PUBLIC_WORK_CODE_PREFIX: &str = "PC-";
|
||||
pub const PUZZLE_CLEAR_SESSION_ID_PREFIX: &str = "puzzle-clear-session-";
|
||||
pub const PUZZLE_CLEAR_PROFILE_ID_PREFIX: &str = "puzzle-clear-profile-";
|
||||
pub const PUZZLE_CLEAR_WORK_ID_PREFIX: &str = "puzzle-clear-work-";
|
||||
pub const PUZZLE_CLEAR_RUN_ID_PREFIX: &str = "puzzle-clear-run-";
|
||||
pub const PUZZLE_CLEAR_LEVEL_DURATION_SECONDS: u32 = 600;
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PuzzleClearShapeKind {
|
||||
OneByTwo,
|
||||
OneByThree,
|
||||
TwoByTwo,
|
||||
TwoByThree,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PuzzleClearOrientation {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PuzzleClearRunStatus {
|
||||
Playing,
|
||||
LevelFailed,
|
||||
LevelCleared,
|
||||
Finished,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PuzzleClearLevelConfig {
|
||||
pub level_index: u32,
|
||||
pub board_size: u32,
|
||||
pub target_clears: u32,
|
||||
pub duration_seconds: u32,
|
||||
pub unlocked_shapes: Vec<PuzzleClearShapeKind>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PuzzleClearShapeQuota {
|
||||
pub shape: PuzzleClearShapeKind,
|
||||
pub count: u32,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PuzzleClearPatternGroup {
|
||||
pub group_id: String,
|
||||
pub shape: PuzzleClearShapeKind,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub atlas_x: u32,
|
||||
pub atlas_y: u32,
|
||||
pub atlas_width: u32,
|
||||
pub atlas_height: u32,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PuzzleClearCard {
|
||||
pub card_id: String,
|
||||
pub group_id: String,
|
||||
pub shape: PuzzleClearShapeKind,
|
||||
pub orientation: PuzzleClearOrientation,
|
||||
pub part_x: u32,
|
||||
pub part_y: u32,
|
||||
pub image_src: String,
|
||||
pub image_object_key: String,
|
||||
pub asset_object_id: String,
|
||||
pub source_atlas_cell: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PuzzleClearCell {
|
||||
pub row: u32,
|
||||
pub col: u32,
|
||||
pub card: Option<PuzzleClearCard>,
|
||||
pub locked_group_id: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PuzzleClearBoard {
|
||||
pub rows: u32,
|
||||
pub cols: u32,
|
||||
pub cells: Vec<PuzzleClearCell>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PuzzleClearDeck {
|
||||
pub ready_columns: Vec<Vec<PuzzleClearCard>>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PuzzleClearMove {
|
||||
pub from_row: u32,
|
||||
pub from_col: u32,
|
||||
pub to_row: u32,
|
||||
pub to_col: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PuzzleClearElimination {
|
||||
pub group_id: String,
|
||||
pub positions: Vec<(u32, u32)>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PuzzleClearRunSnapshot {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub profile_id: String,
|
||||
pub status: PuzzleClearRunStatus,
|
||||
pub level_index: u32,
|
||||
pub clears_done: u32,
|
||||
pub board: PuzzleClearBoard,
|
||||
pub deck: PuzzleClearDeck,
|
||||
pub started_at_ms: u64,
|
||||
pub level_started_at_ms: u64,
|
||||
pub finished_at_ms: Option<u64>,
|
||||
}
|
||||
|
||||
impl PuzzleClearShapeKind {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::OneByTwo => "1x2",
|
||||
Self::OneByThree => "1x3",
|
||||
Self::TwoByTwo => "2x2",
|
||||
Self::TwoByThree => "2x3",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base_dimensions(self) -> (u32, u32) {
|
||||
match self {
|
||||
Self::OneByTwo => (2, 1),
|
||||
Self::OneByThree => (3, 1),
|
||||
Self::TwoByTwo => (2, 2),
|
||||
Self::TwoByThree => (3, 2),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dimensions(self, orientation: PuzzleClearOrientation) -> (u32, u32) {
|
||||
let (width, height) = self.base_dimensions();
|
||||
if matches!(orientation, PuzzleClearOrientation::Vertical)
|
||||
&& matches!(
|
||||
self,
|
||||
PuzzleClearShapeKind::OneByTwo
|
||||
| PuzzleClearShapeKind::OneByThree
|
||||
| PuzzleClearShapeKind::TwoByThree
|
||||
)
|
||||
{
|
||||
(height, width)
|
||||
} else {
|
||||
(width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PuzzleClearOrientation {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Horizontal => "horizontal",
|
||||
Self::Vertical => "vertical",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PuzzleClearRunStatus {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Playing => "playing",
|
||||
Self::LevelFailed => "level_failed",
|
||||
Self::LevelCleared => "level_cleared",
|
||||
Self::Finished => "finished",
|
||||
}
|
||||
}
|
||||
}
|
||||
37
server-rs/crates/module-puzzle-clear/src/errors.rs
Normal file
37
server-rs/crates/module-puzzle-clear/src/errors.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum PuzzleClearError {
|
||||
MissingRunId,
|
||||
MissingOwnerUserId,
|
||||
MissingProfileId,
|
||||
InvalidLevel,
|
||||
InvalidBoard,
|
||||
InvalidPosition,
|
||||
EmptyDeck,
|
||||
NoPlayableMove,
|
||||
RunNotPlaying,
|
||||
LevelExpired,
|
||||
MissingCard,
|
||||
}
|
||||
|
||||
impl fmt::Display for PuzzleClearError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let message = match self {
|
||||
Self::MissingRunId => "puzzle-clear run_id 不能为空",
|
||||
Self::MissingOwnerUserId => "puzzle-clear owner_user_id 不能为空",
|
||||
Self::MissingProfileId => "puzzle-clear profile_id 不能为空",
|
||||
Self::InvalidLevel => "puzzle-clear 关卡配置无效",
|
||||
Self::InvalidBoard => "puzzle-clear 棋盘状态无效",
|
||||
Self::InvalidPosition => "puzzle-clear 坐标无效",
|
||||
Self::EmptyDeck => "puzzle-clear 发牌池为空",
|
||||
Self::NoPlayableMove => "puzzle-clear 棋盘没有可解拼接",
|
||||
Self::RunNotPlaying => "puzzle-clear 当前 run 不在 playing 状态",
|
||||
Self::LevelExpired => "puzzle-clear 当前关卡已经超时",
|
||||
Self::MissingCard => "puzzle-clear 目标格子没有卡牌",
|
||||
};
|
||||
f.write_str(message)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PuzzleClearError {}
|
||||
31
server-rs/crates/module-puzzle-clear/src/events.rs
Normal file
31
server-rs/crates/module-puzzle-clear/src/events.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
//! 拼消消领域事件。
|
||||
//!
|
||||
//! 事件只表达已经发生的领域事实,持久化、统计投影和前端通知由
|
||||
//! SpacetimeDB adapter 与 BFF 编排层决定。
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum PuzzleClearDomainEvent {
|
||||
DraftCompiled {
|
||||
profile_id: String,
|
||||
owner_user_id: String,
|
||||
occurred_at_micros: i64,
|
||||
},
|
||||
WorkPublished {
|
||||
profile_id: String,
|
||||
owner_user_id: String,
|
||||
occurred_at_micros: i64,
|
||||
},
|
||||
LevelCleared {
|
||||
run_id: String,
|
||||
owner_user_id: String,
|
||||
level_index: u32,
|
||||
clears_done: u32,
|
||||
occurred_at_micros: i64,
|
||||
},
|
||||
RunSettled {
|
||||
run_id: String,
|
||||
owner_user_id: String,
|
||||
status: String,
|
||||
occurred_at_micros: i64,
|
||||
},
|
||||
}
|
||||
11
server-rs/crates/module-puzzle-clear/src/lib.rs
Normal file
11
server-rs/crates/module-puzzle-clear/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
mod application;
|
||||
mod commands;
|
||||
mod domain;
|
||||
mod errors;
|
||||
mod events;
|
||||
|
||||
pub use application::*;
|
||||
pub use commands::*;
|
||||
pub use domain::*;
|
||||
pub use errors::*;
|
||||
pub use events::*;
|
||||
Reference in New Issue
Block a user