Skip to content

Commit

Permalink
Merge pull request #3 from BakerNet/feature/replays
Browse files Browse the repository at this point in the history
Feature:  Replays
  • Loading branch information
BakerNet authored Aug 28, 2024
2 parents 41a9e1b + 9c9f151 commit 7c57e40
Show file tree
Hide file tree
Showing 15 changed files with 970 additions and 286 deletions.
10 changes: 9 additions & 1 deletion minesweeper-lib/src/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use core::fmt;
use std::{
fmt::{Debug, Display, Formatter},
ops::{Index, IndexMut},
slice::{Iter, IterMut},
slice::{Chunks, ChunksMut, Iter, IterMut},
};

use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -130,6 +130,14 @@ impl<T> Board<T> {
self.board.is_empty()
}

pub fn rows_iter(&self) -> Chunks<T> {
self.board.chunks(self.cols)
}

pub fn rows_iter_mut(&mut self) -> ChunksMut<T> {
self.board.chunks_mut(self.cols)
}

pub fn iter(&self) -> Iter<'_, T> {
self.board.iter()
}
Expand Down
34 changes: 32 additions & 2 deletions minesweeper-lib/src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,11 @@ impl Board<(Cell, CellState)> {

#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct Play {
#[serde(rename = "p", alias = "player")]
pub player: usize,
#[serde(rename = "a", alias = "action")]
pub action: Action,
#[serde(rename = "bp", alias = "point")]
pub point: BoardPoint,
}

Expand Down Expand Up @@ -591,6 +594,10 @@ impl CompletedMinesweeper {
log: Some(log),
}
}

pub fn recover_log(self) -> Option<Vec<(Play, PlayOutcome)>> {
self.log
}
}

impl CompletedMinesweeper {
Expand Down Expand Up @@ -655,7 +662,9 @@ impl CompletedMinesweeper {

fn board_start(&self) -> Board<PlayerCell> {
let mut board = self.board.clone();
board.iter_mut().for_each(|pc| *pc = pc.into_hidden());
board
.iter_mut()
.for_each(|pc| *pc = pc.into_hidden().remove_flag());
board
}

Expand All @@ -674,7 +683,11 @@ impl CompletedMinesweeper {
})
.cloned()
.collect();
Some(MinesweeperReplay::new(self.board_start(), player_log))
Some(MinesweeperReplay::new(
self.board_start(),
player_log,
self.players.len(),
))
}
}

Expand All @@ -696,16 +709,33 @@ pub struct Player {

#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum Action {
#[serde(rename = "f", alias = "Flag")]
Flag,
#[serde(rename = "r", alias = "Reveal")]
Reveal,
#[serde(rename = "ra", alias = "RevealAdjacent")]
RevealAdjacent,
}

