diff --git a/client_web/assets/style.css b/client_web/assets/style.css index 3691894..9b80fbb 100644 --- a/client_web/assets/style.css +++ b/client_web/assets/style.css @@ -1131,3 +1131,63 @@ body { flex-wrap: wrap; min-height: 2rem; /* reserve height so layout doesn't shift when buttons appear */ } + +/* ── Pre-game ceremony overlay ──────────────────────────────────────────── */ +.ceremony-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.65); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.ceremony-box { + background: var(--ui-parchment); + border-radius: 8px; + padding: 2.5rem 3rem; + text-align: center; + box-shadow: 0 12px 40px rgba(0,0,0,0.5), 0 0 0 2px var(--ui-gold-dark); + display: flex; + flex-direction: column; + align-items: center; + gap: 1.4rem; + min-width: 300px; + animation: game-over-appear 0.4s cubic-bezier(0.22, 0.61, 0.36, 1); +} + +.ceremony-box h2 { + font-family: var(--font-display); + font-size: 1.8rem; + font-weight: 600; + color: var(--ui-ink); + letter-spacing: 0.06em; +} + +.ceremony-dice { + display: flex; + gap: 3rem; + align-items: flex-end; +} + +.ceremony-die-slot { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.ceremony-die-label { + font-family: var(--font-ui); + font-size: 0.85rem; + color: var(--ui-ink); + font-weight: 500; +} + +.ceremony-tie { + font-family: var(--font-display); + font-size: 1rem; + color: var(--ui-red-accent); + font-style: italic; +} diff --git a/client_web/locales/en.json b/client_web/locales/en.json index f390ce4..c29121d 100644 --- a/client_web/locales/en.json +++ b/client_web/locales/en.json @@ -42,6 +42,12 @@ "after_opponent_roll": "Opponent rolled", "after_opponent_go": "Opponent chose to continue", "after_opponent_move": "Opponent moved — your turn", + "after_opponent_pre_game_roll": "Opponent rolled — your turn", + "pre_game_roll_title": "Who goes first?", + "pre_game_roll_btn": "Roll", + "pre_game_roll_tie": "Tie! Roll again", + "pre_game_roll_your_die": "Your die", + "pre_game_roll_opp_die": "Opponent's die", "continue_btn": "Continue", "scored_pts": "+{{ n }} pts", "hole_made": "Hole! {{ holes }}/12", diff --git a/client_web/locales/fr.json b/client_web/locales/fr.json index 910d7c0..93f76e5 100644 --- a/client_web/locales/fr.json +++ b/client_web/locales/fr.json @@ -42,6 +42,12 @@ "after_opponent_roll": "L'adversaire a lancé les dés", "after_opponent_go": "L'adversaire s'en va", "after_opponent_move": "L'adversaire a joué — à vous", + "after_opponent_pre_game_roll": "L'adversaire a lancé — à vous", + "pre_game_roll_title": "Qui joue en premier ?", + "pre_game_roll_btn": "Lancer", + "pre_game_roll_tie": "Égalité ! Relancez", + "pre_game_roll_your_die": "Votre dé", + "pre_game_roll_opp_die": "Dé adverse", "continue_btn": "Continuer", "scored_pts": "+{{ n }} pts", "hole_made": "Trou ! {{ holes }}/12", diff --git a/client_web/src/app.rs b/client_web/src/app.rs index aa86ca8..cebdb17 100644 --- a/client_web/src/app.rs +++ b/client_web/src/app.rs @@ -13,7 +13,7 @@ use crate::i18n::I18nContextProvider; use crate::trictrac::backend::TrictracBackend; use crate::trictrac::bot_local::bot_decide; use crate::trictrac::types::{ - GameDelta, JanEntry, PlayerAction, ScoredEvent, SerTurnStage, ViewState, + GameDelta, JanEntry, PlayerAction, ScoredEvent, SerStage, SerTurnStage, ViewState, }; use trictrac_store::CheckerMove; @@ -48,6 +48,8 @@ pub enum PauseReason { AfterOpponentRoll, AfterOpponentGo, AfterOpponentMove, + /// Opponent rolled their die in the pre-game ceremony. + AfterOpponentPreGameRoll, } /// Which screen is currently shown. @@ -382,32 +384,35 @@ async fn run_local_bot_game( } loop { - match bot_decide(backend.get_game()) { + let pgr = backend.get_view_state().pre_game_roll.clone(); + match bot_decide(backend.get_game(), pgr.as_ref()) { None => break, Some(action) => { - let prev_vs = vs.clone(); backend.inform_rpc(1, action); + // Process each delta individually so intermediate ceremony + // states (both dice shown) can trigger a pause via push_or_show. for cmd in backend.drain_commands() { if let BackendCommand::Delta(delta) = cmd { + let delta_prev_vs = vs.clone(); vs.apply_delta(&delta); + push_or_show( + &delta_prev_vs, + GameUiState { + view_state: vs.clone(), + player_id: 0, + room_id: String::new(), + is_bot_game: true, + waiting_for_confirm: false, + pause_reason: None, + my_scored_event: None, + opp_scored_event: None, + last_moves: compute_last_moves(&delta_prev_vs, &vs), + }, + pending, + screen, + ); } } - push_or_show( - &prev_vs, - GameUiState { - view_state: vs.clone(), - player_id: 0, - room_id: String::new(), - is_bot_game: true, - waiting_for_confirm: false, - pause_reason: None, - my_scored_event: None, - opp_scored_event: None, - last_moves: compute_last_moves(&prev_vs, &vs), - }, - pending, - screen, - ); } } } @@ -530,6 +535,24 @@ fn push_or_show( fn infer_pause_reason(prev: &ViewState, next: &ViewState, player_id: u16) -> Option { let opponent_id = 1 - player_id; + // Pre-game ceremony: pause when both dice are revealed simultaneously + // (i.e. the second die was just rolled). Both players see this pause. + if next.stage == SerStage::PreGameRoll { + if let (Some(prev_pgr), Some(next_pgr)) = (&prev.pre_game_roll, &next.pre_game_roll) { + let both_now = next_pgr.host_die.is_some() && next_pgr.guest_die.is_some(); + let both_before = prev_pgr.host_die.is_some() && prev_pgr.guest_die.is_some(); + if both_now && !both_before { + return Some(PauseReason::AfterOpponentPreGameRoll); + } + } + return None; + } + + // Don't fire normal pause rules on the PreGameRoll → InGame transition. + if prev.stage == SerStage::PreGameRoll { + return None; + } + if next.active_mp_player == Some(opponent_id) { // Dice changed → opponent just rolled. if next.dice != prev.dice { @@ -574,6 +597,7 @@ mod tests { dice, dice_jans: Vec::new(), dice_moves: (CheckerMove::default(), CheckerMove::default()), + pre_game_roll: None, } } diff --git a/client_web/src/components/game_screen.rs b/client_web/src/components/game_screen.rs index 4be98b8..a94194f 100644 --- a/client_web/src/components/game_screen.rs +++ b/client_web/src/components/game_screen.rs @@ -7,7 +7,8 @@ use trictrac_store::{Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, use crate::app::{GameUiState, NetCommand, PauseReason}; use crate::i18n::*; -use crate::trictrac::types::{PlayerAction, SerStage, SerTurnStage}; +use crate::trictrac::types::{PlayerAction, PreGameRollState, SerStage, SerTurnStage}; +use super::die::Die; use super::board::Board; use super::score_panel::PlayerScorePanel; @@ -78,7 +79,11 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { // Guard: suppressed while waiting_for_confirm — the AfterOpponentMove // buffered state shows the human's RollDice turn but the auto-roll must // wait until the buffer is drained and the live screen state is shown. - let show_roll = is_my_turn && vs.turn_stage == SerTurnStage::RollDice; + // Guard: never auto-roll during the pre-game ceremony (the ceremony overlay + // has its own Roll button for PlayerAction::PreGameRoll). + let show_roll = is_my_turn + && vs.turn_stage == SerTurnStage::RollDice + && vs.stage != SerStage::PreGameRoll; if show_roll && !waiting_for_confirm { let cmd_tx_auto = cmd_tx.clone(); Effect::new(move |_| { @@ -132,6 +137,13 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { let my_score = vs.scores[player_id as usize].clone(); let opp_score = vs.scores[1 - player_id as usize].clone(); + // ── Ceremony state (extracted before vs is moved into Board) ──────────────── + let is_ceremony = vs.stage == SerStage::PreGameRoll; + let pre_game_roll_data: Option = vs.pre_game_roll.clone(); + let my_name_ceremony = my_score.name.clone(); + let opp_name_ceremony = opp_score.name.clone(); + let cmd_tx_ceremony = cmd_tx.clone(); + // ── Scoring notifications ────────────────────────────────────────────────── let my_scored_event = state.my_scored_event.clone(); let opp_scored_event = state.opp_scored_event.clone(); @@ -246,6 +258,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { PauseReason::AfterOpponentRoll => t_string!(i18n, after_opponent_roll), PauseReason::AfterOpponentGo => t_string!(i18n, after_opponent_go), PauseReason::AfterOpponentMove => t_string!(i18n, after_opponent_move), + PauseReason::AfterOpponentPreGameRoll => t_string!(i18n, after_opponent_pre_game_roll), }); } let n = staged_moves.get().len(); @@ -254,7 +267,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { } else { String::from(match (&stage, is_my_turn, &turn_stage) { (SerStage::Ended, _, _) => t_string!(i18n, game_over), - (SerStage::PreGame, _, _) => t_string!(i18n, waiting_for_opponent), + (SerStage::PreGame, _, _) | (SerStage::PreGameRoll, _, _) => t_string!(i18n, waiting_for_opponent), (SerStage::InGame, true, SerTurnStage::RollDice) => t_string!(i18n, your_turn_roll), (SerStage::InGame, true, SerTurnStage::HoldOrGoChoice) => t_string!(i18n, hold_or_go), (SerStage::InGame, true, _) => t_string!(i18n, your_turn), @@ -352,6 +365,55 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { // ── Player score (below board) ──────────────────────────────────── + // ── Pre-game ceremony overlay ───────────────────────────────────── + {is_ceremony.then(|| { + let pgr = pre_game_roll_data.unwrap_or(PreGameRollState { + host_die: None, + guest_die: None, + tie_count: 0, + }); + let my_die = if player_id == 0 { pgr.host_die } else { pgr.guest_die }; + let opp_die = if player_id == 0 { pgr.guest_die } else { pgr.host_die }; + let can_roll = is_my_turn && !waiting_for_confirm; + let show_tie = pgr.tie_count > 0; + view! { +
+
+

{t!(i18n, pre_game_roll_title)}

+ {show_tie.then(|| view! { +

{t!(i18n, pre_game_roll_tie)}

+ })} +
+
+ {my_name_ceremony} + +
+
+ {opp_name_ceremony} + +
+
+ {waiting_for_confirm.then(|| { + let pending_c = pending; + view! { + + } + })} + {can_roll.then(|| { + let cmd_tx_c = cmd_tx_ceremony.clone(); + view! { + + } + })} +
+
+ } + })} + // ── Game-over overlay ───────────────────────────────────────────── {stage_is_ended.then(|| { let opp_name_end_clone = opp_name_end.clone(); diff --git a/client_web/src/trictrac/backend.rs b/client_web/src/trictrac/backend.rs index 2e51bd8..288f5e7 100644 --- a/client_web/src/trictrac/backend.rs +++ b/client_web/src/trictrac/backend.rs @@ -1,7 +1,7 @@ use backbone_lib::traits::{BackEndArchitecture, BackendCommand}; use trictrac_store::{DiceRoller, GameEvent, GameState, TurnStage}; -use crate::trictrac::types::{GameDelta, PlayerAction, ViewState}; +use crate::trictrac::types::{GameDelta, PlayerAction, PreGameRollState, SerStage, ViewState}; // Store PlayerId (u64) values used for the two players. const HOST_PLAYER_ID: u64 = 1; @@ -14,11 +14,32 @@ pub struct TrictracBackend { view_state: ViewState, /// Arrival flags: have host (index 0) and guest (index 1) joined? arrived: [bool; 2], + /// Die rolled by each player during the ceremony ([host, guest]). + pre_game_dice: [Option; 2], + /// Number of tied rounds so far. + tie_count: u8, + /// True while the first-player ceremony is running. + ceremony_started: bool, } impl TrictracBackend { fn sync_view_state(&mut self) { - self.view_state = ViewState::from_game_state(&self.game, HOST_PLAYER_ID, GUEST_PLAYER_ID); + let mut vs = ViewState::from_game_state(&self.game, HOST_PLAYER_ID, GUEST_PLAYER_ID); + if self.ceremony_started { + vs.stage = SerStage::PreGameRoll; + vs.pre_game_roll = Some(PreGameRollState { + host_die: self.pre_game_dice[0], + guest_die: self.pre_game_dice[1], + tie_count: self.tie_count, + }); + // The active mp player is whoever hasn't rolled yet (host rolls first). + vs.active_mp_player = match self.pre_game_dice { + [None, _] => Some(0), + [Some(_), None] => Some(1), + _ => None, + }; + } + self.view_state = vs; } fn broadcast_state(&mut self) { @@ -29,6 +50,42 @@ impl TrictracBackend { self.commands.push(BackendCommand::Delta(delta)); } + /// Process one ceremony die-roll for `mp_player` (0 = host, 1 = guest). + fn handle_pre_game_roll(&mut self, mp_player: u16) { + // Enforce turn order: host rolls first, then guest. + let expected: u16 = match self.pre_game_dice { + [None, _] => 0, + [Some(_), None] => 1, + _ => return, // both already rolled (shouldn't happen) + }; + if mp_player != expected { + return; + } + let idx = mp_player as usize; + let single = self.dice_roller.roll().values.0; + self.pre_game_dice[idx] = Some(single); + + if let [Some(h), Some(g)] = self.pre_game_dice { + // Both have rolled — broadcast both dice before resolving. + self.broadcast_state(); + if h == g { + // Tie: reset for another round. + self.tie_count += 1; + self.pre_game_dice = [None; 2]; + self.broadcast_state(); + } else { + // Highest die goes first. + let goes_first = if h > g { HOST_PLAYER_ID } else { GUEST_PLAYER_ID }; + self.ceremony_started = false; + let _ = self.game.consume(&GameEvent::BeginGame { goes_first }); + self.broadcast_state(); + } + } else { + // Only one die rolled so far — broadcast the partial result. + self.broadcast_state(); + } + } + /// Roll dice using the store's DiceRoller and fire Roll + RollResult events. fn do_roll(&mut self) { let dice = self.dice_roller.roll(); @@ -86,6 +143,9 @@ impl BackEndArchitecture for TrictracBackend commands: Vec::new(), view_state, arrived: [false; 2], + pre_game_dice: [None; 2], + tie_count: 0, + ceremony_started: false, } } @@ -110,11 +170,13 @@ impl BackEndArchitecture for TrictracBackend timer_id: mp_player, }); - // Start the game once both players have arrived. - if self.arrived[0] && self.arrived[1] && self.game.stage == trictrac_store::Stage::PreGame { - let _ = self.game.consume(&GameEvent::BeginGame { - goes_first: HOST_PLAYER_ID, - }); + // Start the ceremony once both players have arrived. + if self.arrived[0] && self.arrived[1] && self.game.stage == trictrac_store::Stage::PreGame + && !self.ceremony_started + { + self.ceremony_started = true; + self.pre_game_dice = [None; 2]; + self.tie_count = 0; self.sync_view_state(); self.commands.push(BackendCommand::ResetViewState); } else { @@ -135,6 +197,14 @@ impl BackEndArchitecture for TrictracBackend } fn inform_rpc(&mut self, mp_player: u16, action: PlayerAction) { + // During the first-player ceremony only PreGameRoll actions are accepted. + if self.ceremony_started { + if matches!(action, PlayerAction::PreGameRoll) { + self.handle_pre_game_roll(mp_player); + } + return; + } + if self.game.stage == trictrac_store::Stage::Ended { return; } @@ -167,8 +237,6 @@ impl BackEndArchitecture for TrictracBackend moves: (m1, m2), }; if self.game.validate(&event) { - // let message = format!("Event {:?} validated on {:?}", event, self.game); - // console_log(message); let _ = self.game.consume(&event); self.drive_automatic_stages(); } @@ -188,6 +256,7 @@ impl BackEndArchitecture for TrictracBackend self.drive_automatic_stages(); } } + PlayerAction::PreGameRoll => {} // ignored outside ceremony } self.broadcast_state(); @@ -216,6 +285,7 @@ impl BackEndArchitecture for TrictracBackend mod tests { use super::*; use backbone_lib::traits::BackEndArchitecture; + use crate::trictrac::types::{SerStage, SerTurnStage}; fn make_backend() -> TrictracBackend { TrictracBackend::new(0) @@ -233,28 +303,67 @@ mod tests { .collect() } + /// Drive the ceremony to completion (both players roll until one wins). + fn complete_ceremony(b: &mut TrictracBackend) { + loop { + if b.get_view_state().stage != SerStage::PreGameRoll { + break; + } + match b.get_view_state().active_mp_player { + Some(0) => b.inform_rpc(0, PlayerAction::PreGameRoll), + Some(1) => b.inform_rpc(1, PlayerAction::PreGameRoll), + _ => break, + } + b.drain_commands(); + } + } + #[test] - fn both_players_arrive_starts_game() { + fn both_players_arrive_starts_ceremony() { let mut b = make_backend(); b.player_arrival(0); // host b.drain_commands(); b.player_arrival(1); // guest let cmds = b.drain_commands(); - // ResetViewState should have been issued after BeginGame. + // ResetViewState should have been issued to start the ceremony. let has_reset = cmds .iter() .any(|c| matches!(c, BackendCommand::ResetViewState)); - assert!( - has_reset, - "expected ResetViewState after both players arrive" - ); + assert!(has_reset, "expected ResetViewState after both players arrive"); + + // Stage should now be PreGameRoll, not InGame. + assert_eq!(b.get_view_state().stage, SerStage::PreGameRoll); + } + + #[test] + fn ceremony_resolves_to_in_game() { + let mut b = make_backend(); + b.player_arrival(0); + b.player_arrival(1); + b.drain_commands(); + + complete_ceremony(&mut b); - // Game should now be InGame. - use crate::trictrac::types::SerStage; assert_eq!(b.get_view_state().stage, SerStage::InGame); } + #[test] + fn ceremony_wrong_order_ignored() { + let mut b = make_backend(); + b.player_arrival(0); + b.player_arrival(1); + b.drain_commands(); + + // Guest tries to roll before host (host goes first in ceremony). + b.inform_rpc(1, PlayerAction::PreGameRoll); + let cmds = b.drain_commands(); + assert!( + cmds.is_empty(), + "guest PreGameRoll should be ignored when it is host's turn" + ); + } + #[test] fn unknown_player_kicked() { let mut b = make_backend(); @@ -272,12 +381,15 @@ mod tests { b.player_arrival(1); b.drain_commands(); - // Host rolls (player_id 0, whose store id == HOST_PLAYER_ID == active after BeginGame). - b.inform_rpc(0, PlayerAction::Roll); + // Complete ceremony before rolling. + complete_ceremony(&mut b); + + // Roll for whoever won the ceremony (either player could go first). + let first_player = b.get_view_state().active_mp_player.expect("someone should be active"); + b.inform_rpc(first_player, PlayerAction::Roll); let states = drain_deltas(&mut b); assert!(!states.is_empty(), "expected a state broadcast after roll"); - use crate::trictrac::types::SerTurnStage; let last = states.last().unwrap(); assert!( matches!( @@ -298,13 +410,16 @@ mod tests { b.player_arrival(0); b.player_arrival(1); b.drain_commands(); + complete_ceremony(&mut b); - // Guest tries to roll when it's the host's turn. - b.inform_rpc(1, PlayerAction::Roll); + // Identify who goes first and have the OTHER player try to roll. + let active = b.get_view_state().active_mp_player; + let wrong_player = if active == Some(0) { 1u16 } else { 0u16 }; + b.inform_rpc(wrong_player, PlayerAction::Roll); let cmds = b.drain_commands(); assert!( cmds.is_empty(), - "guest roll should be ignored when it's host's turn" + "wrong player roll should be ignored" ); } diff --git a/client_web/src/trictrac/bot_local.rs b/client_web/src/trictrac/bot_local.rs index d2a0ca3..73658ca 100644 --- a/client_web/src/trictrac/bot_local.rs +++ b/client_web/src/trictrac/bot_local.rs @@ -1,12 +1,22 @@ use rand::prelude::IndexedRandom; use trictrac_store::{CheckerMove, Color, GameState, MoveRules, Stage, TurnStage}; -use crate::trictrac::types::PlayerAction; +use crate::trictrac::types::{PlayerAction, PreGameRollState}; const GUEST_PLAYER_ID: u64 = 2; /// Returns the next action for the bot (mp_player 1 / guest), or None if it is not the bot's turn. -pub fn bot_decide(game: &GameState) -> Option { +/// `pgr` is the current pre-game ceremony state if the ceremony is in progress. +pub fn bot_decide(game: &GameState, pgr: Option<&PreGameRollState>) -> Option { + // During the ceremony, the bot (guest) rolls when its die is missing. + if game.stage == Stage::PreGame { + if let Some(pgr) = pgr { + if pgr.guest_die.is_none() { + return Some(PlayerAction::PreGameRoll); + } + } + return None; + } if game.stage == Stage::Ended { return None; } @@ -15,8 +25,8 @@ pub fn bot_decide(game: &GameState) -> Option { } match game.turn_stage { TurnStage::RollDice => Some(PlayerAction::Roll), - // TurnStage::HoldOrGoChoice => Some(PlayerAction::Go), - TurnStage::Move | TurnStage::HoldOrGoChoice => { + TurnStage::HoldOrGoChoice => Some(PlayerAction::Go), + TurnStage::Move => { let rules = MoveRules::new(&Color::Black, &game.board, game.dice); let sequences = rules.get_possible_moves_sequences(true, vec![]); let mut rng = rand::rng(); diff --git a/client_web/src/trictrac/types.rs b/client_web/src/trictrac/types.rs index f431482..b6f43da 100644 --- a/client_web/src/trictrac/types.rs +++ b/client_web/src/trictrac/types.rs @@ -14,6 +14,8 @@ pub enum PlayerAction { Go, /// Acknowledge point marking (hold / advance points). Mark, + /// Roll a single die during the pre-game ceremony to decide who goes first. + PreGameRoll, } // ── Incremental state update broadcast to all clients ──────────────────────── @@ -27,6 +29,18 @@ pub struct GameDelta { // ── Full game snapshot ──────────────────────────────────────────────────────── +/// State of the pre-game ceremony where each player rolls one die to decide +/// who goes first. Present only when `stage == SerStage::PreGameRoll`. +#[derive(Clone, PartialEq, Serialize, Deserialize)] +pub struct PreGameRollState { + /// Die value (1–6) rolled by the host; `None` = not yet rolled this round. + pub host_die: Option, + /// Die value (1–6) rolled by the guest; `None` = not yet rolled this round. + pub guest_die: Option, + /// Number of tied rounds so far (0 on the first round). + pub tie_count: u8, +} + #[derive(Clone, PartialEq, Serialize, Deserialize)] pub struct ViewState { /// Board positions: index i = field i+1. Positive = white, negative = black. @@ -43,6 +57,9 @@ pub struct ViewState { pub dice_jans: Vec, /// Last two checker moves played; default when no move has occurred yet. pub dice_moves: (CheckerMove, CheckerMove), + /// Present while the pre-game ceremony is in progress. + #[serde(default)] + pub pre_game_roll: Option, } /// One scoring event from a dice roll. @@ -86,6 +103,7 @@ impl ViewState { dice: (0, 0), dice_jans: Vec::new(), dice_moves: (CheckerMove::default(), CheckerMove::default()), + pre_game_roll: None, } } @@ -184,6 +202,7 @@ impl ViewState { dice: (gs.dice.values.0, gs.dice.values.1), dice_jans, dice_moves: gs.dice_moves, + pre_game_roll: None, } } } @@ -220,6 +239,8 @@ pub struct PlayerScore { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SerStage { PreGame, + /// Both players have arrived; ceremony in progress to decide who goes first. + PreGameRoll, InGame, Ended, }