service_contracts/
lib.rs

1#![deny(clippy::use_self, clippy::shadow_unrelated)]
2use std::fmt::{self, Display};
3use ts_rs::TS;
4
5use mahjong_core::{
6    deck::DeckContent, game::GameVersion, game_summary::GameSummary, hand::SetIdContent, Game,
7    GameId, Hand, Hands, PlayerId, TileId,
8};
9use rustc_hash::{FxHashMap, FxHashSet};
10use serde::{Deserialize, Serialize};
11pub use service_player::{ServicePlayer, ServicePlayerGame, ServicePlayerSummary};
12
13mod service_player;
14
15#[derive(Debug, Clone, Serialize, Deserialize, TS)]
16#[ts(export)]
17pub struct GameSettings {
18    pub ai_enabled: bool,
19    pub auto_sort_players: FxHashSet<PlayerId>,
20    pub auto_stop_claim_meld: FxHashSet<PlayerId>,
21    pub dead_wall: bool,
22    pub discard_wait_ms: Option<i32>,
23    pub fixed_settings: bool,
24    pub last_discard_time: i128,
25}
26
27impl Default for GameSettings {
28    fn default() -> Self {
29        Self {
30            ai_enabled: true,
31            auto_sort_players: FxHashSet::default(),
32            auto_stop_claim_meld: FxHashSet::default(),
33            dead_wall: false,
34            discard_wait_ms: Some(1000),
35            fixed_settings: false,
36            last_discard_time: 0,
37        }
38    }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, TS)]
42#[ts(export)]
43pub struct ServiceGame {
44    pub created_at: i64,
45    pub game: Game,
46    pub players: FxHashMap<PlayerId, ServicePlayer>,
47    pub settings: GameSettings,
48    pub updated_at: i64,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, TS)]
52#[ts(export)]
53pub struct GameSettingsSummary {
54    pub ai_enabled: bool,
55    pub auto_sort: bool,
56    pub auto_stop_claim_meld: bool,
57    pub dead_wall: bool,
58    pub discard_wait_ms: Option<i32>,
59    pub fixed_settings: bool,
60    pub last_discard_time: String,
61}
62
63impl GameSettingsSummary {
64    pub fn from_game_settings(settings: &GameSettings, player_id: &PlayerId) -> Self {
65        Self {
66            ai_enabled: settings.ai_enabled,
67            auto_sort: settings.auto_sort_players.iter().any(|p| p == player_id),
68            auto_stop_claim_meld: settings.auto_stop_claim_meld.iter().any(|p| p == player_id),
69            dead_wall: settings.dead_wall,
70            discard_wait_ms: settings.discard_wait_ms,
71            fixed_settings: settings.fixed_settings,
72            last_discard_time: settings.last_discard_time.to_string(),
73        }
74    }
75
76    pub fn to_game_settings(&self, player_id: &PlayerId, settings: &GameSettings) -> GameSettings {
77        let mut new_settings = settings.clone();
78
79        if self.auto_sort {
80            new_settings.auto_sort_players.insert(player_id.clone());
81        } else {
82            new_settings.auto_sort_players.remove(player_id);
83        }
84
85        if self.auto_stop_claim_meld {
86            new_settings.auto_stop_claim_meld.insert(player_id.clone());
87        } else {
88            new_settings.auto_stop_claim_meld.remove(player_id);
89        }
90
91        new_settings.ai_enabled = self.ai_enabled;
92        new_settings.discard_wait_ms = self.discard_wait_ms;
93        new_settings.fixed_settings = self.fixed_settings;
94        new_settings.last_discard_time = self.last_discard_time.parse().unwrap_or(0);
95
96        new_settings
97    }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, TS)]
101#[ts(export)]
102pub struct ServiceGameSummary {
103    pub game_summary: GameSummary,
104    pub players: FxHashMap<PlayerId, ServicePlayerSummary>,
105    pub settings: GameSettingsSummary,
106}
107
108impl ServiceGame {
109    pub fn get_ai_players(&self) -> FxHashSet<PlayerId> {
110        self.players
111            .iter()
112            .filter(|(_, player)| player.is_ai)
113            .map(|(id, _)| id.clone())
114            .collect::<FxHashSet<PlayerId>>()
115    }
116}
117
118impl ServiceGameSummary {
119    pub fn from_service_game(game: &ServiceGame, player_id: &PlayerId) -> Option<Self> {
120        let game_summary = GameSummary::from_game(&game.game, player_id);
121
122        game_summary.as_ref()?;
123
124        let game_summary = game_summary.unwrap();
125
126        let players: FxHashMap<PlayerId, ServicePlayerSummary> = game
127            .players
128            .clone()
129            .into_iter()
130            .map(|(id, player)| {
131                (
132                    id,
133                    ServicePlayerSummary {
134                        id: player.id,
135                        name: player.name,
136                    },
137                )
138            })
139            .collect();
140
141        Some(Self {
142            game_summary,
143            players,
144            settings: GameSettingsSummary::from_game_settings(&game.settings, player_id),
145        })
146    }
147
148    pub fn get_turn_player(&self) -> Option<ServicePlayerSummary> {
149        let player_id = self.game_summary.players.0[self.game_summary.round.player_index].clone();
150
151        self.players.get(&player_id).cloned()
152    }
153
154    pub fn get_dealer_player(&self) -> Option<ServicePlayerSummary> {
155        let player_id =
156            self.game_summary.players.0[self.game_summary.round.dealer_player_index].clone();
157
158        self.players.get(&player_id).cloned()
159    }
160}
161
162#[allow(clippy::large_enum_variant)]
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub enum SocketMessage {
165    GameUpdate(ServiceGame),
166    GameSummaryUpdate(ServiceGameSummary),
167    ListRooms,
168    Name(String),
169    PlayerLeft,
170    PlayerJoined,
171}
172
173#[derive(Serialize, Deserialize, TS)]
174#[ts(export)]
175pub struct WebSocketQuery {
176    pub game_id: GameId,
177    pub player_id: Option<PlayerId>,
178    pub token: String,
179}
180
181pub type AdminGetGamesResponse = Vec<ServicePlayerGame>;
182
183#[derive(Deserialize, Serialize, TS)]
184#[ts(export)]
185#[serde(tag = "type")]
186pub enum Queries {
187    UserBreakMeld {
188        game_id: GameId,
189        player_id: PlayerId,
190        set_id: SetIdContent,
191    },
192    UserCreateGame {
193        ai_player_names: Option<Vec<String>>,
194        auto_sort_own: Option<bool>,
195        dead_wall: Option<bool>,
196        player_id: PlayerId,
197    },
198    UserCreateMeld {
199        game_id: GameId,
200        is_concealed: bool,
201        is_upgrade: bool,
202        player_id: PlayerId,
203        tiles: FxHashSet<TileId>,
204    },
205    UserDiscardTile {
206        game_id: GameId,
207        tile_id: TileId,
208    },
209    UserDrawTile {
210        game_id: GameId,
211        game_version: GameVersion,
212        player_id: PlayerId,
213    },
214    UserGetDashboard,
215    UserMovePlayer {
216        game_id: GameId,
217        player_id: PlayerId,
218    },
219}
220
221#[derive(Deserialize, Serialize, TS)]
222#[ts(export)]
223pub struct UserGetDashboardResponse {
224    pub auth_info: AuthInfoSummary,
225    pub player: DashboardPlayer,
226    pub player_games: Vec<DashboardGame>,
227    pub player_total_score: i32,
228}
229
230#[derive(Deserialize, Serialize, TS)]
231#[ts(export)]
232#[serde(tag = "type")]
233pub enum QueriesResponses {
234    UserBreakMeld { game: ServiceGameSummary },
235    UserCreateGame { game: ServiceGameSummary },
236    UserCreateMeld { game: ServiceGameSummary },
237    UserDiscardTile { game: ServiceGameSummary },
238    UserDrawTile { game: ServiceGameSummary },
239    UserGetDashboard { dashboard: UserGetDashboardResponse },
240    UserMovePlayer { game: ServiceGameSummary },
241}
242
243pub type AdminPostDrawTileResponse = Hand;
244
245#[derive(Debug, Clone, Serialize, Deserialize, TS)]
246#[ts(export)]
247pub struct AdminPostCreateMeldRequest {
248    pub is_concealed: bool,
249    pub is_upgrade: bool,
250    pub player_id: String,
251    pub tiles: FxHashSet<TileId>,
252}
253pub type AdminPostCreateMeldResponse = Hand;
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct AdminPostBreakMeldRequest {
257    pub player_id: String,
258    pub set_id: SetIdContent,
259}
260pub type AdminPostBreakMeldResponse = Hand;
261
262#[derive(Debug, Clone, Serialize, Deserialize, TS)]
263#[ts(export)]
264pub struct AdminPostDiscardTileRequest {
265    pub tile_id: TileId,
266}
267pub type AdminPostDiscardTileResponse = ServiceGame;
268
269pub type AdminPostMovePlayerRequest = ();
270
271#[derive(Deserialize, Serialize, TS)]
272#[ts(export)]
273pub struct AdminPostMovePlayerResponse(pub ServiceGame);
274
275pub type AdminPostSortHandsRequest = ();
276
277#[derive(Deserialize, Serialize, TS)]
278#[ts(export)]
279pub struct AdminPostSortHandsResponse(pub Hands);
280
281#[derive(Debug, Clone, Serialize, Deserialize, TS)]
282#[ts(export)]
283pub struct AdminPostClaimTileRequest {
284    pub player_id: PlayerId,
285}
286#[derive(Debug, Clone, Serialize, Deserialize, TS)]
287#[ts(export)]
288pub struct AdminPostClaimTileResponse(pub ServiceGame);
289
290#[derive(Deserialize, Serialize, TS)]
291#[ts(export)]
292pub struct UserLoadGameQuery {
293    pub player_id: PlayerId,
294}
295#[derive(Debug, Clone, Serialize, Deserialize, TS)]
296#[ts(export)]
297pub struct UserGetLoadGameResponse(pub ServiceGameSummary);
298
299#[derive(Deserialize, Serialize, TS)]
300#[ts(export)]
301pub struct UserPostSortHandRequest {
302    pub game_version: GameVersion,
303    pub player_id: PlayerId,
304    pub tiles: Option<Vec<TileId>>,
305}
306#[derive(Deserialize, Serialize, TS)]
307#[ts(export)]
308pub struct UserPostSortHandResponse(pub ServiceGameSummary);
309
310#[derive(Deserialize, Serialize, TS)]
311#[ts(export)]
312pub struct AdminPostSayMahjongRequest {
313    pub player_id: PlayerId,
314}
315pub type AdminPostSayMahjongResponse = ServiceGame;
316
317#[derive(Deserialize, Serialize, TS)]
318#[ts(export)]
319pub struct AdminPostAIContinueRequest {
320    pub draw: Option<bool>,
321}
322#[derive(Deserialize, Serialize)]
323pub struct AdminPostAIContinueResponse {
324    pub service_game: ServiceGame,
325    pub changed: bool,
326}
327
328#[derive(Deserialize, Serialize, TS)]
329#[ts(export)]
330pub struct UserPostAIContinueRequest {
331    pub player_id: PlayerId,
332}
333#[derive(Deserialize, Serialize, TS)]
334#[ts(export)]
335pub struct UserPostAIContinueResponse {
336    pub service_game_summary: ServiceGameSummary,
337    pub changed: bool,
338}
339
340#[derive(Deserialize, Serialize, TS)]
341#[ts(export)]
342pub struct UserPostClaimTileRequest {
343    pub player_id: PlayerId,
344}
345#[derive(Deserialize, Serialize, TS)]
346#[ts(export)]
347pub struct UserPostClaimTileResponse(pub ServiceGameSummary);
348
349#[derive(Deserialize, Serialize, TS)]
350#[ts(export)]
351pub struct UserPostSayMahjongRequest {
352    pub player_id: PlayerId,
353}
354#[derive(Deserialize, Serialize, TS)]
355#[ts(export)]
356pub struct UserPostSayMahjongResponse(pub ServiceGameSummary);
357
358#[derive(Deserialize, Serialize, TS)]
359#[ts(export)]
360pub struct UserPostSetGameSettingsRequest {
361    pub player_id: PlayerId,
362    pub settings: GameSettingsSummary,
363}
364#[derive(Deserialize, Serialize, TS)]
365#[ts(export)]
366pub struct UserPostSetGameSettingsResponse(pub ServiceGameSummary);
367
368#[derive(Deserialize, Serialize, TS)]
369#[ts(export)]
370pub struct UserPostJoinGameResponse(pub PlayerId);
371
372#[derive(Deserialize, Serialize, Debug, TS)]
373#[ts(export)]
374pub struct UserPostSetAuthRequest {
375    pub username: String,
376    pub password: String,
377}
378#[derive(Deserialize, Serialize, TS)]
379#[ts(export)]
380pub struct UserPostSetAuthResponse {
381    pub token: String,
382}
383
384#[derive(Deserialize, Serialize, Debug, TS)]
385#[ts(export)]
386pub struct UserPostSetAuthAnonRequest {
387    pub id_token: String,
388}
389#[derive(Deserialize, Serialize, TS)]
390#[ts(export)]
391pub struct UserPostSetAuthAnonResponse {
392    pub token: String,
393}
394
395#[derive(Deserialize, Serialize, TS)]
396#[ts(export)]
397pub struct UserPostPassRoundRequest {
398    pub player_id: PlayerId,
399}
400#[derive(Deserialize, Serialize, TS)]
401#[ts(export)]
402pub struct UserPostPassRoundResponse(pub ServiceGameSummary);
403
404#[derive(Deserialize, Serialize, TS)]
405#[ts(export)]
406pub struct UserGetInfoResponse {
407    pub name: String,
408    pub total_score: i32,
409}
410
411#[derive(Deserialize, Serialize, TS)]
412#[ts(export)]
413pub struct DashboardPlayer {
414    pub id: String,
415    pub name: String,
416    pub created_at: String,
417}
418
419#[derive(Deserialize, Serialize, TS)]
420#[ts(export)]
421pub struct DashboardGame {
422    pub id: String,
423    pub created_at: String,
424    pub updated_at: String,
425}
426
427#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, TS)]
428#[ts(export)]
429pub enum AuthProvider {
430    Anonymous,
431    Email,
432    Github,
433}
434
435#[derive(Deserialize, Serialize, Clone, TS)]
436#[ts(export)]
437pub struct AuthInfoSummary {
438    pub provider: AuthProvider,
439    pub username: Option<String>,
440}
441
442#[derive(Deserialize, Serialize, TS)]
443#[ts(export)]
444pub struct UserPatchInfoRequest {
445    pub name: String,
446}
447#[derive(Deserialize, Serialize, TS)]
448#[ts(export)]
449pub struct UserPatchInfoResponse {
450    pub name: String,
451    pub total_score: i32,
452}
453
454#[derive(Deserialize, Serialize, TS)]
455#[ts(export)]
456pub struct GetDeckResponse(pub DeckContent);
457
458impl Display for AuthProvider {
459    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
460        let result = match self {
461            Self::Anonymous => "anonymous".to_string(),
462            Self::Email => "email".to_string(),
463            Self::Github => "github".to_string(),
464        };
465
466        write!(f, "{}", result)
467    }
468}