Skip to content

Commit a625a64

Browse files
authored
Merge pull request #65 from CurtisTechSolutions/feat/connect4-tensor-observation
feat(connect4): expose board and turn tensors for training
2 parents d96564c + b03f0c8 commit a625a64

2 files changed

Lines changed: 64 additions & 4 deletions

File tree

‎crates/apps/gfa-server/src/briefing_tests.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ async fn every_game_briefing_contains_executable_examples_and_matching_views() -
151151
data(&compact, "observation_format")?["tensor_shape"],
152152
if game == "tictactoe" {
153153
json!([3, 3, 3])
154+
} else if game == "connect4" {
155+
json!([3, 6, 7])
154156
} else if game == "chess" {
155157
json!([20, 8, 8])
156158
} else if game == "sudoku" {

‎crates/games/gfa-game-connect4/src/lib.rs‎

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use gfa_core::serde::{Deserialize, Serialize};
44
use gfa_core::serde_json;
55
use gfa_core::{
66
schema, ErrorCode, Game, GameError, GameSpec, Information, Observation, PlayerId, StepEvents,
7-
TurnStructure, Viewer,
7+
Tensor, TurnStructure, Viewer,
88
};
99
use std::collections::HashSet;
1010

@@ -96,7 +96,7 @@ impl Game for Connect4 {
9696
name: "Connect Four".into(),
9797
summary: "Drop discs into seven columns and connect four in any direction.".into(),
9898
engine_version: env!("CARGO_PKG_VERSION").into(),
99-
info_version: "1.0.0".into(),
99+
info_version: "1.1.0".into(),
100100
num_players: [2, 2],
101101
seat_names: vec!["Red".into(), "Yellow".into()],
102102
turn_structure: TurnStructure::Sequential,
@@ -118,7 +118,7 @@ impl Game for Connect4 {
118118
Some(gfa_core::PlayGuide {
119119
objective: "Connect four of your discs horizontally, vertically, or diagonally to win. A full board without a winner draws. Play stops immediately on a win.".into(),
120120
observation: "Text and JSON rows run top to bottom; columns 1..7 run left to right. R is Red (seat 0), Y is Yellow (seat 1), and . is empty. JSON rows contains six rows of seven cells (null, 0, or 1); to_move is the next seat. Both seats and spectators see the entire board.".into(),
121-
tensor: "No tensor representation is supplied by this engine.".into(),
121+
tensor: "Float32 [3,6,7], planes first, rows top to bottom: Red discs, Yellow discs, and next seat (all 0 for Red or 1 for Yellow).".into(),
122122
rewards: "Terminal returns are +1 for the winner, -1 for the loser, and 0 each for a draw. Returns before termination are 0. Shaped rewards are not supported.".into(),
123123
config: "No configurable fields. Use {} for the standard 7-column, 6-row board.".into(),
124124
solved: "Solved: the first player can force a win on the standard board.".into(),
@@ -304,13 +304,29 @@ impl Game for Connect4 {
304304
} else {
305305
"Yellow to move (seat 1)."
306306
});
307+
let values = (0..3)
308+
.flat_map(|plane| {
309+
rows.iter().flatten().map(move |cell| {
310+
if plane == 2 {
311+
f32::from(state.to_move)
312+
} else if *cell == Some(plane as PlayerId) {
313+
1.0
314+
} else {
315+
0.0
316+
}
317+
})
318+
})
319+
.collect();
307320
Observation {
308321
text,
309322
json: serde_json::json!(Board {
310323
rows,
311324
to_move: state.to_move
312325
}),
313-
tensor: None,
326+
tensor: Some(Tensor {
327+
shape: vec![3, 6, 7],
328+
values,
329+
}),
314330
}
315331
}
316332

@@ -410,3 +426,45 @@ mod tests {
410426
Ok(())
411427
}
412428
}
429+
430+
#[cfg(test)]
431+
mod tensor_tests {
432+
use super::*;
433+
434+
#[test]
435+
fn planes_match_public_board_orientation_and_turn() -> Result<(), GameError> {
436+
let mut state = Connect4::new_initial_state(&Config::default(), 0)?;
437+
for column in [1, 2, 1] {
438+
let seat = Connect4::current_players(&state)[0];
439+
Connect4::apply(&mut state, seat, &Action { column })?;
440+
}
441+
let observation = Connect4::observe(&state, Viewer::Spectator);
442+
assert_eq!(observation, Connect4::observe(&state, Viewer::Player(0)));
443+
assert_eq!(observation, Connect4::observe(&state, Viewer::Player(1)));
444+
let board: Board = serde_json::from_value(observation.json)?;
445+
let tensor = observation
446+
.tensor
447+
.ok_or_else(|| GameError::position("missing tensor"))?;
448+
assert_eq!(tensor.shape, vec![3, 6, 7]);
449+
assert_eq!(tensor.values.len(), 126);
450+
assert_eq!(tensor.values[4 * 7], 1.0);
451+
assert_eq!(tensor.values[5 * 7], 1.0);
452+
assert_eq!(tensor.values[42 + 5 * 7 + 1], 1.0);
453+
for (row, cells) in board.rows.iter().enumerate() {
454+
for (col, cell) in cells.iter().enumerate() {
455+
for plane in 0..2 {
456+
assert_eq!(
457+
tensor.values[plane * 42 + row * 7 + col],
458+
if *cell == Some(plane as PlayerId) {
459+
1.0
460+
} else {
461+
0.0
462+
}
463+
);
464+
}
465+
}
466+
}
467+
assert!(tensor.values[84..].iter().all(|value| *value == 1.0));
468+
Ok(())
469+
}
470+
}

0 commit comments

Comments
 (0)