1use turnbase::{Game, PlayerId, Prng, sample_chance};
4use turnbase_protocol::{Request, Response};
5
6use crate::Session;
7
8const CHANCE_SEED_OFFSET: u64 = 0x00C0_FFEE;
13
14pub struct LocalSession<G: Game> {
28 game: G,
29 state: G::State,
30 version: u64,
31 chance: Prng,
32}
33
34impl<G: Game> LocalSession<G> {
35 #[must_use]
39 pub fn new(game: G, state: G::State, seed: u64) -> Self {
40 let mut session = Self {
41 game,
42 state,
43 version: 0,
44 chance: Prng::new(seed ^ CHANCE_SEED_OFFSET),
45 };
46 session.resolve_chance();
47 session
48 }
49
50 #[must_use]
55 pub const fn resume(game: G, state: G::State, version: u64, chance: Prng) -> Self {
56 Self {
57 game,
58 state,
59 version,
60 chance,
61 }
62 }
63
64 #[must_use]
66 pub const fn game(&self) -> &G {
67 &self.game
68 }
69
70 #[must_use]
72 pub const fn state(&self) -> &G::State {
73 &self.state
74 }
75
76 #[must_use]
79 pub const fn version(&self) -> u64 {
80 self.version
81 }
82
83 #[must_use]
85 pub fn into_parts(self) -> (G, G::State, u64, Prng) {
86 (self.game, self.state, self.version, self.chance)
87 }
88
89 fn resolve_chance(&mut self) {
92 while !self.game.is_terminal(&self.state) {
93 let Some(player) = self.game.active_players(&self.state).iter().next() else {
94 break;
95 };
96 if !player.is_chance() {
97 break;
98 }
99 let Some(action) = sample_chance(&self.game, &self.state, &mut self.chance) else {
100 break;
101 };
102 self.game.apply(&mut self.state, PlayerId::CHANCE, action);
103 self.version += 1;
104 }
105 }
106
107 fn query(&self, player: PlayerId) -> Response<G::View> {
110 Response::State {
111 version: self.version,
112 view: self.game.view(&self.state, Some(player)),
113 }
114 }
115
116 fn act(&mut self, player: PlayerId, action: G::Action) -> Response<G::View> {
123 if !self.game.active_players(&self.state).contains(player) {
124 return Response::Error(format!("{player} is not active"));
125 }
126 if !self.game.is_legal(&self.state, player, &action) {
127 return Response::Error(format!("illegal action for {player}"));
128 }
129 self.game.apply(&mut self.state, player, action);
130 self.version += 1;
131 self.resolve_chance();
132 Response::Ack
133 }
134}
135
136impl<G: Game> Session<G> for LocalSession<G> {
137 fn submit(&mut self, player: PlayerId, request: Request<G::Action>) -> Response<G::View> {
138 match request {
139 Request::Query => self.query(player),
140 Request::Act(action) => self.act(player, action),
141 }
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use turnbase::{ActivePlayers, Game, PlayerId, PlayerView, State};
148 use turnbase_protocol::{Request, Response};
149
150 use super::LocalSession;
151 use crate::Session;
152
153 const P0: PlayerId = PlayerId::new(0);
154 const P1: PlayerId = PlayerId::new(1);
155
156 struct Secret;
159
160 impl Game for Secret {
161 type State = State<u32, u32>;
162 type Action = u32;
163 type View = PlayerView<u32, u32>;
164
165 fn new_initial_state(&self, seed: u64) -> Self::State {
166 let mut state = State::new(0, seed);
167 state.insert_private(P0, 111);
168 state.insert_private(P1, 222);
169 state
170 }
171 fn num_players(&self) -> usize {
172 2
173 }
174 fn active_players(&self, _state: &Self::State) -> ActivePlayers {
175 ActivePlayers::one(P0)
176 }
177 fn legal_actions(&self, _state: &Self::State, _player: PlayerId) -> Vec<Self::Action> {
178 vec![0]
179 }
180 fn apply(&self, _state: &mut Self::State, _player: PlayerId, _action: Self::Action) {}
181 fn is_terminal(&self, _state: &Self::State) -> bool {
182 false
183 }
184 fn reward(&self, _state: &Self::State, _player: PlayerId) -> f64 {
185 0.0
186 }
187 fn view(&self, state: &Self::State, viewer: Option<PlayerId>) -> Self::View {
188 state.view_for(viewer)
189 }
190 }
191
192 #[test]
193 fn query_redacts_to_the_requesting_seat() {
194 let mut session = LocalSession::new(Secret, Secret.new_initial_state(1), 1);
195
196 let Response::State { view, .. } = session.submit(P0, Request::Query) else {
197 panic!("expected a state response");
198 };
199 assert_eq!(view.own_private, Some(111), "seat 0 sees its own secret");
200
201 let Response::State { view, .. } = session.submit(P1, Request::Query) else {
202 panic!("expected a state response");
203 };
204 assert_eq!(
205 view.own_private,
206 Some(222),
207 "seat 1 sees its own, never seat 0's"
208 );
209 }
210
211 struct RevealOnce;
213
214 impl Game for RevealOnce {
215 type State = Option<u8>;
216 type Action = u8;
217 type View = Option<u8>;
218
219 fn new_initial_state(&self, _seed: u64) -> Self::State {
220 None
221 }
222 fn num_players(&self) -> usize {
223 0
224 }
225 fn active_players(&self, state: &Self::State) -> ActivePlayers {
226 if state.is_some() {
227 ActivePlayers::none()
228 } else {
229 ActivePlayers::one(PlayerId::CHANCE)
230 }
231 }
232 fn legal_actions(&self, state: &Self::State, player: PlayerId) -> Vec<Self::Action> {
233 if player.is_chance() && state.is_none() {
234 vec![0, 1, 2]
235 } else {
236 Vec::new()
237 }
238 }
239 fn apply(&self, state: &mut Self::State, _player: PlayerId, action: Self::Action) {
240 *state = Some(action);
241 }
242 fn is_terminal(&self, state: &Self::State) -> bool {
243 state.is_some()
244 }
245 fn reward(&self, _state: &Self::State, _player: PlayerId) -> f64 {
246 0.0
247 }
248 fn view(&self, state: &Self::State, _viewer: Option<PlayerId>) -> Self::View {
249 *state
250 }
251 }
252
253 #[test]
254 fn new_resolves_an_opening_chance_node() {
255 let session = LocalSession::new(RevealOnce, RevealOnce.new_initial_state(0), 5);
258 assert!(matches!(session.state(), Some(0..=2)));
259 assert_eq!(session.version(), 1, "one chance outcome was applied");
260 }
261}