use turnbase::{Game, PlayerId, Prng, sample_chance};
use turnbase_protocol::{Request, Response};
use crate::Session;
const CHANCE_SEED_OFFSET: u64 = 0x00C0_FFEE;
pub struct LocalSession<G: Game> {
game: G,
state: G::State,
version: u64,
chance: Prng,
}
impl<G: Game> LocalSession<G> {
#[must_use]
pub fn new(game: G, state: G::State, seed: u64) -> Self {
let mut session = Self {
game,
state,
version: 0,
chance: Prng::new(seed ^ CHANCE_SEED_OFFSET),
};
session.resolve_chance();
session
}
#[must_use]
pub const fn resume(game: G, state: G::State, version: u64, chance: Prng) -> Self {
Self {
game,
state,
version,
chance,
}
}
#[must_use]
pub const fn game(&self) -> &G {
&self.game
}
#[must_use]
pub const fn state(&self) -> &G::State {
&self.state
}
#[must_use]
pub const fn version(&self) -> u64 {
self.version
}
#[must_use]
pub fn into_parts(self) -> (G, G::State, u64, Prng) {
(self.game, self.state, self.version, self.chance)
}
fn resolve_chance(&mut self) {
while !self.game.is_terminal(&self.state) {
let Some(player) = self.game.active_players(&self.state).iter().next() else {
break;
};
if !player.is_chance() {
break;
}
let Some(action) = sample_chance(&self.game, &self.state, &mut self.chance) else {
break;
};
self.game.apply(&mut self.state, PlayerId::CHANCE, action);
self.version += 1;
}
}
fn query(&self, player: PlayerId) -> Response<G::View> {
Response::State {
version: self.version,
view: self.game.view(&self.state, Some(player)),
}
}
fn act(&mut self, player: PlayerId, action: G::Action) -> Response<G::View> {
if !self.game.active_players(&self.state).contains(player) {
return Response::Error(format!("{player} is not active"));
}
if !self.game.is_legal(&self.state, player, &action) {
return Response::Error(format!("illegal action for {player}"));
}
self.game.apply(&mut self.state, player, action);
self.version += 1;
self.resolve_chance();
Response::Ack
}
}
impl<G: Game> Session<G> for LocalSession<G> {
fn submit(&mut self, player: PlayerId, request: Request<G::Action>) -> Response<G::View> {
match request {
Request::Query => self.query(player),
Request::Act(action) => self.act(player, action),
}
}
}
#[cfg(test)]
mod tests {
use turnbase::{ActivePlayers, Game, PlayerId, PlayerView, State};
use turnbase_protocol::{Request, Response};
use super::LocalSession;
use crate::Session;
const P0: PlayerId = PlayerId::new(0);
const P1: PlayerId = PlayerId::new(1);
struct Secret;
impl Game for Secret {
type State = State<u32, u32>;
type Action = u32;
type View = PlayerView<u32, u32>;
fn new_initial_state(&self, seed: u64) -> Self::State {
let mut state = State::new(0, seed);
state.insert_private(P0, 111);
state.insert_private(P1, 222);
state
}
fn num_players(&self) -> usize {
2
}
fn active_players(&self, _state: &Self::State) -> ActivePlayers {
ActivePlayers::one(P0)
}
fn legal_actions(&self, _state: &Self::State, _player: PlayerId) -> Vec<Self::Action> {
vec![0]
}
fn apply(&self, _state: &mut Self::State, _player: PlayerId, _action: Self::Action) {}
fn is_terminal(&self, _state: &Self::State) -> bool {
false
}
fn reward(&self, _state: &Self::State, _player: PlayerId) -> f64 {
0.0
}
fn view(&self, state: &Self::State, viewer: Option<PlayerId>) -> Self::View {
state.view_for(viewer)
}
}
#[test]
fn query_redacts_to_the_requesting_seat() {
let mut session = LocalSession::new(Secret, Secret.new_initial_state(1), 1);
let Response::State { view, .. } = session.submit(P0, Request::Query) else {
panic!("expected a state response");
};
assert_eq!(view.own_private, Some(111), "seat 0 sees its own secret");
let Response::State { view, .. } = session.submit(P1, Request::Query) else {
panic!("expected a state response");
};
assert_eq!(
view.own_private,
Some(222),
"seat 1 sees its own, never seat 0's"
);
}
struct RevealOnce;
impl Game for RevealOnce {
type State = Option<u8>;
type Action = u8;
type View = Option<u8>;
fn new_initial_state(&self, _seed: u64) -> Self::State {
None
}
fn num_players(&self) -> usize {
0
}
fn active_players(&self, state: &Self::State) -> ActivePlayers {
if state.is_some() {
ActivePlayers::none()
} else {
ActivePlayers::one(PlayerId::CHANCE)
}
}
fn legal_actions(&self, state: &Self::State, player: PlayerId) -> Vec<Self::Action> {
if player.is_chance() && state.is_none() {
vec![0, 1, 2]
} else {
Vec::new()
}
}
fn apply(&self, state: &mut Self::State, _player: PlayerId, action: Self::Action) {
*state = Some(action);
}
fn is_terminal(&self, state: &Self::State) -> bool {
state.is_some()
}
fn reward(&self, _state: &Self::State, _player: PlayerId) -> f64 {
0.0
}
fn view(&self, state: &Self::State, _viewer: Option<PlayerId>) -> Self::View {
*state
}
}
#[test]
fn new_resolves_an_opening_chance_node() {
let session = LocalSession::new(RevealOnce, RevealOnce.new_initial_state(0), 5);
assert!(matches!(session.state(), Some(0..=2)));
assert_eq!(session.version(), 1, "one chance outcome was applied");
}
}