use std::collections::HashMap;
use super::ChatCommand;
#[derive(Debug, PartialEq, Clone, Copy)]
enum Mark {
X,
O,
}
impl Mark {
fn to_char(self) -> char {
match self {
Self::O => 'O',
Self::X => 'X',
}
}
fn to_value(self) -> i8 {
match self {
Self::O => 1,
Self::X => -1,
}
}
}
#[derive(Debug, Clone, Copy)]
enum State {
Tie,
Turn(Mark),
Winner(Mark),
}
#[derive(Debug, Clone, Copy)]
struct Board {
marks: [Option<Mark>; 9],
}
const NO_MARK: Option<Mark> = None;
impl Board {
fn new() -> Self {
Board {
marks: [NO_MARK; 9],
}
}
fn print(&self) -> Vec<String> {
let mut str = String::new();
match self.get_state() {
State::Tie => str.push_str("The game ended in a Tie!"),
State::Winner(m) => str.push_str(&format!("{} Won!", m.to_char())),
State::Turn(m) => str.push_str(&format!("{}'s turn!", m.to_char())),
}
str.push('\n');
for i in 0..9 {
str.push(match self.marks[i] {
Some(m) => m.to_char(),
None => '_',
});
str.push(match i % 3 {
2 => '\n',
_ => '|',
});
}
str.lines().map(|s| s.to_owned()).collect()
}
fn get_state(&self) -> State {
for row in 0..3 {
let mark = self.marks[row * 3];
if mark.is_none() {
continue;
}
if self.marks.iter().skip(row * 3).take(3).all(|&m| m == mark) {
return State::Winner(mark.unwrap());
}
}
for col in 0..3 {
let mark = self.marks[col];
if mark.is_none() {
continue;
}
if self
.marks
.iter()
.skip(col)
.step_by(3)
.take(3)
.all(|&m| m == mark)
{
return State::Winner(mark.unwrap());
}
}
for diag in 0..2 {
let mark = &self.marks[diag * 2];
if mark.is_none() {
continue;
}
if self
.marks
.iter()
.skip(diag * 2)
.step_by(4 - diag * 2)
.take(3)
.all(|m| m == mark)
{
return State::Winner(mark.unwrap());
}
}
if self.marks.iter().all(Option::is_some) {
return State::Tie;
}
let r = self.marks.iter().filter(|m| m.is_some()).count();
if r % 2 == 0 {
State::Turn(Mark::X)
} else {
State::Turn(Mark::O)
}
}
fn place(&mut self, i: usize) -> Option<()> {
let state = self.get_state();
let turn = match state {
State::Turn(m) => m,
_ => {
return None;
}
};
if i > 8 {
return None;
}
match self.marks[i] {
Some(_) => None,
None => {
self.marks[i] = Some(turn);
Some(())
}
}
}
fn empty(&self) -> Vec<usize> {
self.marks
.iter()
.enumerate()
.filter(|(_, e)| e.is_none())
.map(|(i, _)| i)
.collect()
}
}
fn minimax(board: &Board) -> (usize, i8) {
let possible = board.empty();
let mut results = Vec::new();
let player = match board.get_state() {
State::Turn(m) => m,
_ => return (0, 0),
};
for mve in possible {
let mut new_board = *board;
new_board.place(mve);
match new_board.get_state() {
State::Turn(_) => {
results.push((mve, -minimax(&new_board).1));
}
State::Tie => results.push((mve, 0)),
State::Winner(m) => results.push((mve, m.to_value() * player.to_value())),
}
}
*results.iter().max_by_key(|t| t.1).unwrap()
}
pub struct TicTacToe {
players: HashMap<String, Board>,
}
impl ChatCommand for TicTacToe {
fn new() -> Self {
Self {
players: HashMap::new(),
}
}
fn help(&self) -> String {
"usage: !tictactoe/!ttt (1-9/reset)".to_owned()
}
fn names() -> Vec<String> {
vec!["tictactoe".to_owned(), "ttt".to_owned()]
}
fn handle(
&mut self,
api: &mut crate::api::TwitchApiWrapper,
ctx: &twitcheventsub::MessageData,
) -> anyhow::Result<()> {
let arg = ctx.message.text.split_whitespace().nth(1);
if let Some(board) = self.players.get(&ctx.chatter.id) {
match board.get_state() {
State::Turn(_) => {}
_ => {
self.players.remove(&ctx.chatter.id);
}
}
}
match arg {
None => {
let _ = api.send_chat_message(self.help());
return Ok(());
}
Some("reset") => {
self.players.remove(&ctx.chatter.id);
}
_ => {
if let Some(arg) = arg {
match arg.chars().next() {
Some(c @ '1'..='9') => {
if !self.players.contains_key(&ctx.chatter.id) {
self.players.insert(ctx.chatter.id.clone(), Board::new());
}
let board = self.players.get_mut(&ctx.chatter.id).unwrap();
if board
.place((c.to_digit(10).unwrap() - 1) as usize)
.is_some()
{
let bot_move = minimax(board).0;
board.place(bot_move);
for row in board.print() {
let _ = api.send_chat_message(row);
}
} else {
let _ = api.send_chat_message("Invalid move!");
}
}
_ => {
let _ = api.send_chat_message(self.help());
return Ok(());
}
}
} else {
let _ = api.send_chat_message(self.help());
}
return Ok(());
}
}
Ok(())
}
}