AAAAAAAAAAAAAAAAAAAAAAAAa

This commit is contained in:
nora 2022-11-28 13:43:30 +01:00
parent 7f3a0e5ad2
commit d3d7011c43
No known key found for this signature in database
13 changed files with 280 additions and 88 deletions

BIN
first

Binary file not shown.

BIN
second

Binary file not shown.

View file

@ -1,6 +1,9 @@
use std::ops::Index; use std::{
fmt::{Display, Write},
ops::{Index, IndexMut},
};
use crate::{Player, State}; use crate::{minmax::Score, Game, GamePlayer, Player, State};
type Position = Option<Player>; type Position = Option<Player>;
@ -12,11 +15,12 @@ const BOARD_POSITIONS: usize = WIDTH * HEIGTH;
/// 7 8 9 10 11 12 13 /// 7 8 9 10 11 12 13
/// 14 15 16 17 18 19 20 /// 14 15 16 17 18 19 20
/// 21 22 23 24 25 26 27 /// 21 22 23 24 25 26 27
pub struct Board { #[derive(Clone)]
pub struct Connect4 {
positions: [Position; BOARD_POSITIONS], positions: [Position; BOARD_POSITIONS],
} }
impl Board { impl Connect4 {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
positions: [None; BOARD_POSITIONS], positions: [None; BOARD_POSITIONS],
@ -86,9 +90,30 @@ impl Board {
}) })
.unwrap_or(State::InProgress) .unwrap_or(State::InProgress)
} }
fn rate(&self, player: Player) -> Score {
#[rustfmt::skip]
const WIN_COUNT_TABLE: [i32; BOARD_POSITIONS] = [
3, 4, 6, 7, 6, 4, 3,
2, 4, 6, 7, 6, 4, 2,
2, 4, 6, 7, 6, 4, 2,
3, 4, 6, 7, 6, 4, 2,
];
let score_player = |player: Player| {
self.positions
.iter()
.enumerate()
.filter(|(_, state)| **state == Some(player))
.map(|(pos, _)| WIN_COUNT_TABLE[pos])
.sum::<i32>()
};
Score::new(score_player(player) - score_player(player.opponent()))
}
} }
impl Index<usize> for Board { impl Index<usize> for Connect4 {
type Output = Position; type Output = Position;
fn index(&self, index: usize) -> &Self::Output { fn index(&self, index: usize) -> &Self::Output {
@ -96,13 +121,87 @@ impl Index<usize> for Board {
} }
} }
impl IndexMut<usize> for Connect4 {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.positions[index]
}
}
impl Game for Connect4 {
type Move = usize;
const REASONABLE_SEARCH_DEPTH: Option<usize> = Some(5);
fn empty() -> Self {
Self::new()
}
fn possible_moves(&self) -> impl Iterator<Item = Self::Move> {
let board = self.clone();
(0..WIDTH).filter(move |col| board[*col].is_none())
}
fn result(&self) -> State {
Connect4::result(&self)
}
fn make_move(&mut self, position: Self::Move, player: Player) {
for i in 0..3 {
let prev = position + (i * WIDTH);
let next = position + ((i + 1) * WIDTH);
if self[next].is_some() {
self[prev] = Some(player);
break;
}
}
let bottom = position + (3 * WIDTH);
self[bottom] = Some(player);
}
fn undo_move(&mut self, position: Self::Move) {
for i in 0..4 {
let pos = position + (i * WIDTH);
if self[pos].is_some() {
self[pos] = None;
}
}
}
fn rate(&self, player: Player) -> Score {
Connect4::rate(&self, player)
}
}
impl Display for Connect4 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for i in 0..HEIGTH {
for j in 0..WIDTH {
let index = (i * WIDTH) + j;
match self[index] {
Some(player) => {
write!(f, "\x1B[33m {player}\x1B[0m ")?;
}
None => {
write!(f, "\x1B[35m{index:3 }\x1B[0m ")?;
}
}
}
f.write_char('\n')?;
}
Ok(())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{Player, State}; use crate::{Player, State};
use super::Board; use super::Connect4;
fn parse_board(board: &str) -> Board { fn parse_board(board: &str) -> Connect4 {
let positions = board let positions = board
.chars() .chars()
.filter(|char| !char.is_whitespace()) .filter(|char| !char.is_whitespace())
@ -119,7 +218,7 @@ mod tests {
board.chars().filter(|c| !c.is_whitespace()).count() board.chars().filter(|c| !c.is_whitespace()).count()
)); ));
Board { positions } Connect4 { positions }
} }
fn test(board: &str, state: State) { fn test(board: &str, state: State) {

View file

@ -1 +1,6 @@
use self::board::Connect4;
pub use player::HumanPlayer;
pub mod board; pub mod board;
pub mod player;

35
src/connect4/player.rs Normal file
View file

@ -0,0 +1,35 @@
use std::io::Write;
use crate::{Game, GamePlayer, Player};
use super::Connect4;
#[derive(Clone, Default)]
pub struct HumanPlayer;
impl GamePlayer<Connect4> for HumanPlayer {
fn next_move(&mut self, board: &mut Connect4, this_player: Player) {
loop {
print!("{board}where to put the next {this_player}? (0-7): ");
std::io::stdout().flush().unwrap();
let mut buf = String::new();
std::io::stdin().read_line(&mut buf).unwrap();
match buf.trim().parse() {
Ok(number) if number < 7 => match board[number] {
None => {
board.make_move(number, this_player);
return;
}
Some(_) => {
println!("Field is occupied already.")
}
},
Ok(_) | Err(_) => {
println!("Invalid input.")
}
}
}
}
}

View file

@ -1,4 +1,9 @@
#![feature(never_type, try_trait_v2, return_position_impl_trait_in_trait)] #![feature(
never_type,
try_trait_v2,
return_position_impl_trait_in_trait,
let_chains
)]
#![allow(incomplete_features)] #![allow(incomplete_features)]
pub mod connect4; pub mod connect4;
@ -7,13 +12,56 @@ pub mod tic_tac_toe;
mod player; mod player;
use self::minmax::GameBoard; use std::fmt::Display;
use minmax::Score;
pub use player::{Player, State}; pub use player::{Player, State};
pub trait Game { pub trait GamePlayer<G: ?Sized + Game>: Default {
type Board: GameBoard; fn next_move(&mut self, board: &mut G, this_player: Player);
} }
pub trait GamePlayer<G: Game>: Default { pub trait Game: Display {
fn next_move(&mut self, board: &mut G::Board, this_player: Player); type Move: Copy;
const REASONABLE_SEARCH_DEPTH: Option<usize>;
fn empty() -> Self;
fn possible_moves(&self) -> impl Iterator<Item = Self::Move>;
fn result(&self) -> State;
/// Only called if [`GameBoard::REASONABLE_SEARCH_DEPTH`] is `Some`.
fn rate(&self, player: Player) -> Score;
fn make_move(&mut self, position: Self::Move, player: Player);
fn undo_move(&mut self, position: Self::Move);
fn play<A: GamePlayer<Self>, B: GamePlayer<Self>>(
&mut self,
x: &mut A,
o: &mut B,
) -> Option<Player> {
let mut current_player = Player::X;
loop {
if current_player == Player::X {
x.next_move(self, current_player);
} else {
o.next_move(self, current_player);
}
match self.result() {
State::Winner(player) => return Some(player),
State::Draw => {
return None;
}
State::InProgress => {}
}
current_player = current_player.opponent();
}
}
} }

