use crate::game::{DrawError, DrawTileResult};
use crate::meld::PossibleMeld;
use crate::{Game, GamePhase, PlayerId, TileId, Wind, WINDS_ROUND_ORDER};
use rand::seq::SliceRandom;
use rand::thread_rng;
use rustc_hash::FxHashSet;
use strum_macros::EnumIter;
mod best_drops;
pub struct StandardAI<'a> {
ai_players: FxHashSet<PlayerId>,
pub auto_stop_claim_meld: FxHashSet<PlayerId>,
pub can_draw_round: bool,
pub can_pass_turn: bool,
pub dealer_order_deterministic: Option<bool>,
pub draw_tile_for_real_player: bool,
pub game: &'a mut Game,
pub shuffle_players: bool,
pub sort_on_draw: bool,
pub sort_on_initial_draw: bool,
pub with_dead_wall: bool,
}
#[derive(Debug, Eq, PartialEq, EnumIter, Clone)]
pub enum PlayExitLocation {
AIPlayerTileDrawn,
AIPlayerTurnPassed,
AlreadyEnd,
AutoStoppedDrawMahjong,
AutoStoppedDrawNormal,
ClaimedTile,
CompletedPlayers,
CouldNotClaimTile,
DecidedDealer,
FinishedCharleston,
InitialDraw,
InitialDrawError(DrawError),
InitialShuffle,
MeldCreated,
NewRoundFromMeld,
NoAction,
NoAutoDrawTile,
RoundPassed,
StartGame,
SuccessMahjong,
TileDiscarded,
TileDrawn,
TurnPassed,
WaitingDealerOrder,
WaitingPlayers,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Metadata {}
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct PlayActionResult {
pub changed: bool,
pub exit_location: PlayExitLocation,
pub metadata: Option<Metadata>,
}
pub fn sort_by_is_mahjong(a: &PossibleMeld, b: &PossibleMeld) -> std::cmp::Ordering {
if a.is_mahjong && !b.is_mahjong {
std::cmp::Ordering::Less
} else if !a.is_mahjong && b.is_mahjong {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
}
impl<'a> StandardAI<'a> {
pub fn get_is_after_discard(&self) -> bool {
let current_player = self.game.get_current_player();
if current_player.is_none() {
return false;
}
let current_hand = self.game.table.hands.get(¤t_player.unwrap());
current_hand.unwrap().len() < self.game.style.tiles_after_claim()
&& self.game.round.tile_claimed.is_some()
}
}
impl<'a> StandardAI<'a> {
pub fn new(
game: &'a mut Game,
ai_players: FxHashSet<PlayerId>,
auto_stop_claim_meld: FxHashSet<PlayerId>,
) -> Self {
Self {
ai_players,
auto_stop_claim_meld,
can_draw_round: false,
can_pass_turn: true,
dealer_order_deterministic: None,
draw_tile_for_real_player: true,
game,
shuffle_players: false,
sort_on_draw: false,
sort_on_initial_draw: false,
with_dead_wall: false,
}
}
pub fn play_action(&mut self, with_metadata: bool) -> PlayActionResult {
let mut metadata: Option<Metadata> = None;
if with_metadata {
metadata = Some(Metadata {});
}
match self.game.phase {
GamePhase::Charleston => {
let finished_charleston = self.game.move_charleston();
if finished_charleston.is_ok() {
return PlayActionResult {
changed: true,
exit_location: PlayExitLocation::FinishedCharleston,
metadata,
};
}
return PlayActionResult {
changed: false,
exit_location: PlayExitLocation::NoAction,
metadata,
};
}
GamePhase::WaitingPlayers => {
return match self.game.complete_players(self.shuffle_players) {
Ok(_) => PlayActionResult {
changed: true,
exit_location: PlayExitLocation::CompletedPlayers,
metadata,
},
Err(_) => PlayActionResult {
changed: false,
exit_location: PlayExitLocation::WaitingPlayers,
metadata,
},
};
}
GamePhase::InitialShuffle => {
self.game.prepare_table(self.with_dead_wall);
return PlayActionResult {
changed: true,
exit_location: PlayExitLocation::InitialShuffle,
metadata,
};
}
GamePhase::DecidingDealer => {
if self.dealer_order_deterministic.is_some() {
let dealer_order_deterministic = self.dealer_order_deterministic.unwrap();
self.game
.round
.set_initial_winds(Some(if dealer_order_deterministic {
WINDS_ROUND_ORDER.clone()
} else {
let mut winds: [Wind; 4] = WINDS_ROUND_ORDER.clone();
winds.shuffle(&mut thread_rng());
winds
}))
.unwrap();
} else if self.game.round.initial_winds.is_none() {
return PlayActionResult {
metadata,
changed: false,
exit_location: PlayExitLocation::WaitingDealerOrder,
};
}
self.game.decide_dealer().unwrap();
return PlayActionResult {
changed: true,
metadata,
exit_location: PlayExitLocation::DecidedDealer,
};
}
GamePhase::Beginning => {
self.game.start(self.shuffle_players);
return PlayActionResult {
changed: true,
metadata,
exit_location: PlayExitLocation::StartGame,
};
}
GamePhase::InitialDraw => match self.game.initial_draw() {
Ok(_) => {
if self.sort_on_initial_draw {
for player in self.game.table.hands.0.clone().keys() {
self.game.table.hands.sort_player_hand(player);
}
}
return PlayActionResult {
changed: true,
exit_location: PlayExitLocation::InitialDraw,
metadata,
};
}
Err(e) => {
return PlayActionResult {
metadata,
changed: false,
exit_location: PlayExitLocation::InitialDrawError(e),
};
}
},
GamePhase::End => {
return PlayActionResult {
changed: false,
exit_location: PlayExitLocation::AlreadyEnd,
metadata,
};
}
GamePhase::Playing => {}
}
let mut melds = self.game.get_possible_melds(true);
let mut rng = thread_rng();
melds.shuffle(&mut rng);
melds.sort_by(sort_by_is_mahjong);
for meld in melds {
if self.ai_players.contains(&meld.player_id) {
if meld.is_mahjong {
let mahjong_success = self.game.say_mahjong(&meld.player_id);
if mahjong_success.is_ok() {
return PlayActionResult {
changed: true,
exit_location: PlayExitLocation::SuccessMahjong,
metadata,
};
}
}
let player_hand = self.game.table.hands.0.get(&meld.player_id).unwrap();
let missing_tile = meld
.tiles
.iter()
.find(|tile| !player_hand.get_has_tile(tile));
if let Some(missing_tile) = missing_tile {
if let Some(claimable_type) =
self.game.round.get_claimable_tile(&meld.player_id)
{
if claimable_type == *missing_tile {
let was_tile_claimed = self.game.claim_tile(&meld.player_id);
if was_tile_claimed {
return PlayActionResult {
changed: true,
exit_location: PlayExitLocation::ClaimedTile,
metadata,
};
} else {
return PlayActionResult {
changed: false,
exit_location: PlayExitLocation::CouldNotClaimTile,
metadata,
};
}
}
}
}
if meld.discard_tile.is_some() {
continue;
}
let phase_before = self.game.phase;
let meld_created = self.game.create_meld(
&meld.player_id,
&meld.tiles,
meld.is_upgrade,
meld.is_concealed,
);
if phase_before == GamePhase::Playing && self.game.phase != GamePhase::Playing {
return PlayActionResult {
changed: true,
exit_location: PlayExitLocation::NewRoundFromMeld,
metadata,
};
}
if meld_created.is_ok() {
return PlayActionResult {
changed: true,
exit_location: PlayExitLocation::MeldCreated,
metadata,
};
}
}
}
let current_player = self.game.get_current_player().unwrap();
if self.ai_players.contains(¤t_player) {
let is_tile_claimed = self.game.round.tile_claimed.is_some();
if !is_tile_claimed {
let tile_drawn = self.game.draw_tile_from_wall();
match tile_drawn {
DrawTileResult::Bonus(_) | DrawTileResult::Normal(_) => {
if let DrawTileResult::Normal(_) = tile_drawn {
if self.sort_on_initial_draw {
self.game.table.hands.sort_player_hand(¤t_player);
}
}
return PlayActionResult {
changed: true,
exit_location: PlayExitLocation::AIPlayerTileDrawn,
metadata,
};
}
DrawTileResult::AlreadyDrawn | DrawTileResult::WallExhausted => {}
};
}
let player_hand = self.game.table.hands.0.get(¤t_player).unwrap();
if player_hand.len() == self.game.style.tiles_after_claim() {
let mut tiles_without_meld = player_hand
.list
.iter()
.filter(|tile| tile.set_id.is_none())
.map(|tile| tile.id)
.collect::<Vec<TileId>>();
if !tiles_without_meld.is_empty() {
let tile_to_discard = 'a: {
if let Some(tile_claimed) = self.game.round.tile_claimed.clone() {
for tile in tiles_without_meld.iter() {
if tile_claimed.id == *tile {
break 'a tile_claimed.id;
}
}
}
tiles_without_meld.shuffle(&mut thread_rng());
tiles_without_meld[0]
};
let discarded = self.game.discard_tile_to_board(&tile_to_discard);
if discarded.is_ok() {
return PlayActionResult {
changed: true,
exit_location: PlayExitLocation::TileDiscarded,
metadata,
};
}
}
} else if self.can_pass_turn {
let auto_stop_claim_meld = self.auto_stop_claim_meld.clone();
if !auto_stop_claim_meld.is_empty() {
for player in auto_stop_claim_meld {
if player.is_empty() {
continue;
}
let (can_claim_tile, tile_claimed, _) =
self.game.get_can_claim_tile(&player);
if !can_claim_tile {
continue;
}
let tile_claimed = tile_claimed.unwrap();
let melds_mahjong = self.game.get_possible_melds_for_player(&player, true);
let melds_with_draw_mahjong = melds_mahjong
.iter()
.filter(|meld| meld.tiles.contains(&tile_claimed))
.collect::<Vec<&PossibleMeld>>();
if !melds_with_draw_mahjong.is_empty() {
return PlayActionResult {
changed: false,
exit_location: PlayExitLocation::AutoStoppedDrawMahjong,
metadata,
};
}
let melds_normal = self.game.get_possible_melds_for_player(&player, false);
let melds_with_draw_normal = melds_normal
.iter()
.filter(|meld| meld.tiles.contains(&tile_claimed))
.collect::<Vec<&PossibleMeld>>();
if !melds_with_draw_normal.is_empty() {
return PlayActionResult {
changed: false,
exit_location: PlayExitLocation::AutoStoppedDrawNormal,
metadata,
};
}
}
}
let success = self.game.round.next_turn(&self.game.table.hands);
if success.is_ok() {
return PlayActionResult {
changed: true,
exit_location: PlayExitLocation::AIPlayerTurnPassed,
metadata,
};
}
};
} else {
let is_tile_claimed = self.game.round.tile_claimed.is_some();
if !is_tile_claimed {
if !self.draw_tile_for_real_player {
return PlayActionResult {
changed: false,
exit_location: PlayExitLocation::NoAutoDrawTile,
metadata,
};
}
let tile_drawn = self.game.draw_tile_from_wall();
match tile_drawn {
DrawTileResult::Bonus(_) | DrawTileResult::Normal(_) => {
if let DrawTileResult::Normal(_) = tile_drawn {
if self.sort_on_draw {
self.game.table.hands.sort_player_hand(¤t_player);
}
}
return PlayActionResult {
changed: true,
exit_location: PlayExitLocation::TileDrawn,
metadata,
};
}
DrawTileResult::AlreadyDrawn | DrawTileResult::WallExhausted => {}
};
} else if self.can_pass_turn {
let player_hand = self.game.table.hands.0.get(¤t_player).unwrap();
if player_hand.len() < self.game.style.tiles_after_claim() {
let success = self.game.round.next_turn(&self.game.table.hands);
if success.is_ok() {
return PlayActionResult {
changed: true,
exit_location: PlayExitLocation::TurnPassed,
metadata,
};
}
}
}
}
if self.game.table.draw_wall.is_empty() && self.can_draw_round {
let round_passed = self.game.pass_null_round();
if round_passed.is_ok() {
return PlayActionResult {
changed: true,
exit_location: PlayExitLocation::RoundPassed,
metadata,
};
}
}
PlayActionResult {
changed: false,
exit_location: PlayExitLocation::NoAction,
metadata,
}
}
}