feat: add puzzle clear template runtime

This commit is contained in:
2026-06-03 22:11:46 +08:00
parent 6e74cf5add
commit 1b5e098225
148 changed files with 19588 additions and 241 deletions

View 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 }

File diff suppressed because it is too large Load Diff

View 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())
}

View 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",
}
}
}

View 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 {}

View 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,
},
}

View 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::*;