Skip to content

Commit

Permalink
改善對槓子的支持
Browse files Browse the repository at this point in the history
  • Loading branch information
igncp committed Aug 4, 2024
1 parent 5febee4 commit 5025258
Show file tree
Hide file tree
Showing 85 changed files with 1,840 additions and 603 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ handle games which handles most of the game mechanics.
- Includes E2E tests
1. A Rust cli for running simulations

You can find the project's Rust documentation [here](https://mahjong-rust.com/doc/mahjong_core).

## Development

The project main dependencies are Rust and Nodejs. There is a
Expand Down
1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ crossterm = "0.26.1"
futures-util = "0.3.28"
mahjong_core = { path = "../mahjong_core" }
service_contracts = { path = "../service_contracts" }
mahjong_service = { path = "../service" }
reqwest = { version = "0.11.18", features = ["json"] }
serde_json = "1.0.100"
tokio = { version = "1.29.1", features = ["full"] }
Expand Down
2 changes: 2 additions & 0 deletions cli/src/base.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::print_game::PrintGameOpts;
use crate::simulate::SimulateOpts;

#[derive(Debug, Clone, PartialEq)]
pub enum AppCommand {
Simulate(SimulateOpts),
PrintGame(PrintGameOpts),
}

pub struct App {
Expand Down
11 changes: 10 additions & 1 deletion cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
use crate::{
base::{App, AppCommand},
print_game::{get_print_game_command, get_print_game_opts},
simulate::{get_simulate_command, get_simulate_opts},
};
use clap::command;

pub async fn parse_args(app: &mut App) {
let simulate_command = get_simulate_command();
let print_game_command = get_print_game_command();

let matches = command!().subcommand(simulate_command).get_matches();
let matches = command!()
.subcommand(simulate_command)
.subcommand(print_game_command)
.get_matches();

match matches.subcommand() {
Some(("simulate", args_matches)) => {
let opts = get_simulate_opts(args_matches);
app.command = Some(AppCommand::Simulate(opts));
}
Some(("print-game", args_matches)) => {
let opts = get_print_game_opts(args_matches);
app.command = Some(AppCommand::PrintGame(opts));
}
_ => {
println!("Error: no command specified");
std::process::exit(1);
Expand Down
7 changes: 7 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#![deny(clippy::use_self)]
use base::{App, AppCommand};
use cli::parse_args;
use print_game::print_game;
use simulate::run_simulation;

mod base;
mod cli;
mod log;
mod print_game;
mod simulate;

#[tokio::main]
Expand All @@ -20,5 +22,10 @@ async fn main() {
AppCommand::Simulate(opts) => {
run_simulation(opts).await;
}
AppCommand::PrintGame(opts) => {
print_game(opts).await.unwrap_or_else(|e| {
println!("Error: {:?}", e);
});
}
}
}
46 changes: 46 additions & 0 deletions cli/src/print_game.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use std::io::{Error, ErrorKind};

use clap::{Arg, Command};
use mahjong_service::db_storage::DBStorage;

#[derive(Debug, Clone, PartialEq)]
pub struct PrintGameOpts {
pub game_id: String,
}

pub async fn print_game(opts: PrintGameOpts) -> Result<(), Error> {
let storage = DBStorage::new_dyn();

let game = storage
.get_game(&opts.game_id, false)
.await
.unwrap()
.ok_or_else(|| {
Error::new(
ErrorKind::Other,
format!("Game with ID {} not found", opts.game_id),
)
})?;

println!("Game:\n{}", game.game.get_summary_sorted());

Ok(())
}

pub fn get_print_game_command() -> Command {
Command::new("print-game")
.about("Print the game summary")
.arg(
Arg::new("game-id")
.short('i')
.help("The ID of the game to print"),
)
}

pub fn get_print_game_opts(matches: &clap::ArgMatches) -> PrintGameOpts {
let game_id: &String = matches.get_one("game-id").unwrap();

PrintGameOpts {
game_id: game_id.clone(),
}
}
65 changes: 57 additions & 8 deletions cli/src/simulate/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use mahjong_core::{ai::StandardAI, Game, GamePhase};
use std::process;

use mahjong_core::{
ai::{PlayActionResult, StandardAI},
Game, GamePhase,
};
use rustc_hash::FxHashSet;

pub use self::simulate_cli::{get_simulate_command, get_simulate_opts, SimulateOpts};
Expand All @@ -7,11 +12,19 @@ use self::stats::Stats;
mod simulate_cli;
mod stats;

#[derive(Debug)]
struct HistoryItem {
game: Game,
result: PlayActionResult,
}

pub async fn run_simulation(opts: SimulateOpts) {
let mut stats = Stats::new();

loop {
let mut game = Game::new(None);
let mut history: Option<Vec<HistoryItem>> =
if opts.debug { Some(Vec::new()) } else { None };

for player in 0..Game::get_players_num(&game.style) {
game.players.push(player.to_string());
Expand All @@ -25,13 +38,49 @@ pub async fn run_simulation(opts: SimulateOpts) {
game_ai.can_draw_round = true;

loop {
let result = game_ai.play_action();
assert!(
result.changed,
"Didn't change anything in the round\n{}\n{:?}",
game_ai.game.get_summary(),
result
);
let result = game_ai.play_action(opts.debug);

if opts.debug {
let history_vect = history.as_mut().unwrap();
history_vect.push(HistoryItem {
game: game_ai.game.clone(),
result: result.clone(),
});
}

if !result.changed {
println!("Game didn't change, breaking");
if opts.debug {
let history = history
.as_ref()
.unwrap()
.iter()
.rev()
.take(10)
.rev()
.collect::<Vec<_>>();

println!("History:");
for (idx, history_item) in history.iter().enumerate() {
if idx > 0 {
println!("---");

if idx == history.len() - 1 {
break;
}
}
println!("- {:?}", history_item.result);
println!("{}", history_item.game.get_summary());
println!("\n\n\n");
}
}
println!(
"Current state:\n{}\n{:?}",
game_ai.game.get_summary(),
result
);
process::exit(1);
}

if game_ai.game.phase == GamePhase::End {
stats.complete_game(game_ai.game);
Expand Down
23 changes: 17 additions & 6 deletions cli/src/simulate/simulate_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,32 @@ use clap::{Arg, ArgAction, Command};
#[derive(Debug, Clone, PartialEq)]
pub struct SimulateOpts {
pub once: bool,
pub debug: bool,
}

pub fn get_simulate_command() -> Command {
Command::new("simulate").about("Simulates games").arg(
Arg::new("once")
.short('o')
.help("Only run one simulation")
.action(ArgAction::SetTrue),
)
Command::new("simulate")
.about("Simulates games")
.arg(
Arg::new("once")
.short('o')
.help("Only run one simulation")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("debug")
.short('d')
.help("Store debugging information to troubleshoot issues")
.action(ArgAction::SetTrue),
)
}

pub fn get_simulate_opts(matches: &clap::ArgMatches) -> SimulateOpts {
let once: Option<&bool> = matches.get_one("once");
let debug: Option<&bool> = matches.get_one("debug");

SimulateOpts {
once: once == Some(&true),
debug: debug == Some(&true),
}
}
7 changes: 4 additions & 3 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- FE: Audio effects
- FE: Perspective of tiles
- FE: Display the last tile in board in different size
- FE: Display other player melds, and bonus tiles, in small size and on hover
- BE: leaderboard using redis
- BE: promote anonymous account to real account
- BE: decouple mahjong specific logic from server to a different package
Expand All @@ -20,19 +21,19 @@
- CORE: Support charleston in the drawing phase
- CORE: Average rounds are too high in the simulation
- CORE: Support the deciding of the dealer with dice
- CORE: Bonus tiles directions
- CORE: Support three players: high effort
- FS: Refactor logic to support multiple types of games (e.g. listed in wikipedia)
- Move most business logic to the core (rust/ts)
- FS: Support rhythym of play setting
- FS: Move more logic from web to the web_lib
- FS: Random starting position
- Move other projects bash scripts to the main scripts dir
- Convert DB operations into transactions
- Convert DB operations into transactions (there is an example)
- Change player names when they are AI
- Full AI game
- Use the game version in more endpoints
- Improve scoring logic (explicitly list points sources)
- Add unit tests
- Game hall state where it waits for other real players to join
- Statistics for moves
- Impersonate player from admin view
- Record of games for each player
Expand Down
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
)
++ (
if is-docker-ci == false && is-checks-ci == false
then [libargon2 gh entr]
then [libargon2 gh entr scc]
else []
)
++ rust.extra-shell-packages;
Expand Down
Loading

0 comments on commit 5025258

Please sign in to comment.