Add end game conditions
Also fix some UI bugs
This commit is contained in:
parent
0d30ac0b46
commit
e0fe22e9ce
6 changed files with 302 additions and 77 deletions
139
src/game.rs
139
src/game.rs
|
@ -1,3 +1,5 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use rand::prelude::SliceRandom;
|
use rand::prelude::SliceRandom;
|
||||||
use rand::rngs::SmallRng;
|
use rand::rngs::SmallRng;
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
|
@ -123,6 +125,18 @@ impl PlayerStates {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Tsify, Debug, Clone)]
|
||||||
|
#[tsify(from_wasm_abi)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum GameState {
|
||||||
|
InProgress,
|
||||||
|
Ended {
|
||||||
|
finisher: Option<String>,
|
||||||
|
remaining_tiles: HashMap<String, Vec<Letter>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Game{
|
pub struct Game{
|
||||||
pub tile_pool: Vec<Letter>,
|
pub tile_pool: Vec<Letter>,
|
||||||
rng: SmallRng,
|
rng: SmallRng,
|
||||||
|
@ -130,6 +144,8 @@ pub struct Game{
|
||||||
pub player_states: PlayerStates,
|
pub player_states: PlayerStates,
|
||||||
dictionary: DictionaryImpl,
|
dictionary: DictionaryImpl,
|
||||||
turn_order: usize,
|
turn_order: usize,
|
||||||
|
turns_not_played: usize,
|
||||||
|
state: GameState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -183,6 +199,8 @@ impl Game {
|
||||||
player_states: PlayerStates(player_states),
|
player_states: PlayerStates(player_states),
|
||||||
dictionary,
|
dictionary,
|
||||||
turn_order: 0,
|
turn_order: 0,
|
||||||
|
turns_not_played: 0,
|
||||||
|
state: GameState::InProgress,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,7 +212,7 @@ impl Game {
|
||||||
self.board = new_board;
|
self.board = new_board;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_trays(&mut self){
|
fn fill_trays(&mut self){
|
||||||
for state in self.player_states.0.iter_mut() {
|
for state in self.player_states.0.iter_mut() {
|
||||||
let tray = &mut state.tray;
|
let tray = &mut state.tray;
|
||||||
tray.fill(&mut self.tile_pool);
|
tray.fill(&mut self.tile_pool);
|
||||||
|
@ -205,7 +223,15 @@ impl Game {
|
||||||
&self.dictionary
|
&self.dictionary
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn receive_play(&mut self, tray_tile_locations: Vec<Option<PlayedTile>>, commit_move: bool) -> Result<TurnAction, String> {
|
fn verify_game_in_progress(&self) -> Result<(), String> {
|
||||||
|
if !matches!(self.state, GameState::InProgress) {
|
||||||
|
return Err("Moves cannot be made after a game has finished".to_string());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receive_play(&mut self, tray_tile_locations: Vec<Option<PlayedTile>>, commit_move: bool) -> Result<(TurnAction, GameState), String> {
|
||||||
|
self.verify_game_in_progress()?;
|
||||||
|
|
||||||
let player = self.current_player_name();
|
let player = self.current_player_name();
|
||||||
|
|
||||||
|
@ -244,18 +270,26 @@ impl Game {
|
||||||
self.set_board(board_instance);
|
self.set_board(board_instance);
|
||||||
self.fill_trays();
|
self.fill_trays();
|
||||||
|
|
||||||
self.increment_turn();
|
self.increment_turn(true);
|
||||||
|
|
||||||
|
if self.get_player_tile_count(&player).unwrap() == 0 {
|
||||||
|
// game is over
|
||||||
|
self.end_game(Some(player));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(TurnAction::PlayTiles {
|
}
|
||||||
|
|
||||||
|
Ok((TurnAction::PlayTiles {
|
||||||
result: ScoreResult {
|
result: ScoreResult {
|
||||||
words,
|
words,
|
||||||
total: total_score,
|
total: total_score,
|
||||||
},
|
},
|
||||||
})
|
}, self.state.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exchange_tiles(&mut self, tray_tile_locations: Vec<bool>) -> Result<(Tray, TurnAction), String> {
|
pub fn exchange_tiles(&mut self, tray_tile_locations: Vec<bool>) -> Result<(Tray, TurnAction, GameState), String> {
|
||||||
|
self.verify_game_in_progress()?;
|
||||||
|
|
||||||
let player = self.current_player_name();
|
let player = self.current_player_name();
|
||||||
let tray = match self.player_states.get_tray_mut(&player) {
|
let tray = match self.player_states.get_tray_mut(&player) {
|
||||||
None => {return Err(format!("Player {} not found", player))}
|
None => {return Err(format!("Player {} not found", player))}
|
||||||
|
@ -285,9 +319,9 @@ impl Game {
|
||||||
|
|
||||||
let tray = tray.clone();
|
let tray = tray.clone();
|
||||||
|
|
||||||
self.increment_turn();
|
let state = self.increment_turn(false);
|
||||||
|
|
||||||
Ok((tray, TurnAction::ExchangeTiles { tiles_exchanged }))
|
Ok((tray, TurnAction::ExchangeTiles { tiles_exchanged }, state.clone()))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,15 +331,66 @@ impl Game {
|
||||||
self.dictionary.insert(word, -1.0);
|
self.dictionary.insert(word, -1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn increment_turn(&mut self) {
|
pub fn pass(&mut self) -> Result<GameState, String> {
|
||||||
|
self.verify_game_in_progress()?;
|
||||||
|
Ok(self.increment_turn(false).clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_turn(&mut self, played: bool) -> &GameState{
|
||||||
self.turn_order += 1;
|
self.turn_order += 1;
|
||||||
|
if !played {
|
||||||
|
self.turns_not_played += 1;
|
||||||
|
|
||||||
|
// check if game has ended due to passing
|
||||||
|
if self.turns_not_played >= 2*self.player_states.0.len() {
|
||||||
|
self.end_game(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
self.turns_not_played = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_game(&mut self, finisher: Option<String>) {
|
||||||
|
|
||||||
|
let mut finished_letters_map = HashMap::new();
|
||||||
|
let mut points_forfeit = 0;
|
||||||
|
|
||||||
|
for player in self.player_states.0.iter_mut() {
|
||||||
|
let tray = &mut player.tray;
|
||||||
|
let mut letters_remaining = Vec::new();
|
||||||
|
let mut player_points_lost = 0;
|
||||||
|
for letter in tray.letters.iter_mut() {
|
||||||
|
if let Some(letter) = letter {
|
||||||
|
player_points_lost += letter.points;
|
||||||
|
letters_remaining.push(letter.clone());
|
||||||
|
}
|
||||||
|
*letter = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
points_forfeit += player_points_lost;
|
||||||
|
player.score -= player_points_lost;
|
||||||
|
finished_letters_map.insert(player.player.get_name().to_string(), letters_remaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(finisher) = &finisher {
|
||||||
|
let mut state = self.player_states.get_player_state_mut(finisher).unwrap();
|
||||||
|
state.score += points_forfeit;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = GameState::Ended {
|
||||||
|
finisher,
|
||||||
|
remaining_tiles: finished_letters_map
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_player_name(&self) -> String {
|
pub fn current_player_name(&self) -> String {
|
||||||
self.player_states.get_player_name_by_turn_id(self.turn_order).to_string()
|
self.player_states.get_player_name_by_turn_id(self.turn_order).to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn advance_turn(&mut self) -> Result<TurnAdvanceResult, String> {
|
pub fn advance_turn(&mut self) -> Result<(TurnAdvanceResult, GameState), String> {
|
||||||
let current_player = self.current_player_name();
|
let current_player = self.current_player_name();
|
||||||
let state = self.player_states.get_player_state_mut(¤t_player).ok_or("There should be a player available")?;
|
let state = self.player_states.get_player_state_mut(¤t_player).ok_or("There should be a player available")?;
|
||||||
|
|
||||||
|
@ -330,32 +415,32 @@ impl Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.tile_pool.is_empty(){
|
if self.tile_pool.is_empty(){
|
||||||
self.increment_turn();
|
let game_state = self.increment_turn(false);
|
||||||
Ok(TurnAdvanceResult::AIMove {
|
Ok((TurnAdvanceResult::AIMove {
|
||||||
name: current_player,
|
name: current_player,
|
||||||
action: TurnAction::Pass,
|
action: TurnAction::Pass,
|
||||||
})
|
}, game_state.clone()))
|
||||||
} else {
|
} else {
|
||||||
let (_, action) = self.exchange_tiles(to_exchange)?;
|
let (_, action, game_state) = self.exchange_tiles(to_exchange)?;
|
||||||
Ok(TurnAdvanceResult::AIMove {
|
Ok((TurnAdvanceResult::AIMove {
|
||||||
name: current_player,
|
name: current_player,
|
||||||
action,
|
action,
|
||||||
})
|
}, game_state))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Some(best_move) => {
|
Some(best_move) => {
|
||||||
let play = best_move.convert_to_play(tray);
|
let play = best_move.convert_to_play(tray);
|
||||||
let action = self.receive_play(play, true)?;
|
let (action, game_state) = self.receive_play(play, true)?;
|
||||||
Ok(TurnAdvanceResult::AIMove {
|
Ok((TurnAdvanceResult::AIMove {
|
||||||
name: current_player,
|
name: current_player,
|
||||||
action,
|
action,
|
||||||
})
|
}, game_state))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(TurnAdvanceResult::HumanInputRequired{name: self.current_player_name()})
|
Ok((TurnAdvanceResult::HumanInputRequired{name: self.current_player_name()}, self.state.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -372,12 +457,12 @@ impl Game {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
tray.letters.iter()
|
tray.count()
|
||||||
.filter(|l| l.is_some())
|
|
||||||
.count()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Tsify, Debug)]
|
#[derive(Serialize, Deserialize, Tsify, Debug)]
|
||||||
|
@ -426,13 +511,17 @@ mod tests {
|
||||||
println!("Current player is {current_player}");
|
println!("Current player is {current_player}");
|
||||||
assert_eq!(current_player, "Player");
|
assert_eq!(current_player, "Player");
|
||||||
|
|
||||||
game.increment_turn();
|
assert_eq!(0, game.turns_not_played);
|
||||||
assert_eq!(game.current_player_name(), "AI 0");
|
game.increment_turn(false);
|
||||||
|
assert_eq!(1, game.turns_not_played);
|
||||||
|
|
||||||
|
assert_eq!(game.current_player_name(), "AI");
|
||||||
|
|
||||||
let result = game.advance_turn();
|
let result = game.advance_turn();
|
||||||
println!("AI move is {result:?}");
|
println!("AI move is {result:?}");
|
||||||
|
|
||||||
assert_eq!(game.current_player_name(), "Player");
|
assert_eq!(game.current_player_name(), "Player");
|
||||||
|
assert_eq!(0, game.turns_not_played);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,12 @@ impl Tray {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn count(&self) -> usize {
|
||||||
|
self.letters.iter()
|
||||||
|
.filter(|l| l.is_some())
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
56
src/wasm.rs
56
src/wasm.rs
|
@ -4,7 +4,7 @@ use tsify::Tsify;
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
use crate::board::{CellType, Letter};
|
use crate::board::{CellType, Letter};
|
||||||
use crate::game::{Game, PlayedTile};
|
use crate::game::{Game, GameState, PlayedTile};
|
||||||
use crate::player_interaction::ai::Difficulty;
|
use crate::player_interaction::ai::Difficulty;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
@ -22,10 +22,12 @@ pub enum ResponseType {
|
||||||
pub struct MyResult<E: Serialize> {
|
pub struct MyResult<E: Serialize> {
|
||||||
response_type: ResponseType,
|
response_type: ResponseType,
|
||||||
value: E,
|
value: E,
|
||||||
|
game_state: Option<GameState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
impl GameWasm {
|
impl GameWasm {
|
||||||
|
|
||||||
|
@ -69,16 +71,18 @@ impl GameWasm {
|
||||||
let result = self.0.receive_play(tray_tile_locations, commit_move);
|
let result = self.0.receive_play(tray_tile_locations, commit_move);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(x) => {
|
Ok((x, game_state)) => {
|
||||||
serde_wasm_bindgen::to_value(&MyResult {
|
serde_wasm_bindgen::to_value(&MyResult {
|
||||||
response_type: ResponseType::OK,
|
response_type: ResponseType::OK,
|
||||||
value: x
|
value: x,
|
||||||
|
game_state: Some(game_state)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
serde_wasm_bindgen::to_value(&MyResult {
|
serde_wasm_bindgen::to_value(&MyResult {
|
||||||
response_type: ResponseType::ERR,
|
response_type: ResponseType::ERR,
|
||||||
value: e
|
value: e,
|
||||||
|
game_state: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,16 +117,18 @@ impl GameWasm {
|
||||||
let tray_tile_locations: Vec<bool> = serde_wasm_bindgen::from_value(tray_tile_locations)?;
|
let tray_tile_locations: Vec<bool> = serde_wasm_bindgen::from_value(tray_tile_locations)?;
|
||||||
|
|
||||||
match self.0.exchange_tiles(tray_tile_locations) {
|
match self.0.exchange_tiles(tray_tile_locations) {
|
||||||
Ok((_, turn_action)) => {
|
Ok((_, turn_action, state)) => {
|
||||||
serde_wasm_bindgen::to_value(&MyResult {
|
serde_wasm_bindgen::to_value(&MyResult {
|
||||||
response_type: ResponseType::OK,
|
response_type: ResponseType::OK,
|
||||||
value: turn_action
|
value: turn_action,
|
||||||
|
game_state: Some(state),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
serde_wasm_bindgen::to_value(&MyResult {
|
serde_wasm_bindgen::to_value(&MyResult {
|
||||||
response_type: ResponseType::ERR,
|
response_type: ResponseType::ERR,
|
||||||
value: e
|
value: e,
|
||||||
|
game_state: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,17 +140,43 @@ impl GameWasm {
|
||||||
self.0.add_word(word);
|
self.0.add_word(word);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn skip_turn(&mut self) {self.0.increment_turn()}
|
pub fn skip_turn(&mut self) -> Result<JsValue, Error>{
|
||||||
|
let result = self.0.pass();
|
||||||
|
match result {
|
||||||
|
Ok(game_state) => {
|
||||||
|
Ok(serde_wasm_bindgen::to_value(&MyResult {
|
||||||
|
response_type: ResponseType::OK,
|
||||||
|
value: "Turn passed",
|
||||||
|
game_state: Some(game_state),
|
||||||
|
})?)
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
Ok(serde_wasm_bindgen::to_value(&MyResult {
|
||||||
|
response_type: ResponseType::ERR,
|
||||||
|
value: e,
|
||||||
|
game_state: None,
|
||||||
|
})?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn advance_turn(&mut self) -> Result<JsValue, Error> {
|
pub fn advance_turn(&mut self) -> Result<JsValue, Error> {
|
||||||
let result = self.0.advance_turn();
|
let result = self.0.advance_turn();
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(x) => {
|
Ok((turn_advance_result, game_state)) => {
|
||||||
Ok(serde_wasm_bindgen::to_value(&x)?)
|
Ok(serde_wasm_bindgen::to_value(&MyResult {
|
||||||
|
response_type: ResponseType::OK,
|
||||||
|
value: turn_advance_result,
|
||||||
|
game_state: Some(game_state),
|
||||||
|
})?)
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
Ok(serde_wasm_bindgen::to_value(&e)?)
|
Ok(serde_wasm_bindgen::to_value(&MyResult {
|
||||||
|
response_type: ResponseType::ERR,
|
||||||
|
value: e,
|
||||||
|
game_state: None,
|
||||||
|
})?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,12 +197,14 @@ impl GameWasm {
|
||||||
Ok(serde_wasm_bindgen::to_value(&MyResult{
|
Ok(serde_wasm_bindgen::to_value(&MyResult{
|
||||||
response_type: ResponseType::OK,
|
response_type: ResponseType::OK,
|
||||||
value: count,
|
value: count,
|
||||||
|
game_state: None,
|
||||||
})?)
|
})?)
|
||||||
},
|
},
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
Ok(serde_wasm_bindgen::to_value(&MyResult{
|
Ok(serde_wasm_bindgen::to_value(&MyResult{
|
||||||
response_type: ResponseType::OK,
|
response_type: ResponseType::OK,
|
||||||
value: msg,
|
value: msg,
|
||||||
|
game_state: None,
|
||||||
})?)
|
})?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
158
ui/src/Game.tsx
158
ui/src/Game.tsx
|
@ -1,6 +1,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
GameWasm,
|
GameState,
|
||||||
|
GameWasm, Letter,
|
||||||
Letter as LetterData,
|
Letter as LetterData,
|
||||||
MyResult,
|
MyResult,
|
||||||
PlayedTile,
|
PlayedTile,
|
||||||
|
@ -38,6 +39,7 @@ export function Game(props: {
|
||||||
return props.wasm.get_board_cell_types();
|
return props.wasm.get_board_cell_types();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [isGameOver, setGameOver] = useState<boolean>(false);
|
||||||
const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
|
const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
|
||||||
|
|
||||||
function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
|
function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
|
||||||
|
@ -89,13 +91,16 @@ export function Game(props: {
|
||||||
function exchangeFunction(selectedArray: Array<boolean>) {
|
function exchangeFunction(selectedArray: Array<boolean>) {
|
||||||
|
|
||||||
const result: MyResult<TurnAction | string> = props.wasm.exchange_tiles(selectedArray);
|
const result: MyResult<TurnAction | string> = props.wasm.exchange_tiles(selectedArray);
|
||||||
console.log({result});
|
|
||||||
|
|
||||||
if(result.response_type === "ERR") {
|
if(result.response_type === "ERR") {
|
||||||
logDispatch(<div><em>{(result.value as string)}</em></div>);
|
logDispatch(<div><em>{(result.value as string)}</em></div>);
|
||||||
} else {
|
} else {
|
||||||
handlePlayerAction(result.value as TurnAction, props.settings.playerName);
|
handlePlayerAction(result.value as TurnAction, props.settings.playerName);
|
||||||
setTurnCount(turnCount + 1);
|
setTurnCount(turnCount + 1);
|
||||||
|
|
||||||
|
if(result.game_state.type === "Ended") {
|
||||||
|
endGame(result.game_state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -105,26 +110,13 @@ export function Game(props: {
|
||||||
logDispatch(<div><em>{word} was added to dictionary.</em></div>);
|
logDispatch(<div><em>{word} was added to dictionary.</em></div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
function runAI() {
|
|
||||||
const result: TurnAdvanceResult = props.wasm.advance_turn();
|
|
||||||
if(result.type == "AIMove"){
|
|
||||||
handlePlayerAction(result.action, props.settings.aiName);
|
|
||||||
} else {
|
|
||||||
// this would be quite surprising
|
|
||||||
console.error({result});
|
|
||||||
}
|
|
||||||
|
|
||||||
setTurnCount(turnCount + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []);
|
const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []);
|
||||||
const [logInfo, logDispatch] = useReducer(addLogInfo, []);
|
const [logInfo, logDispatch] = useReducer(addLogInfo, []);
|
||||||
|
|
||||||
const [turnCount, setTurnCount] = useState<number>(1);
|
const [turnCount, setTurnCount] = useState<number>(1);
|
||||||
const playerAndScores: PlayerAndScore[] = useMemo(() => {
|
const playerAndScores: PlayerAndScore[] = useMemo(() => {
|
||||||
return props.wasm.get_scores();
|
return props.wasm.get_scores();
|
||||||
}, [turnCount]);
|
}, [turnCount, isGameOver]);
|
||||||
|
|
||||||
const [boardLetters, setBoardLetters] = useState<HighlightableLetterData[]>(() => {
|
const [boardLetters, setBoardLetters] = useState<HighlightableLetterData[]>(() => {
|
||||||
const newLetterData = [] as HighlightableLetterData[];
|
const newLetterData = [] as HighlightableLetterData[];
|
||||||
|
@ -156,18 +148,6 @@ export function Game(props: {
|
||||||
return props.wasm.get_current_player();
|
return props.wasm.get_current_player();
|
||||||
}, [turnCount]);
|
}, [turnCount]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
logDispatch(<h4>Turn {turnCount}</h4>);
|
|
||||||
logDispatch(<div>{playerTurnName}'s turn</div>)
|
|
||||||
setConfirmedScorePoints(-1);
|
|
||||||
trayDispatch({action: TileDispatchActionType.RETRIEVE});
|
|
||||||
if(playerTurnName != props.settings.playerName) {
|
|
||||||
runAI();
|
|
||||||
}
|
|
||||||
|
|
||||||
}, [turnCount]);
|
|
||||||
|
|
||||||
const logDivRef = useRef(null);
|
const logDivRef = useRef(null);
|
||||||
|
|
||||||
const [isTileExchangeOpen, setIsTileExchangeOpen] = useState<boolean>(false);
|
const [isTileExchangeOpen, setIsTileExchangeOpen] = useState<boolean>(false);
|
||||||
|
@ -181,7 +161,7 @@ export function Game(props: {
|
||||||
|
|
||||||
const remainingTiles = useMemo(() => {
|
const remainingTiles = useMemo(() => {
|
||||||
return props.wasm.get_remaining_tiles();
|
return props.wasm.get_remaining_tiles();
|
||||||
}, [turnCount]);
|
}, [turnCount, isGameOver]);
|
||||||
|
|
||||||
const remainingAITiles = useMemo(() => {
|
const remainingAITiles = useMemo(() => {
|
||||||
let result = props.wasm.get_player_tile_count(props.settings.aiName) as MyResult<number | String>;
|
let result = props.wasm.get_player_tile_count(props.settings.aiName) as MyResult<number | String>;
|
||||||
|
@ -192,7 +172,7 @@ export function Game(props: {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [turnCount]);
|
}, [turnCount, isGameOver]);
|
||||||
|
|
||||||
function handlePlayerAction(action: TurnAction, playerName: string) {
|
function handlePlayerAction(action: TurnAction, playerName: string) {
|
||||||
|
|
||||||
|
@ -211,6 +191,110 @@ export function Game(props: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function endGame(state: GameState) {
|
||||||
|
|
||||||
|
if(state.type != "InProgress") {
|
||||||
|
|
||||||
|
setGameOver(true);
|
||||||
|
logDispatch(<h4>Scoring</h4>);
|
||||||
|
|
||||||
|
const scores = props.wasm.get_scores() as PlayerAndScore[];
|
||||||
|
let pointsBonus = 0;
|
||||||
|
for(const playerAndScore of scores) {
|
||||||
|
const name = playerAndScore.name;
|
||||||
|
|
||||||
|
if(name == state.finisher) {
|
||||||
|
// we'll do the finisher last
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const letters = state.remaining_tiles.get(name);
|
||||||
|
if(letters.length == 0) {
|
||||||
|
logDispatch(<div>{name} has no remaining tiles.</div>);
|
||||||
|
} else {
|
||||||
|
let pointsLost = 0;
|
||||||
|
let letterListStr = '';
|
||||||
|
for(let i=0; i<letters.length; i++) {
|
||||||
|
const letter = letters[i];
|
||||||
|
const letterText = letter.is_blank ? 'a blank' : letter.text;
|
||||||
|
pointsLost += letter.points;
|
||||||
|
|
||||||
|
letterListStr += letterText;
|
||||||
|
|
||||||
|
// we're doing a list of 3 or more so add commas
|
||||||
|
if(letters.length > 2) {
|
||||||
|
if(i == letters.length - 2) {
|
||||||
|
letterListStr += ', and ';
|
||||||
|
} else if (i < letters.length - 2) {
|
||||||
|
letterListStr += ', ';
|
||||||
|
}
|
||||||
|
} else if (i == 0 && letters.length == 2){
|
||||||
|
// list of 2
|
||||||
|
letterListStr += ' and ';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
logDispatch(<div>{name} penalized {pointsLost} points for not using {letterListStr}.</div>);
|
||||||
|
pointsBonus += pointsLost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(state.finisher != null) {
|
||||||
|
logDispatch(<div>{state.finisher} receives {pointsBonus} bonus for completing first.</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
const highestScore = scores
|
||||||
|
.map((score) => score.score)
|
||||||
|
.sort((a, b) => b - a)
|
||||||
|
.at(0);
|
||||||
|
|
||||||
|
const playersAtHighest = scores.filter((score) => score.score == highestScore);
|
||||||
|
let endGameMsg: string = '';
|
||||||
|
|
||||||
|
if(playersAtHighest.length > 1 && state.finisher == null) {
|
||||||
|
endGameMsg = "Tie game!";
|
||||||
|
} else if (playersAtHighest.length > 1 && state.finisher != null) {
|
||||||
|
// if there's a tie then the finisher gets the win
|
||||||
|
endGameMsg = `${playersAtHighest[0].name} won by finishing first!`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
endGameMsg = `${playersAtHighest[0].name} won!`;
|
||||||
|
}
|
||||||
|
logDispatch(<h4>Game over - {endGameMsg}</h4>);
|
||||||
|
} else {
|
||||||
|
// what are we doing in this function?!
|
||||||
|
console.error("endGame was called despite the state being InProgress!");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function runAI() {
|
||||||
|
const result: MyResult<TurnAdvanceResult> = props.wasm.advance_turn();
|
||||||
|
if(result.response_type === "OK" && result.value.type == "AIMove") {
|
||||||
|
handlePlayerAction(result.value.action, props.settings.aiName);
|
||||||
|
if(result.game_state.type === "Ended") {
|
||||||
|
endGame(result.game_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// this would be quite surprising
|
||||||
|
console.error({result});
|
||||||
|
}
|
||||||
|
|
||||||
|
setTurnCount(turnCount + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logDispatch(<h4>Turn {turnCount}</h4>);
|
||||||
|
logDispatch(<div>{playerTurnName}'s turn</div>)
|
||||||
|
setConfirmedScorePoints(-1);
|
||||||
|
trayDispatch({action: TileDispatchActionType.RETRIEVE});
|
||||||
|
if(playerTurnName != props.settings.playerName && !isGameOver) {
|
||||||
|
runAI();
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [turnCount]);
|
||||||
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<TileExchangeModal
|
<TileExchangeModal
|
||||||
|
@ -236,7 +320,9 @@ export function Game(props: {
|
||||||
<div>
|
<div>
|
||||||
{props.settings.aiName} has {remainingAITiles} tiles
|
{props.settings.aiName} has {remainingAITiles} tiles
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => {
|
<button
|
||||||
|
disabled={remainingTiles == 0}
|
||||||
|
onClick={() => {
|
||||||
trayDispatch({action: TileDispatchActionType.RETURN}); // want all tiles back on tray for tile exchange
|
trayDispatch({action: TileDispatchActionType.RETURN}); // want all tiles back on tray for tile exchange
|
||||||
setIsTileExchangeOpen(true);
|
setIsTileExchangeOpen(true);
|
||||||
}}>Open Tile Exchange</button>
|
}}>Open Tile Exchange</button>
|
||||||
|
@ -289,6 +375,10 @@ export function Game(props: {
|
||||||
setTurnCount(turnCount + 1);
|
setTurnCount(turnCount + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.game_state.type === "Ended") {
|
||||||
|
endGame(result.game_state);
|
||||||
|
}
|
||||||
|
|
||||||
setConfirmedScorePoints(total_points);
|
setConfirmedScorePoints(total_points);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,9 +389,13 @@ export function Game(props: {
|
||||||
trayDispatch({action: TileDispatchActionType.RETURN});
|
trayDispatch({action: TileDispatchActionType.RETURN});
|
||||||
}}>Return Tiles</button>
|
}}>Return Tiles</button>
|
||||||
<button className="pass" onClick={() => {
|
<button className="pass" onClick={() => {
|
||||||
props.wasm.skip_turn();
|
const result = props.wasm.skip_turn() as MyResult<string>;
|
||||||
handlePlayerAction({type: "Pass"}, props.settings.playerName);
|
handlePlayerAction({type: "Pass"}, props.settings.playerName);
|
||||||
setTurnCount(turnCount + 1);
|
setTurnCount(turnCount + 1);
|
||||||
|
|
||||||
|
if(result.game_state.type === "Ended") {
|
||||||
|
endGame(result.game_state);
|
||||||
|
}
|
||||||
}}>Pass</button>
|
}}>Pass</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -96,8 +96,10 @@ function TilesExchangedTray(props: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="tray">
|
return <div className="tray-container">
|
||||||
|
<div className="tray">
|
||||||
{divContent}
|
{divContent}
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,7 @@
|
||||||
@board-length: 15;
|
@board-length: 15;
|
||||||
@tile-star-size: 45px;
|
@tile-star-size: 45px;
|
||||||
|
|
||||||
.tray-row {
|
.tray { // Don't put under tray-row as this also gets used in tile exchange
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
width: @board-length*@tile-width;
|
|
||||||
|
|
||||||
.tray {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, @tile-width);
|
grid-template-columns: repeat(7, @tile-width);
|
||||||
grid-gap: 5px;
|
grid-gap: 5px;
|
||||||
|
@ -18,6 +13,11 @@
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tray-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
width: @board-length*@tile-width;
|
||||||
|
|
||||||
.player-controls {
|
.player-controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: "check return"
|
grid-template-areas: "check return"
|
||||||
|
|
Loading…
Reference in a new issue