impl Action {
pub fn to_str(&self) -> &'static str {
match self {
Action::Flag => "Flag",
Action::Reveal => "Reveal",
Action::RevealAdjacent => "Reveal Adjacent",
}
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum PlayOutcome {
#[serde(rename = "s", alias = "Success")]
Success(Vec<(BoardPoint, RevealedCell)>),
#[serde(rename = "x", alias = "Failure")]
Failure((BoardPoint, RevealedCell)),
#[serde(rename = "v", alias = "Victory")]
Victory(Vec<(BoardPoint, RevealedCell)>),
#[serde(rename = "f", alias = "Flag")]
Flag((BoardPoint, PlayerCell)),
}

Expand Down
140 changes: 116 additions & 24 deletions minesweeper-lib/src/replay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,73 @@ use anyhow::{bail, Result};
use crate::{
board::Board,
cell::{HiddenCell, PlayerCell},
client::ClientPlayer,
game::{Play, PlayOutcome},
};

#[derive(Debug)]
pub enum ReplayPosition {
End,
Beginning,
Other(usize),
}

impl ReplayPosition {
pub fn from_pos(pos: usize, len: usize) -> Self {
match pos {
p if p == len => ReplayPosition::End,
0 => ReplayPosition::Beginning,
default => ReplayPosition::Other(default),
}
}

pub fn to_pos(&self, len: usize) -> usize {
match self {
ReplayPosition::End => len,
ReplayPosition::Beginning => 0,
ReplayPosition::Other(default) => *default,
}
}
}

#[derive(Debug, Default, Clone, Copy)]
pub struct SimplePlayer {
score: usize,
dead: bool,
victory_click: bool,
}

impl SimplePlayer {
pub fn update_client_player(self, cp: &mut ClientPlayer) {
cp.top_score = false;
cp.score = self.score;
cp.dead = self.dead;
cp.victory_click = self.victory_click;
}
}

pub struct MinesweeperReplay {
current_play: Option<Play>,
current_board: Board<PlayerCell>,
current_players: Vec<SimplePlayer>,
current_flags: usize,
current_revealed_mines: usize,
log: Vec<(Play, PlayOutcome)>,
current_pos: usize,
}

impl MinesweeperReplay {
pub fn new(starting_board: Board<PlayerCell>, log: Vec<(Play, PlayOutcome)>) -> Self {
pub fn new(
starting_board: Board<PlayerCell>,
log: Vec<(Play, PlayOutcome)>,
players: usize,
) -> Self {
Self {
current_board: starting_board,
current_play: None,
current_players: vec![SimplePlayer::default(); players],
current_flags: 0,
current_revealed_mines: 0,
log,
current_pos: 0,
}
Expand All @@ -31,8 +83,8 @@ impl MinesweeperReplay {
false
}

pub fn current_pos(&self) -> usize {
self.current_pos
pub fn current_pos(&self) -> ReplayPosition {
ReplayPosition::from_pos(self.current_pos, self.len() - 1)
}

pub fn current_play(&self) -> Option<Play> {
Expand All @@ -43,35 +95,52 @@ impl MinesweeperReplay {
&self.current_board
}

pub fn advance(&mut self) -> Result<()> {
pub fn current_players(&self) -> &Vec<SimplePlayer> {
&self.current_players
}

pub fn current_flags_and_revealed_mines(&self) -> usize {
self.current_flags + self.current_revealed_mines
}

pub fn advance(&mut self) -> Result<ReplayPosition> {
if self.current_pos == self.len() - 1 {
bail!("Called next on end")
}
let play = &self.log[self.current_pos];
self.current_play = Some(play.0);
match &play.1 {
PlayOutcome::Success(results) => results.iter().for_each(|rc| {
self.current_players[rc.1.player].score += 1;
self.current_board[rc.0] = PlayerCell::Revealed(rc.1);
}),
PlayOutcome::Failure(rc) => {
self.current_players[rc.1.player].dead = true;
self.current_revealed_mines += 1;
self.current_board[rc.0] = PlayerCell::Revealed(rc.1);
}
PlayOutcome::Victory(results) => results.iter().for_each(|rc| {
self.current_board[rc.0] = PlayerCell::Revealed(rc.1);
}),
PlayOutcome::Victory(results) => {
self.current_players[results[0].1.player].victory_click = true;
results.iter().for_each(|rc| {
self.current_players[rc.1.player].score += 1;
self.current_board[rc.0] = PlayerCell::Revealed(rc.1);
});
}
PlayOutcome::Flag(res) => {
if matches!(res.1, PlayerCell::Hidden(HiddenCell::Flag)) {
self.current_flags += 1;
self.current_board[res.0] = self.current_board[res.0].add_flag()
} else {
self.current_flags -= 1;
self.current_board[res.0] = self.current_board[res.0].remove_flag()
}
}
};
self.current_pos += 1;
Ok(())
Ok(self.current_pos())
}

pub fn rewind(&mut self) -> Result<()> {
pub fn rewind(&mut self) -> Result<ReplayPosition> {
if self.current_pos == 0 {
bail!("Called prev on start")
}
Expand All @@ -84,26 +153,35 @@ impl MinesweeperReplay {
};
match &play_to_undo.1 {
PlayOutcome::Success(results) => results.iter().for_each(|rc| {
self.current_players[rc.1.player].score -= 1;
self.current_board[rc.0] = PlayerCell::Hidden(HiddenCell::Empty);
}),
PlayOutcome::Failure(rc) => {
self.current_players[rc.1.player].dead = false;
self.current_revealed_mines -= 1;
self.current_board[rc.0] = PlayerCell::Hidden(HiddenCell::Mine);
}
PlayOutcome::Victory(results) => results.iter().for_each(|rc| {
self.current_board[rc.0] = PlayerCell::Hidden(HiddenCell::Empty);
}),
PlayOutcome::Victory(results) => {
self.current_players[results[0].1.player].victory_click = false;
results.iter().for_each(|rc| {
self.current_players[rc.1.player].score -= 1;
self.current_board[rc.0] = PlayerCell::Hidden(HiddenCell::Empty);
});
}
PlayOutcome::Flag(res) => {
if matches!(res.1, PlayerCell::Hidden(HiddenCell::Flag)) {
self.current_flags -= 1;
self.current_board[res.0] = self.current_board[res.0].remove_flag()
} else {
self.current_flags += 1;
self.current_board[res.0] = self.current_board[res.0].add_flag()
}
}
};
Ok(())
Ok(self.current_pos())
}

pub fn to_pos(&mut self, pos: usize) -> Result<()> {
pub fn to_pos(&mut self, pos: usize) -> Result<ReplayPosition> {
if pos >= self.len() {
bail!(
"Called to_pos with pos out of bounds (max {}): {}",
Expand All @@ -117,7 +195,7 @@ impl MinesweeperReplay {
while pos > self.current_pos {
let _ = self.advance();
}
Ok(())
Ok(self.current_pos())
}
}

Expand Down Expand Up @@ -282,30 +360,44 @@ mod test {
PlayOutcome::Failure(PLAY_4_RES),
),
]),
2,
);

// test advance
// test defaults
assert_eq!(replay.current_players.len(), 2);
assert_eq!(
replay
.current_players
.iter()
.map(|p| p.score)
.sum::<usize>(),
0
);
assert_eq!(replay.current_flags, 0);
assert_eq!(replay.current_revealed_mines, 0);
assert_eq!(replay.len(), 5);
assert!(matches!(replay.advance(), Ok(())));

// test advance
assert!(matches!(replay.advance(), Ok(ReplayPosition::Other(1))));
assert_eq!(replay.current_board(), &expected_board_1);
assert!(matches!(replay.advance(), Ok(())));
assert!(matches!(replay.advance(), Ok(ReplayPosition::Other(2))));
assert_eq!(replay.current_board(), &expected_board_2);
assert!(matches!(replay.advance(), Ok(())));
assert!(matches!(replay.advance(), Ok(ReplayPosition::Other(3))));
assert_eq!(replay.current_board(), &expected_board_3);
assert!(matches!(replay.advance(), Ok(())));
assert!(matches!(replay.advance(), Ok(ReplayPosition::End)));
assert_eq!(replay.current_board(), &expected_final_board);

// should error on advance at end
assert!(replay.advance().is_err());

// test rewind
assert!(matches!(replay.rewind(), Ok(())));
assert!(matches!(replay.rewind(), Ok(ReplayPosition::Other(3))));
assert_eq!(replay.current_board(), &expected_board_3);
assert!(matches!(replay.rewind(), Ok(())));
assert!(matches!(replay.rewind(), Ok(ReplayPosition::Other(2))));
assert_eq!(replay.current_board(), &expected_board_2);
assert!(matches!(replay.rewind(), Ok(())));
assert!(matches!(replay.rewind(), Ok(ReplayPosition::Other(1))));
assert_eq!(replay.current_board(), &expected_board_1);
assert!(matches!(replay.rewind(), Ok(())));
assert!(matches!(replay.rewind(), Ok(ReplayPosition::Beginning)));
assert_eq!(replay.current_board(), &expected_starting_board);

// should error on rewind at beginning
Expand Down
16 changes: 14 additions & 2 deletions web/src/app/minesweeper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@ mod client;
mod entry;
mod game;
mod players;
mod replay;
mod widgets;

use chrono::{DateTime, Utc};
pub use entry::{GameMode, JoinOrCreateGame};
pub use game::Game;
pub use game::{GameReplay, GameView, GameWrapper};

use serde::{Deserialize, Serialize};

use minesweeper_lib::{cell::PlayerCell, client::ClientPlayer};
use minesweeper_lib::{
cell::PlayerCell,
client::ClientPlayer,
game::{Play, PlayOutcome},
};

#[cfg(feature = "ssr")]
use super::auth::FrontendUser;
Expand All @@ -35,6 +40,13 @@ pub struct GameInfo {
players: Vec<Option<ClientPlayer>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameInfoWithLog {
game_info: GameInfo,
player_num: Option<u8>,
log: Vec<(Play, PlayOutcome)>,
}

#[cfg(feature = "ssr")]
impl From<&PlayerUser> for ClientPlayer {
fn from(value: &PlayerUser) -> Self {
Expand Down
Loading

0 comments on commit 7c57e40

Please sign in to comment.