View file

@ -1,19 +1,25 @@
#![allow(unused_imports)] #![allow(unused_imports)]
use std::time::SystemTime; use std::{fmt::Display, time::SystemTime};
use minmax::{ use minmax::{
tic_tac_toe::{Board, GreedyPlayer, HumanPlayer, PerfectPlayer, TicTacToe}, connect4::{self, board::Connect4},
GamePlayer, Player, tic_tac_toe::{GreedyPlayer, HumanPlayer, PerfectPlayer, TicTacToe},
Game, GamePlayer, Player,
}; };
fn main() { fn main() {
play::<connect4::HumanPlayer, connect4::HumanPlayer, _>(true);
}
#[allow(dead_code)]
fn tic_tac_toe_stats() {
let mut results = [0, 0, 0]; let mut results = [0, 0, 0];
let start = SystemTime::now(); let start = SystemTime::now();
for _ in 0..100 { for _ in 0..100 {
let result = play_round::<PerfectPlayer, GreedyPlayer>(false); let result = play::<PerfectPlayer, GreedyPlayer, _>(false);
let idx = Player::as_u8(result); let idx = Player::as_u8(result);
results[idx as usize] += 1; results[idx as usize] += 1;
} }
@ -28,23 +34,24 @@ fn main() {
println!("Completed in {}ms", time.as_millis()); println!("Completed in {}ms", time.as_millis());
} }
fn play_round<X: GamePlayer<TicTacToe>, O: GamePlayer<TicTacToe>>(print: bool) -> Option<Player> { fn play<X: GamePlayer<G>, O: GamePlayer<G>, G: Game>(print: bool) -> Option<Player> {
let mut board = Board::empty(); let mut board = G::empty();
let result = board.play(&mut X::default(), &mut O::default()); let result = board.play(&mut X::default(), &mut O::default());
if print { if print {
println!("{board}"); print_result(result, board);
}
match result {
Some(winner) => {
if print {
println!("player {winner} won!");
}
}
None => {
if print {
println!("a draw...")
}
}
} }
result result
} }
fn print_result(result: Option<Player>, board: impl Display) {
println!("{board}");
match result {
Some(winner) => {
println!("player {winner} won!");
}
None => {
println!("a draw...")
}
}
}

View file

@ -2,25 +2,17 @@ use std::ops::Neg;
use crate::{Game, GamePlayer, Player, State}; use crate::{Game, GamePlayer, Player, State};
pub trait GameBoard {
type Move: Copy;
fn possible_moves(&self) -> impl Iterator<Item = Self::Move>;
fn result(&self) -> State;
fn make_move(&mut self, position: Self::Move, player: Player);
fn undo_move(&mut self, position: Self::Move);
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Score(i8); pub struct Score(i32);
impl Score { impl Score {
const LOST: Self = Self(i8::MIN); const LOST: Self = Self(i32::MIN);
const TIE: Self = Self(0); const TIE: Self = Self(0);
const WON: Self = Self(i8::MAX); const WON: Self = Self(i32::MAX);
pub fn new(int: i32) -> Self {
Self(int)
}
} }
impl Neg for Score { impl Neg for Score {
@ -32,25 +24,26 @@ impl Neg for Score {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct PerfectPlayer<B: GameBoard> { pub struct PerfectPlayer<B: Game> {
best_move: Option<B::Move>, best_move: Option<B::Move>,
} }
impl<B: GameBoard> Default for PerfectPlayer<B> { impl<B: Game> Default for PerfectPlayer<B> {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
} }
} }
impl<B: GameBoard> PerfectPlayer<B> { impl<B: Game> PerfectPlayer<B> {
pub fn new() -> Self { pub fn new() -> Self {
Self { best_move: None } Self { best_move: None }
} }
fn minmax(&mut self, board: &mut B, player: Player, depth: usize) -> Score { fn minmax(&mut self, board: &mut B, player: Player, depth: usize) -> Score {
if depth < 2 { if let Some(max_depth) = B::REASONABLE_SEARCH_DEPTH && depth >= max_depth {
//print!("{board}{}| playing {player}: ", " ".repeat(depth)); return board.rate(player);
} }
match board.result() { match board.result() {
State::Winner(winner) => { State::Winner(winner) => {
if winner == player { if winner == player {
@ -83,8 +76,8 @@ impl<B: GameBoard> PerfectPlayer<B> {
} }
} }
impl<G: Game> GamePlayer<G> for PerfectPlayer<G::Board> { impl<G: Game> GamePlayer<G> for PerfectPlayer<G> {
fn next_move(&mut self, board: &mut G::Board, this_player: Player) { fn next_move(&mut self, board: &mut G, this_player: Player) {
self.best_move = None; self.best_move = None;
self.minmax(board, this_player, 0); self.minmax(board, this_player, 0);

View file

@ -1,11 +1,11 @@
use std::fmt::{Display, Write}; use std::fmt::{Display, Write};
use crate::{minmax::GameBoard, Player, State}; use crate::{minmax::Score, Player, State, Game};
#[derive(Clone)] #[derive(Clone)]
pub struct Board(u32); pub struct TicTacToe(u32);
impl Board { impl TicTacToe {
pub fn empty() -> Self { pub fn empty() -> Self {
// A = 1010 // A = 1010
// 18 bits - 9 * 2 bits - 4.5 nibbles // 18 bits - 9 * 2 bits - 4.5 nibbles
@ -75,14 +75,14 @@ impl Board {
} }
mod win_table { mod win_table {
use super::Board; use super::TicTacToe;
use crate::{Player, State}; use crate::{Player, State};
const WIN_TABLE_SIZE: usize = 2usize.pow(2 * 9); const WIN_TABLE_SIZE: usize = 2usize.pow(2 * 9);
static WIN_TABLE: &[u8; WIN_TABLE_SIZE] = static WIN_TABLE: &[u8; WIN_TABLE_SIZE] =
include_bytes!(concat!(env!("OUT_DIR"), "/win_table")); include_bytes!(concat!(env!("OUT_DIR"), "/win_table"));
pub fn result(board: &Board) -> State { pub fn result(board: &TicTacToe) -> State {
match WIN_TABLE[board.0 as usize] { match WIN_TABLE[board.0 as usize] {
0 => State::Winner(Player::X), 0 => State::Winner(Player::X),
1 => State::Winner(Player::X), 1 => State::Winner(Player::X),
@ -93,7 +93,7 @@ mod win_table {
} }
} }
impl Display for Board { impl Display for TicTacToe {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for i in 0..3 { for i in 0..3 {
for j in 0..3 { for j in 0..3 {
@ -113,9 +113,15 @@ impl Display for Board {
} }
} }
impl GameBoard for Board { impl Game for TicTacToe {
type Move = usize; type Move = usize;
const REASONABLE_SEARCH_DEPTH: Option<usize> = None;
fn empty() -> Self {
Self::empty()
}
fn possible_moves(&self) -> impl Iterator<Item = Self::Move> { fn possible_moves(&self) -> impl Iterator<Item = Self::Move> {
debug_assert!( debug_assert!(
!self.iter().all(|x| x.is_some()), !self.iter().all(|x| x.is_some()),
@ -129,7 +135,11 @@ impl GameBoard for Board {
} }
fn result(&self) -> State { fn result(&self) -> State {
Board::result(self) TicTacToe::result(self)
}
fn rate(&self, _: Player) -> Score {
unimplemented!("we always finish the board")
} }
fn make_move(&mut self, position: Self::Move, player: Player) { fn make_move(&mut self, position: Self::Move, player: Player) {
@ -143,11 +153,11 @@ impl GameBoard for Board {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{Board, Player}; use super::{Player, TicTacToe};
#[test] #[test]
fn board_field() { fn board_field() {
let mut board = Board::empty(); let mut board = TicTacToe::empty();
board.set(0, None); board.set(0, None);
board.set(8, Some(Player::X)); board.set(8, Some(Player::X));
board.set(4, Some(Player::O)); board.set(4, Some(Player::O));

View file

@ -1,13 +1,17 @@
use crate::{GamePlayer, Player, State}; use crate::{GamePlayer, Player, State};
use super::{board::Board, TicTacToe}; use super::TicTacToe;
impl Board { impl TicTacToe {
pub fn default_play<X: GamePlayer<TicTacToe>, O: GamePlayer<TicTacToe>>() -> Option<Player> { pub fn default_play<X: GamePlayer<TicTacToe>, O: GamePlayer<TicTacToe>>() -> Option<Player> {
Self::empty().play(&mut X::default(), &mut O::default()) Self::empty().play(&mut X::default(), &mut O::default())
} }
pub fn play<A: GamePlayer<TicTacToe>, B: GamePlayer<TicTacToe>>(&mut self, x: &mut A, o: &mut B) -> Option<Player> { pub fn play<A: GamePlayer<TicTacToe>, B: GamePlayer<TicTacToe>>(
&mut self,
x: &mut A,
o: &mut B,
) -> Option<Player> {
let mut current_player = Player::X; let mut current_player = Player::X;
for _ in 0..9 { for _ in 0..9 {

View file

@ -1,26 +1,17 @@
use crate::Game;
mod board; mod board;
mod game; mod game;
mod perfect; mod perfect;
mod player; mod player;
pub use {board::Board, perfect::PerfectPlayer, player::*}; pub use {board::TicTacToe, perfect::PerfectPlayer, player::*};
pub struct TicTacToe;
impl Game for TicTacToe {
type Board = board::Board;
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{tic_tac_toe::board::Board, GamePlayer, Player}; use crate::{tic_tac_toe::board::TicTacToe, GamePlayer, Player};
use super::{ use super::{
perfect::PerfectPlayer, perfect::PerfectPlayer,
player::{GreedyPlayer, RandomPlayer}, player::{GreedyPlayer, RandomPlayer},
TicTacToe,
}; };
fn assert_win_ratio<X: GamePlayer<TicTacToe>, O: GamePlayer<TicTacToe>>( fn assert_win_ratio<X: GamePlayer<TicTacToe>, O: GamePlayer<TicTacToe>>(
@ -30,7 +21,7 @@ mod tests {
let mut results = [0u64, 0, 0]; let mut results = [0u64, 0, 0];
for _ in 0..runs { for _ in 0..runs {
let result = Board::default_play::<X, O>(); let result = TicTacToe::default_play::<X, O>();
let idx = Player::as_u8(result); let idx = Player::as_u8(result);
results[idx as usize] += 1; results[idx as usize] += 1;
} }

View file

@ -2,7 +2,7 @@ use std::ops::Neg;
use crate::{GamePlayer, Player, State}; use crate::{GamePlayer, Player, State};
use super::{board::Board, TicTacToe}; use super::TicTacToe;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Score { enum Score {
@ -41,7 +41,7 @@ impl PerfectPlayer {
} }
} }
fn minmax(&mut self, board: &mut Board, player: Player, depth: usize) -> Score { fn minmax(&mut self, board: &mut TicTacToe, player: Player, depth: usize) -> Score {
if depth < 2 { if depth < 2 {
//print!("{board}{}| playing {player}: ", " ".repeat(depth)); //print!("{board}{}| playing {player}: ", " ".repeat(depth));
} }
@ -105,7 +105,7 @@ impl PerfectPlayer {
} }
impl GamePlayer<TicTacToe> for PerfectPlayer { impl GamePlayer<TicTacToe> for PerfectPlayer {
fn next_move(&mut self, board: &mut Board, this_player: Player) { fn next_move(&mut self, board: &mut TicTacToe, this_player: Player) {
self.best_move = usize::MAX; self.best_move = usize::MAX;
self.minmax(board, this_player, 0); self.minmax(board, this_player, 0);

View file

@ -2,13 +2,13 @@ use std::io::Write;
use crate::{GamePlayer, Player}; use crate::{GamePlayer, Player};
use super::{board::Board, TicTacToe}; use super::TicTacToe;
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct GreedyPlayer; pub struct GreedyPlayer;
impl GamePlayer<TicTacToe> for GreedyPlayer { impl GamePlayer<TicTacToe> for GreedyPlayer {
fn next_move(&mut self, board: &mut Board, this_player: Player) { fn next_move(&mut self, board: &mut TicTacToe, this_player: Player) {
let first_free = board.iter().position(|p| p.is_none()).unwrap(); let first_free = board.iter().position(|p| p.is_none()).unwrap();
board.set(first_free, Some(this_player)); board.set(first_free, Some(this_player));
} }
@ -18,7 +18,7 @@ impl GamePlayer<TicTacToe> for GreedyPlayer {
pub struct HumanPlayer; pub struct HumanPlayer;
impl GamePlayer<TicTacToe> for HumanPlayer { impl GamePlayer<TicTacToe> for HumanPlayer {
fn next_move(&mut self, board: &mut Board, this_player: Player) { fn next_move(&mut self, board: &mut TicTacToe, this_player: Player) {
loop { loop {
print!("{board}where to put the next {this_player}? (0-8): "); print!("{board}where to put the next {this_player}? (0-8): ");
@ -55,7 +55,7 @@ fn fun_random() -> u64 {
} }
impl GamePlayer<TicTacToe> for RandomPlayer { impl GamePlayer<TicTacToe> for RandomPlayer {
fn next_move(&mut self, board: &mut Board, this_player: Player) { fn next_move(&mut self, board: &mut TicTacToe, this_player: Player) {
loop { loop {
let next = (fun_random() % 9) as usize; let next = (fun_random() % 9) as usize;
match board.get(next) { match board.get(next) {