├── .gitignore ├── server ├── Cargo.toml ├── Cargo.lock └── src │ └── main.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | server/target 3 | ai* 4 | *.log 5 | server/log 6 | *.csv -------------------------------------------------------------------------------- /server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "server" 3 | version = "1.0.0" 4 | authors = ["yilin "] 5 | 6 | [dependencies] 7 | rand = "0.3.9" 8 | time = "0.1.32" -------------------------------------------------------------------------------- /server/Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "server" 3 | version = "1.0.0" 4 | dependencies = [ 5 | "rand 0.3.10 (registry+https://github.com/rust-lang/crates.io-index)", 6 | "time 0.1.32 (registry+https://github.com/rust-lang/crates.io-index)", 7 | ] 8 | 9 | [[package]] 10 | name = "advapi32-sys" 11 | version = "0.1.2" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | dependencies = [ 14 | "winapi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 15 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 16 | ] 17 | 18 | [[package]] 19 | name = "kernel32-sys" 20 | version = "0.1.4" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | dependencies = [ 23 | "winapi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 24 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 25 | ] 26 | 27 | [[package]] 28 | name = "libc" 29 | version = "0.1.10" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | 32 | [[package]] 33 | name = "rand" 34 | version = "0.3.10" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | dependencies = [ 37 | "advapi32-sys 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 38 | "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 39 | "winapi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 40 | ] 41 | 42 | [[package]] 43 | name = "time" 44 | version = "0.1.32" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | dependencies = [ 47 | "kernel32-sys 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 48 | "libc 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 49 | "winapi 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 50 | ] 51 | 52 | [[package]] 53 | name = "winapi" 54 | version = "0.2.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | 57 | [[package]] 58 | name = "winapi-build" 59 | version = "0.1.1" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mahjong 2 | 3 | 本项目为一个麻将服务器,供AI比赛使用。本文档分为两部分,第一部分为此服务器使用的[麻将规则](#规则),第二部分为[交互规范](#交互规则)。编译后的服务器程序下载: 4 | 5 | - [Mac](https://github.com/wormful/Mahjong/releases/download/v1.0.0/server_v1.0.0_Mac_OS_X) 6 | - [Win_x86_64](https://github.com/wormful/Mahjong/releases/download/v1.0.0/server_v1.0.0_Win_x86_64.exe) 7 | - [Ubuntu_x86_64](https://github.com/wormful/Mahjong/releases/download/v1.0.0/server_v1.0.0_Ubuntu_x86_64) 8 | 9 | 其他系统需要下载源码,使用 Rust Nightly 版本的编译器自行编译。 10 | 11 | ### 服务器使用说明 12 | 13 | 请在终端中打开服务器。用法: 14 | 15 | ``` 16 | # *nix 17 | $ ./server_bin [-v] [-d] 18 | ``` 19 | 20 | ``` 21 | # Windows 22 | server_bin [-v] [-d] 23 | ``` 24 | 25 | 开启`-d`选项后,服务器会在当前文件夹下的log文件夹中生成每盘的日志文件。 26 | 27 | ## 规则 28 | 29 | 为降低复杂度,本规则以国标麻将为基础进行了大量的简化并进行一定修改。 30 | 31 | ### 注意点 32 | 33 | * 麻将牌共136张,即在国标麻将的基础上去掉了八张花牌。 34 | * 一局定为4盘。去除了圈风、门风、庄家的概念。 35 | * 没有牌城、掷骰与开牌的概念,AI拿到的牌全部直接由服务器发放。 36 | * 出牌顺序按id为0、1、2、3,但一盘内第一个行牌的AI不确定。 37 | * 整场比赛的第一盘时,第一个行牌的AI由服务器随机决定,从第二盘开始第一个行牌的AI为最近和牌的AI。 38 | * 没有起和番数 39 | * 轮到AI出牌的时候,AI必须在1秒内打出牌,否则以100毫秒1分(四舍五入)进行罚分。 40 | * 一方打出牌后,AI若要吃牌、碰牌、杠牌或是和牌,需要在0.5秒内向服务器发出指令,超过0.5秒按每100毫秒1分(四舍五入)罚分。 41 | * 测试共有6组,每组分为4局。不同组测试中AI相对位置不同。同组内四局使用的随机数种子不变,一局结束后每位AI的位置逆时针旋转一位。 42 | 43 | ### 番种 44 | 45 | 国标麻将的番种非常多,本规则将番种大大精简。 46 | 47 | ##### 88番 48 | 49 | * 大四喜(不计碰碰和) 50 | * 大三元(不计箭刻) 51 | * 十三幺(不计五门齐、单钓将、门前清、混幺九) 52 | * 绿一色(无发记清一色,*有发记混一色*) 53 | * 四杠(不计碰碰和、三杠,暗杠另计) 54 | 55 | ##### 64番 56 | 57 | * 小四喜 58 | * 小三元(不计箭刻) 59 | * 字一色(不计碰碰和) 60 | * 四暗刻(不计门前清、碰碰和) 61 | * 清幺九(不计碰碰和) 62 | 63 | ##### 48番 64 | 65 | * 一色四同顺(不计一色三同顺、一般高) 66 | 67 | ##### 32番 68 | 69 | * 三杠(暗杠另计) 70 | * 混幺九(不计碰碰和) 71 | 72 | ##### 24番 73 | 74 | * 七对(不计门前清、单钓将) 75 | * 清一色 76 | * 一色三同顺(不计一般高) 77 | 78 | ##### 16番 79 | 80 | * 三同刻 81 | * 三暗刻 82 | 83 | ##### 8番 84 | 85 | * 三色三同顺(不计喜相逢) 86 | 87 | ##### 6番 88 | 89 | * 碰碰和 90 | * 混一色 91 | * 五门齐 92 | 93 | ##### 2番 94 | 95 | * 门前清(因取消了不求人,自摸和牌及和他家打出的牌都记) 96 | * 断幺 97 | * 平和 98 | * 箭刻(可算多次) 99 | * 暗杠(可算多次) 100 | 101 | ##### 1番 102 | 103 | * 自摸 104 | * 一般高(可算多次) 105 | * 喜相逢(可算多次) 106 | * 明杠(可算多次) 107 | * 单钓将 108 | 109 | ### 记分规则 110 | 111 | 最开始每位AI的分数都是0分。底分为4分,基本分和牌后各个番种分数的总和。 112 | 113 | 若和牌方自摸,则另外三家都需付给和牌方(底分+基本分)的分数。 114 | 115 | 否则,点炮方付给和牌方(底分+基本分)的分数,其余两家付给和牌方底分的分数。 116 | 117 | ### 结束与排名 118 | 119 | 1. 当有任何一方AI程序意外退出时比赛结束,意外关闭的AI排名最后,其余AI按分数高低排名。 120 | 2. 当总共96盘牌局结束后比赛结束,AI按分数高低排名。 121 | 122 | ## 交互规则 123 | 124 | 服务器与AI通过标准输入和标准输出进行通信,每次通信的内容为一行文本,。 125 | 126 | 示例中的“标准输入”、“标准输出”都是以AI的角度上说的。“标准输入”中的内容即AI需要读取的内容,“标准输出”中的内容为AI需要打印的内容。 127 | 128 | ### 骨牌代号 129 | 130 | * 字牌:东(E)、南(S)、西(W)、北(N)、中(Z)、发(F)、白(B) 131 | * 万子(M):1M、2M、3M、4M、5M、6M、7M、8M、9M 132 | * 索子(S):1S、2S、3S、4S、5S、6S、7S、8S、9S 133 | * 筒子(T):1T、2T、3T、4T、5T、6T、7T、8T、9T 134 | 135 | ~~PS:关于万的缩写的问题,是取自日语读音`man`…………………………~~ 136 | 137 | ### 加入比赛 138 | 139 | AI程序由服务器启动。AI先发送加入信息。 140 | 141 | 标准输出: 142 | 143 | ``` 144 | join 145 | ``` 146 | 147 | 当四位AI都加入比赛之后,服务器会把id号发送给AI。 148 | 149 | 标准输入: 150 | 151 | ``` 152 | id 3 153 | ``` 154 | 155 | 说明:3即为AI被分配到的id号。 156 | 157 | ### 开始比赛 158 | 159 | 每盘牌局开始时,服务器会广播第一个行牌的AI的id。 160 | 161 | 标准输入: 162 | 163 | ``` 164 | first 3 165 | ``` 166 | 167 | 说明:id为3的AI为第一个行牌的AI。 168 | 169 | 然后每个AI会收到13张起手牌。 170 | 171 | 标准输入: 172 | 173 | ``` 174 | init 1S 1S 2T 5M 9S 4T W Z 1T 8M 9M 5S 6T 175 | ``` 176 | 177 | 说明:起手13张牌分别为一索、一索、二筒、五万、九索、四筒、西风、红中、一筒、八万、九万、五索、六筒。 178 | 179 | ### 行牌 180 | 181 | 要注意行牌是有时间限制的。 182 | 183 | ##### 拿牌 184 | 185 | 到AI拿牌的时候,服务器会将拿牌信息发给AI,同时向其他AI广播。 186 | 187 | 标准输入(拿牌AI): 188 | 189 | ``` 190 | pick 5T 191 | ``` 192 | 193 | 说明:拿到的牌为五筒。 194 | 195 | 标准输入(其他AI): 196 | 197 | ``` 198 | mpick 0 199 | ``` 200 | 201 | 说明:id为0的AI拿牌了。 202 | 203 | ##### 出牌 204 | 205 | 出牌需要AI在1秒内发送指令给服务器,然后服务器将出牌信息广播给其他AI。 206 | 207 | 标准输出(出牌AI): 208 | 209 | ``` 210 | out 9M 211 | ``` 212 | 213 | 说明:需要打出的牌为九万。 214 | 215 | 标准输入(其他AI): 216 | 217 | ``` 218 | mout 2 9M 219 | ``` 220 | 221 | 说明:id为2的AI打出了一张九万。 222 | 223 | 这时三位AI有0.5秒的时间向服务器发出吃、碰、杠或和的指令。超过0.5秒按每100毫秒1分罚分。 224 | 225 | 如果不发出吃、碰、杠或和的指令的话: 226 | 227 | 标准输出: 228 | 229 | ``` 230 | pass 231 | ``` 232 | 233 | ###### 吃/碰牌 234 | 235 | 由于吃的方式不唯一,所以吃的指令需要另外指定形成顺子中最小的一张牌。 236 | 237 | 标准输出(AI1): 238 | 239 | ``` 240 | chi 7M 241 | ``` 242 | 243 | 说明:报吃,形成七万、八万、九万一副顺子。 244 | 注1:吃不一定成功。 245 | 注2:如上例,即使吃的方式唯一,也需要另指定牌。 246 | 247 | 标准输出(AI2): 248 | 249 | ``` 250 | peng 251 | ``` 252 | 253 | 说明:报碰。 254 | 255 | 当吃和碰的指令都发送给服务器时,碰优先于吃。然后服务器会广播此消息。 256 | 257 | 标准输入(所有): 258 | 259 | ``` 260 | mpeng 2 9M 261 | ``` 262 | 263 | 说明:id为2的AI碰了九万。 264 | 265 | 吃没有成功(发送在`mpeng`/`mgang`消息之后): 266 | 267 | 标准输入(AI1): 268 | 269 | ``` 270 | mfail 271 | ``` 272 | 273 | 若是吃成功的情况: 274 | 275 | 标准输入(所有): 276 | 277 | ``` 278 | mchi 1 7M 279 | ``` 280 | 281 | 说明:id为1的AI吃成功,形成七万、八万、九万一副顺子。 282 | 283 | ###### 大明杠 284 | 285 | 标准输出(AI3): 286 | 287 | ``` 288 | gang 289 | ``` 290 | 291 | 若杠成功: 292 | 293 | 标准输入(所有): 294 | 295 | ``` 296 | mgang 3 5S 297 | ``` 298 | 299 | 说明:id为3的AI明杠五索。 300 | 301 | ###### 暗杠 302 | 303 | 标准输出(AI3): 304 | 305 | ``` 306 | agang W 307 | ``` 308 | 309 | 标准输出(所有): 310 | 311 | ``` 312 | magang 3 313 | ``` 314 | 315 | 说明:id为3的AI暗杠了。 316 | 317 | ###### 加杠 318 | 319 | 标准输出:(AI3): 320 | 321 | ``` 322 | jgang 5S 323 | ``` 324 | 325 | 说明:需要加杠五索。 326 | 327 | 标准输入(所有): 328 | 329 | ``` 330 | mjgang 3 5S 331 | ``` 332 | 333 | 说明:id为3的AI加杠五索。 334 | 335 | ###### 抢杠和 336 | 337 | 当接到有别的AI加杠的消息时候,如要抢杠,需要在0.5秒内发出抢杠和指令。 338 | 339 | 标准输出:(AI2): 340 | 341 | ``` 342 | qgang 343 | ``` 344 | 345 | 抢杠成功的话,和牌结算同下。 346 | 347 | ###### 和牌 348 | 349 | 在出牌阶段和别家打完牌后都可以发出和牌指令: 350 | 351 | 标准输出(AI3): 352 | 353 | ``` 354 | hu 355 | ``` 356 | 357 | 当一方有效和牌后,会广播该AI和牌的番数。分数将自动扣除并加到和牌方。 358 | 359 | 当有人和牌或流局后即关闭所有AI程序。 -------------------------------------------------------------------------------- /server/src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(mpsc_select)] 2 | extern crate rand; 3 | extern crate time; 4 | 5 | use std::process; 6 | use std::process::*; 7 | use std::io::*; 8 | use std::thread; 9 | use std::env; 10 | use std::rc::Rc; 11 | use std::cell::RefCell; 12 | use rand::*; 13 | use std::sync::*; 14 | use std::sync::mpsc::*; 15 | use std::collections::*; 16 | use std::cmp::*; 17 | use time::*; 18 | use std::fs::File; 19 | use std::fs; 20 | 21 | static mut verbose: bool = false; 22 | static mut debug: bool = false; 23 | 24 | fn print(s: String) { 25 | unsafe { 26 | if verbose { 27 | print!("{}", s); 28 | } 29 | } 30 | } 31 | 32 | fn println(s: String) { 33 | print(format!("{}\n", s)); 34 | } 35 | 36 | fn main() { 37 | let mut args: Vec<_> = env::args().collect(); 38 | unsafe { 39 | verbose = false; 40 | debug = false; 41 | match args.iter().position(|x| x == "-v") { 42 | Some(index) => { 43 | verbose = true; 44 | args.remove(index); 45 | }, 46 | _ => () 47 | }; 48 | match args.iter().position(|x| x == "-d") { 49 | Some(index) => { 50 | debug = true; 51 | args.remove(index); 52 | }, 53 | _ => () 54 | }; 55 | } 56 | if args.len() != 5 { 57 | println!( 58 | "Usage: ./server [-v] [-d]\n\t[-v]:\t\tVerbose mode\n\t[-d]:\t\tDebug mode (Output game logs)"); 59 | process::exit(1); 60 | } 61 | let mut board: HashMap = HashMap::new(); 62 | for i in 1..5 { 63 | board.insert(args[i].to_string(), 0); 64 | } 65 | println("$ Game start!".to_string()); 66 | let positions = vec![ 67 | [[1, 2, 3, 4], [2, 3, 4, 1], [3, 4, 1, 2], [4, 1, 2, 3]], 68 | [[1, 3, 2, 4], [3, 2, 4, 1], [2, 4, 1, 3], [4, 1, 3, 2]], 69 | [[1, 3, 4, 2], [3, 4, 2, 1], [4, 2, 1, 3], [2, 1, 3, 4]], 70 | [[1, 4, 2, 3], [4, 2, 3, 1], [2, 3, 1, 4], [3, 1, 4, 2]], 71 | [[1, 4, 3, 2], [4, 3, 2, 1], [3, 2, 1, 4], [2, 1, 4, 3]], 72 | [[1, 2, 4, 3], [2, 4, 3, 1], [4, 3, 1, 2], [3, 1, 2, 4]], 73 | ]; 74 | let mut round = 0; 75 | let time_sec = get_time().sec; 76 | 77 | let scoresheet_filename = unsafe { 78 | if debug { 79 | format!("scoresheet_{}.csv", time_sec) 80 | } else { 81 | match std::env::consts::OS { 82 | "windows" => "NUL", 83 | _ => "/dev/null" 84 | } 85 | .to_string() 86 | } 87 | }; 88 | let mut scoresheet = LineWriter::new(File::create(scoresheet_filename).unwrap()); 89 | scoresheet.write(b"ScoreSheet\r\n").ok(); 90 | scoresheet.write_fmt(format_args!("Mahjong Contest {}\r\n", time_sec)).ok(); 91 | scoresheet 92 | .write_fmt(format_args!("Player 0:,{},Player 1:,{},Player 2:,{},Player 3:,{}\r\n\r\n", args[1], args[2], args[3], args[4])).ok(); 93 | scoresheet.write_fmt(format_args!("Round,Game,Player0,Player1,Player2,Player3,Game ID\r\n")) 94 | .ok(); 95 | 96 | for group in &positions { 97 | let seed = gen_seed(); 98 | println("$ Generated new random seeds".to_string()); 99 | for position in group { 100 | let rng = Rc::new(RefCell::new(StdRng::from_seed(&seed.clone()))); 101 | round += 1; 102 | for i in 1..5 { 103 | print!("\r"); 104 | println("\n".to_string()); 105 | print!("Round {} Game {}", round, i); 106 | println("\n".to_string()); 107 | std::io::stdout().flush().ok(); 108 | println(format!("$ The positions are {:?}", position)); 109 | let paths = [ 110 | args[position[0]].clone(), args[position[1]].clone(), 111 | args[position[2]].clone(), args[position[3]].clone() 112 | ]; 113 | let mut game = Game::new(paths, rng.clone()); 114 | game.log.write_fmt(format_args!("ver {}\r\n", "1.0")).ok(); 115 | game.log.write_fmt(format_args!("Mahjong Contest Round {} Game {}\r\n", round, i)) 116 | .ok(); 117 | game.run(); 118 | println(format!("$ This hand's score: {:?}", game.score)); 119 | let mut scores = [0; 4]; 120 | for i in 0..4 { 121 | scores[position[i] - 1] = game.score[i]; 122 | } 123 | scoresheet.write_fmt(format_args!("{},{},{},{},{},{},{:x}\r\n", round, i, scores[0], scores[1],scores[2],scores[3],game.gid)).ok(); 124 | let _board = board.clone(); 125 | for i in 0..4 { 126 | let score = _board.get(&args[position[i]].to_string()).unwrap(); 127 | let add = game.score[i]; 128 | board.insert(args[position[i]].to_string(), score + add); 129 | println(format!("$ AI{} shut", i)); 130 | let id = game.pids[i]; 131 | println(format!("AI{} id={}", i, id)); 132 | match std::env::consts::OS { 133 | "windows" => { 134 | Command::new("taskkill") 135 | .arg("/PID") 136 | .arg(id.to_string()) 137 | .arg("/F") 138 | .arg("/T") 139 | .output() 140 | .ok(); 141 | }, 142 | _ => { 143 | Command::new("kill") 144 | .arg("-s") 145 | .arg("KILL") 146 | .arg(id.to_string()) 147 | .output() 148 | .ok(); 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } 155 | scoresheet.write(b"\r\n").ok(); 156 | scoresheet.write_fmt(format_args!("Total,,{},{},{},{}",board[&args[1]],board[&args[2]],board[&args[3]],board[&args[4]])).ok(); 157 | println!("\rFinal score: "); 158 | let mut scores: Vec<_> = board.iter().collect(); 159 | scores.sort_by(|a, b| b.1.cmp(a.1)); 160 | for i in 0..4 { 161 | println!("{}: {}", scores[i].0, scores[i].1); 162 | } 163 | } 164 | 165 | fn gen_seed() -> [usize; 8] { 166 | let mut seed = [0; 8]; 167 | for i in 0..8 { 168 | seed[i] = usize::rand(&mut rand::thread_rng()); 169 | } 170 | seed 171 | } 172 | 173 | #[derive(Clone, Debug)] 174 | struct Tiles { 175 | hands: Vec, 176 | chows: Vec, 177 | pungs: Vec, 178 | kongs: Vec, 179 | ckongs: Vec, 180 | cchows: Vec, 181 | cpungs: Vec 182 | } 183 | 184 | struct Game { 185 | rng: Rc>, 186 | paths: [String; 4], 187 | stage: String, 188 | inputs: Vec, 189 | join_counter: HashSet, 190 | action_id: usize, 191 | tiles: Vec, 192 | left: Vec, 193 | last_time: PreciseTime, 194 | last_tile: String, 195 | score: [i64; 4], 196 | messages: HashMap, 197 | base: i64, 198 | pids: [u32; 4], 199 | gid: u64, 200 | log: LineWriter 201 | } 202 | 203 | static mut flags: [bool; 4] = [true, true, true, true]; 204 | 205 | static mut close_flags: [bool; 4] = [true, true, true, true]; 206 | 207 | impl Game { 208 | fn new(paths: [String; 4], rng: Rc>) -> Game { 209 | let tiles = [ 210 | "1M", "2M", "3M", "4M", "5M", "6M", "7M", "8M", "9M", "1S", "2S", "3S", "4S", "5S", 211 | "6S", "7S", "8S", "9S", "1T", "2T", "3T", "4T", "5T", "6T", "7T", "8T", "9T", "E", "S", 212 | "W", "N", "Z", "F", "B", "1M", "2M", "3M", "4M", "5M", "6M", "7M", "8M", "9M", "1S", 213 | "2S", "3S", "4S", "5S", "6S", "7S", "8S", "9S", "1T", "2T", "3T", "4T", "5T", "6T", 214 | "7T", "8T", "9T", "E", "S", "W", "N", "Z", "F", "B", "1M", "2M", "3M", "4M", "5M", 215 | "6M", "7M", "8M", "9M", "1S", "2S", "3S", "4S", "5S", "6S", "7S", "8S", "9S", "1T", 216 | "2T", "3T", "4T", "5T", "6T", "7T", "8T", "9T", "E", "S", "W", "N", "Z", "F", "B", 217 | "1M", "2M", "3M", "4M", "5M", "6M", "7M", "8M", "9M", "1S", "2S", "3S", "4S", "5S", 218 | "6S", "7S", "8S", "9S", "1T", "2T", "3T", "4T", "5T", "6T", "7T", "8T", "9T", "E", "S", 219 | "W", "N", "Z", "F", "B" 220 | ]; 221 | let mut left: Vec<_> = tiles.iter().map(|x| x.to_string()).collect(); 222 | rng.borrow_mut().shuffle(&mut left); 223 | unsafe { 224 | flags = [true, true, true, true]; 225 | close_flags = [true, true, true, true]; 226 | if debug { 227 | fs::create_dir_all("log").ok(); 228 | } 229 | } 230 | let gid = thread_rng().gen_range(0x100000000000000u64, 0x1000000000000000u64); 231 | let logfile_name = unsafe { 232 | if debug { 233 | format!("log/{:x}.mahjong.log", gid) 234 | } else { 235 | match std::env::consts::OS { 236 | "windows" => "NUL", 237 | _ => "/dev/null" 238 | } 239 | .to_string() 240 | } 241 | }; 242 | Game { 243 | rng: rng, 244 | paths: paths, 245 | stage: "join".to_string(), 246 | inputs: Vec::new(), 247 | join_counter: HashSet::new(), 248 | action_id: 0, 249 | tiles: Vec::new(), 250 | left: left, 251 | last_time: PreciseTime::now(), 252 | score: [0; 4], 253 | last_tile: String::new(), 254 | messages: HashMap::new(), 255 | base: 4, 256 | pids: [0, 0, 0, 0], 257 | gid: gid, 258 | log: LineWriter::new(File::create(logfile_name).unwrap()) 259 | } 260 | } 261 | 262 | fn shut_ai(&mut self, id: usize) { 263 | unsafe { 264 | flags[id] = false; 265 | match std::env::consts::OS { 266 | "windows" => { 267 | Command::new("taskkill") 268 | .arg("/PID") 269 | .arg(self.pids[id].to_string()) 270 | .arg("/F") 271 | .arg("/T") 272 | .output() 273 | .ok(); 274 | }, 275 | _ => { 276 | Command::new("kill") 277 | .arg("-s") 278 | .arg("KILL") 279 | .arg(self.pids[id].to_string()) 280 | .output() 281 | .ok(); 282 | } 283 | } 284 | while close_flags[id] { 285 | thread::sleep_ms(10); 286 | } 287 | println(format!("$ AI{} closed", id)); 288 | } 289 | } 290 | 291 | fn run(&mut self) { 292 | let (tx, rx) = mpsc::channel(); 293 | for i in 0..4 { 294 | let command = Command::new(&self.paths[i]) 295 | .stdin(Stdio::piped()) 296 | .stdout(Stdio::piped()) 297 | .spawn() 298 | .unwrap(); 299 | self.log.write_fmt(format_args!("{}\r\n",self.paths[i])).ok(); 300 | let id = command.id(); 301 | self.pids[i] = id; 302 | self.inputs.push(command.stdin.unwrap()); 303 | let tx = tx.clone(); 304 | let mut output = BufReader::new(command.stdout.unwrap()); 305 | thread::spawn(move || { 306 | unsafe { 307 | println(format!("$ AI{} started", i)); 308 | while flags[i] { 309 | let mut result = String::new(); 310 | output.read_line(&mut result).ok(); 311 | let message = result.trim().to_string(); 312 | if message.len() > 0 { 313 | tx.send(Message { id: i, message: message }).ok(); 314 | } else { 315 | tx.send(Message { id: i, message: "CLOSE".to_string() }).ok(); 316 | break; 317 | } 318 | } 319 | close_flags[i] = false; 320 | println(format!("$ AI{} abandoned", i)); 321 | } 322 | }); 323 | } 324 | self._loop(rx); 325 | } 326 | 327 | fn _loop(&mut self, rx: Receiver) { 328 | loop { 329 | unsafe { 330 | if flags == [false, false, false, false] { 331 | return; 332 | } 333 | let (tx1, rx1) = mpsc::channel(); 334 | 335 | let last_time = self.last_time; 336 | let handle = thread::spawn(move || { 337 | let duration = 5000 - last_time.to(PreciseTime::now()).num_milliseconds(); 338 | let duration = if duration < 0 { 0 } else { duration as u32 }; 339 | thread::park_timeout_ms(duration); 340 | tx1.send(()).ok(); 341 | }); 342 | select! { 343 | msg = rx.recv() => { 344 | match msg { 345 | Ok(msg) => { 346 | handle.thread().unpark(); 347 | handle.join().ok(); 348 | if msg.message == "CLOSE" { 349 | self.shut_ai(msg.id); 350 | if self.stage == "outwait" || self.stage == "qgwait" { 351 | self.process(Message { 352 | id: msg.id, 353 | message: "pass".to_string() 354 | }); 355 | } else { 356 | let last_tile = self.tiles[msg.id].hands[0].to_string(); 357 | self.process(Message { 358 | id: msg.id, 359 | message: format!("out {}", last_tile).to_string() 360 | }); 361 | } 362 | continue; 363 | } 364 | self.process(msg); 365 | }, 366 | _ => () 367 | }; 368 | }, 369 | _ = rx1.recv() => { 370 | if self.stage == "outwait" || self.stage == "qgwait" { 371 | for i in 0..4 { 372 | if i != self.action_id && flags[i] && 373 | self.messages.contains_key(&i) { 374 | self.shut_ai(i); 375 | self.process(Message { 376 | id: i, 377 | message: "pass".to_string() 378 | }); 379 | } 380 | } 381 | } else { 382 | let action_id = self.action_id; 383 | let last_tile = self.tiles[action_id].hands[0].to_string(); 384 | self.process(Message { 385 | id: action_id, 386 | message: format!("out {}", last_tile).to_string() 387 | }); 388 | } 389 | continue; 390 | } 391 | } 392 | } 393 | } 394 | } 395 | 396 | fn process(&mut self, msg: Message) { 397 | println(format!("$ Received message: {:?}", msg)); 398 | if self.stage == "outwait" || self.stage == "qgwait" { 399 | if msg.id == self.action_id { 400 | return; 401 | } 402 | let duration = self.last_time.to(PreciseTime::now()).num_milliseconds(); 403 | if duration >= 550 { 404 | let penalty = (duration - 450) / 100; 405 | self.score[msg.id] -= penalty; 406 | println(format!("$ {} was fined {} due to timeout", msg.id, penalty)); 407 | } 408 | self.messages.insert(msg.id, msg); 409 | println(format!("$ Added to queue! Queue size: {}", self.messages.len())); 410 | unsafe { 411 | let mut count = 0; 412 | for i in 0..4 { 413 | if i != self.action_id && flags[i] { 414 | count += 1; 415 | } 416 | } 417 | if self.messages.len() >= count { 418 | if self.stage == "outwait" { 419 | self.outwait(); 420 | } else { 421 | self.qgwait(); 422 | } 423 | } 424 | } 425 | } else { 426 | let v: Vec<&str> = msg.message.split(' ').collect(); 427 | println(format!("$ vector = {:?}", v)); 428 | match v[0].trim() { 429 | "join" => self.join(msg.id), 430 | "out" => self.out(msg.id, v[1].trim().to_string()), 431 | "agang" => self.agang(msg.id, v[1].trim().to_string()), 432 | "jgang" => self.jgang(msg.id, v[1].trim().to_string()), 433 | "hu" => self.tsumo(msg.id), 434 | _ => () 435 | } 436 | } 437 | } 438 | 439 | fn tsumo(&mut self, id: usize) { 440 | if self.stage != "out" || id != self.action_id { 441 | println(format!("$ {} sent invalid hu", id)); 442 | return; 443 | } 444 | let time = PreciseTime::now(); 445 | match cal_fan(self.tiles[id].clone(), self.last_tile.clone(), true) { 446 | Some(tuple) => { 447 | let (x, fans) = tuple; 448 | let duration = self.last_time.to(time).num_milliseconds(); 449 | if duration >= 1050 { 450 | let penalty = (duration - 950) / 100; 451 | self.score[id] -= penalty; 452 | println(format!("$ {} was fined {} due to timeout", id, penalty)); 453 | } 454 | self.score[id] += 3 * (x + self.base); 455 | for i in 0..4 { 456 | if i != id { 457 | self.score[i] -= x + self.base; 458 | } 459 | } 460 | println(format!("$ {} tsumo!", id)); 461 | self.log.write_fmt(format_args!("hu {}\r\n",id)).ok(); 462 | self.log.write(b"\r\n").ok(); 463 | self.log.write_fmt(format_args!("win {} tsumo\r\n",id)).ok(); 464 | self.log.write_fmt(format_args!("fans {}\r\n",fans.len())).ok(); 465 | for (fan, value) in &fans { 466 | self.log.write_fmt(format_args!("{}:{}\r\n",fan,value)).ok(); 467 | } 468 | self.log.write_fmt(format_args!("score {}\r\n",x)).ok(); 469 | unsafe { 470 | for i in 0..4 { 471 | flags[i] = false; 472 | } 473 | } 474 | }, 475 | None => { 476 | println(format!("$ {} sent invalid hu", id)); 477 | println(format!("$ {}'s tiles are: {:?}", id, self.tiles[id].hands)); 478 | self.shut_ai(id); 479 | let tile = self.last_tile.clone(); 480 | self.out(id, tile); 481 | } 482 | } 483 | } 484 | 485 | fn qgwait(&mut self) { 486 | let mut messages = Vec::new(); 487 | for _msg in self.messages.values() { 488 | if _msg.message.trim() != "pass" { 489 | messages.push(_msg.clone()); 490 | } 491 | } 492 | messages.sort_by(|a, b| ((a.id + 4 - self.action_id) % 4) 493 | .cmp(&((b.id + 4 - self.action_id) % 4))); 494 | for msg in messages { 495 | if msg.id == self.action_id { continue; } 496 | match msg.message.trim() { 497 | "qgang" => { 498 | if self.hu(msg.id) { 499 | println(format!("$ {} robbed the kong", msg.id)); 500 | unsafe { 501 | for i in 0..4 { 502 | flags[i] = false; 503 | } 504 | } 505 | return; 506 | } 507 | }, 508 | _ => () 509 | } 510 | } 511 | self.pick(); 512 | } 513 | 514 | fn outwait(&mut self) { 515 | let mut messages = Vec::new(); 516 | for _msg in self.messages.values() { 517 | if _msg.message.trim() != "pass" { 518 | messages.push(_msg.clone()); 519 | } 520 | } 521 | messages.sort_by(|a, b| { 522 | let o1 = match a.message.split(' ').next().unwrap() { 523 | "hu" => (a.id + 4 - self.action_id) % 4, 524 | "gang" => 16, 525 | "peng" => 64, 526 | "chi" => 254, 527 | _ => 255 528 | }; 529 | let o2 = match b.message.split(' ').next().unwrap() { 530 | "hu" => (b.id + 4 - self.action_id) % 4, 531 | "gang" => 16, 532 | "peng" => 64, 533 | "chi" => 254, 534 | _ => 255 535 | }; 536 | o1.cmp(&o2) 537 | }); 538 | let mut sort = "fail"; 539 | let prev_id = self.action_id; 540 | for msg in messages { 541 | if msg.id == self.action_id { continue; } 542 | let v: Vec<&str> = msg.message.split(' ').collect(); 543 | match v[0].trim() { 544 | "hu" => { 545 | if self.hu(msg.id) { 546 | println(format!("{} hu!", msg.id)); 547 | unsafe { 548 | for i in 0..4 { 549 | flags[i] = false; 550 | } 551 | } 552 | return; 553 | } 554 | }, 555 | "gang" => { 556 | if self.gang(msg.id) { 557 | for i in 0..4 { 558 | unsafe { 559 | if !flags[i] { 560 | continue; 561 | } 562 | } 563 | self.inputs[i] 564 | .write(format!("mgang {} {}\r\n", msg.id, self.last_tile.clone()) 565 | .to_string() 566 | .as_bytes()) 567 | .ok(); 568 | print(format!("Sent to {}: {}", i, format!("mgang {} {}\r\n", msg.id, 569 | self.last_tile.clone()))); 570 | 571 | self.inputs[i].flush().ok(); 572 | } 573 | self.action_id = msg.id; 574 | sort = "gang"; 575 | break; 576 | } 577 | }, 578 | "peng" => { 579 | if self.peng(msg.id) { 580 | for i in 0..4 { 581 | unsafe { 582 | if !flags[i] { 583 | continue; 584 | } 585 | } 586 | self.inputs[i] 587 | .write(format!("mpeng {} {}\r\n", msg.id, self.last_tile.clone()) 588 | .to_string() 589 | .as_bytes()) 590 | .ok(); 591 | print(format!("Sent to {}: {}", i, format!("mpeng {} {}\r\n", msg.id, 592 | self.last_tile.clone()))); 593 | 594 | self.inputs[i].flush().ok(); 595 | } 596 | self.action_id = msg.id; 597 | sort = "peng"; 598 | break; 599 | } 600 | }, 601 | "chi" => { 602 | if self.chi(msg.id, v[1].trim()) { 603 | for i in 0..4 { 604 | unsafe { 605 | if !flags[i] { 606 | continue; 607 | } 608 | } 609 | self.inputs[i] 610 | .write(format!("mchi {} {}\r\n", msg.id, v[1].trim()) 611 | .to_string() 612 | .as_bytes()) 613 | .ok(); 614 | print(format!("Sent to {}: {}", i, 615 | format!("mchi {} {}\r\n", msg.id, v[1].trim()))); 616 | 617 | self.inputs[i].flush().ok(); 618 | } 619 | self.action_id = msg.id; 620 | sort = "chi"; 621 | break; 622 | } 623 | }, 624 | _ => () 625 | } 626 | } 627 | if sort != "chi" { 628 | let post = post_pos(prev_id); 629 | match self.messages.get(&post) { 630 | Some(msg) => { 631 | if msg.message.split(' ').next().unwrap() == "chi" { 632 | println(format!("$ {} failed to chi", post)); 633 | self.inputs[post].write("mfail\r\n".to_string().as_bytes()).ok().expect( 634 | "L603"); 635 | print(format!("Sent to {}: mfail\r\n", post)); 636 | 637 | self.inputs[post].flush().ok(); 638 | } 639 | }, 640 | None => () 641 | } 642 | } 643 | if sort == "gang" { 644 | self.pick(); 645 | } else if sort == "fail" { 646 | self.action_id = post_pos(self.action_id); 647 | self.pick(); 648 | } else { 649 | self.stage = "out".to_string(); 650 | } 651 | } 652 | 653 | fn agang(&mut self, id: usize, tile: String) { 654 | if self.stage != "out" || id != self.action_id { 655 | println(format!("$ {} sent invalid agang", id)); 656 | self.shut_ai(id); 657 | let tile = self.last_tile.clone(); 658 | self.out(id, tile); 659 | return; 660 | } 661 | if self.tiles[id].hands.iter().filter(|&x| *x == tile).count() == 4 { 662 | let duration = self.last_time.to(PreciseTime::now()).num_milliseconds(); 663 | if duration >= 1050 { 664 | let penalty = (duration - 950) / 100; 665 | self.score[id] -= penalty; 666 | println(format!("$ {} was fined {} due to timeout", id, penalty)); 667 | } 668 | self.tiles[id].hands.retain(|x| x != &tile); 669 | self.tiles[id].ckongs.push(tile.to_string()); 670 | println(format!("$ {} gang {} concealedly", id, tile)); 671 | self.log.write_fmt(format_args!("agang {} {}\r\n",id,tile)).ok(); 672 | for i in 0..4 { 673 | unsafe { 674 | if !flags[i] { 675 | continue; 676 | } 677 | } 678 | self.inputs[i].write(format!("magang {}\r\n", id).to_string().as_bytes()).ok(); 679 | print(format!("Sent to {}: {}", i, format!("magang {}\r\n", id))); 680 | self.inputs[i].flush().ok(); 681 | } 682 | self.pick(); 683 | } else { 684 | println(format!("$ {} sent invalid agang", id)); 685 | println(format!("$ {}'s tiles are: {:?}", id, self.tiles[id].hands)); 686 | self.shut_ai(id); 687 | let tile = self.last_tile.clone(); 688 | self.out(id, tile); 689 | } 690 | } 691 | 692 | fn jgang(&mut self, id: usize, tile: String) { 693 | if self.stage != "out" || id != self.action_id { 694 | return; 695 | } 696 | if self.tiles[id].hands.contains(&tile) && self.tiles[id].pungs.contains(&tile) { 697 | let duration = self.last_time.to(PreciseTime::now()).num_milliseconds(); 698 | if duration >= 1050 { 699 | let penalty = (duration - 950) / 100; 700 | self.score[id] -= penalty; 701 | println(format!("$ {} was fined {} due to timeout", id, penalty)); 702 | } 703 | let index = self.tiles[id].hands.iter().position(|x| *x == tile).unwrap(); 704 | self.tiles[id].hands.remove(index); 705 | let index = self.tiles[id].pungs.iter().position(|x| *x == tile).unwrap(); 706 | self.tiles[id].pungs.remove(index); 707 | self.tiles[id].kongs.push(tile.clone()); 708 | println(format!("$ {} gang {} by adding", id, tile)); 709 | self.log.write_fmt(format_args!("jgang {} {}\r\n",id,tile)).ok(); 710 | for i in 0..4 { 711 | unsafe { 712 | if !flags[i] { 713 | continue; 714 | } 715 | } 716 | self.inputs[i] 717 | .write(format!("mjgang {} {}\r\n", id, tile).to_string().as_bytes()) 718 | .ok(); 719 | print(format!("Sent to {}: {}", i, format!("mjgang {} {}\r\n", id, tile))); 720 | self.inputs[i].flush().ok(); 721 | } 722 | self.stage = "qgwait".to_string(); 723 | self.last_tile = tile; 724 | self.messages.clear(); 725 | println("$ Waiting for action".to_string()); 726 | self.last_time = PreciseTime::now(); 727 | } else { 728 | println(format!("$ {} sent invalid jgang", id)); 729 | println(format!("$ {}'s tiles are: {:?}", id, self.tiles[id].hands)); 730 | self.shut_ai(id); 731 | let tile = self.last_tile.clone(); 732 | self.out(id, tile); 733 | } 734 | } 735 | 736 | fn join(&mut self, id: usize) { 737 | if self.stage != "join" { 738 | println(format!("$ {} sent invalid join message", id)); 739 | return; 740 | } 741 | self.join_counter.insert(id); 742 | println(format!("$ {} joined", id)); 743 | if self.join_counter.len() == 4 { 744 | for i in 0..4 { 745 | unsafe { 746 | if !flags[i] { 747 | continue; 748 | } 749 | } 750 | self.inputs[i].write(format!("id {}\r\n", i).as_bytes()).ok(); 751 | print(format!("Sent to {}: {}", i, format!("id {}\r\n", i))); 752 | self.inputs[i].flush().ok(); 753 | } 754 | self.start(); 755 | } 756 | } 757 | 758 | fn start(&mut self) { 759 | self.action_id = self.rng.borrow_mut().gen_range(0, 4); 760 | println(format!("$ {} acts first", self.action_id)); 761 | self.log.write_fmt(format_args!("{}\r\n",self.action_id)).ok(); 762 | for i in 0..4 { 763 | unsafe { 764 | if !flags[i] { 765 | continue; 766 | } 767 | } 768 | self.inputs[i].write(format!("first {}\r\n", self.action_id).as_bytes()).ok().expect( 769 | "L737"); 770 | print(format!("Sent to {}: {}", i, format!("first {}\r\n", self.action_id))); 771 | self.inputs[i].flush().ok(); 772 | } 773 | self.init(); 774 | } 775 | 776 | fn init(&mut self) { 777 | for i in 0..4 { 778 | unsafe { 779 | if !flags[i] { 780 | continue; 781 | } 782 | } 783 | let mut output = "init".to_string(); 784 | self.tiles.push(Tiles { 785 | hands: Vec::new(), 786 | chows: Vec::new(), 787 | pungs: Vec::new(), 788 | kongs: Vec::new(), 789 | ckongs: Vec::new(), 790 | cchows: Vec::new(), 791 | cpungs: Vec::new() 792 | }); 793 | print(format!("$ {}'s first 13 tiles are:", i)); 794 | for j in 0..13 { 795 | let tile = self.left.pop().unwrap(); 796 | output.push_str(" "); 797 | output.push_str(&tile); 798 | print(format!("{} ", tile)); 799 | if j != 0 { 800 | self.log.write(b" ").ok(); 801 | } 802 | self.log.write_fmt(format_args!("{}",tile)).ok(); 803 | self.tiles[i].hands.push(tile); 804 | } 805 | println(String::new()); 806 | output.push_str("\r\n"); 807 | self.log.write(b"\r\n").ok(); 808 | self.inputs[i].write(output.as_bytes()).ok(); 809 | print(format!("Sent to {}: {}", i, output)); 810 | self.inputs[i].flush().ok(); 811 | } 812 | self.log.write(b"\r\n").ok(); 813 | self.pick(); 814 | } 815 | 816 | fn pick(&mut self) { 817 | if self.left.len() == 0 { 818 | self.draw(); 819 | return; 820 | } 821 | unsafe { 822 | let tile = self.left.pop().unwrap(); 823 | println(format!("$ {} picked {}", self.action_id, tile)); 824 | self.log.write_fmt(format_args!("pick {} {}\r\n",self.action_id,tile)).ok(); 825 | self.tiles[self.action_id].hands.push(tile.clone()); 826 | print(format!("Sent to {}: {}", self.action_id, format!("pick {}\r\n", tile))); 827 | if flags[self.action_id] { 828 | self.inputs[self.action_id] 829 | .write(format!("pick {}\r\n", tile).to_string().as_bytes()) 830 | .ok(); 831 | self.inputs[self.action_id].flush().ok(); 832 | } 833 | for i in 0..4 { 834 | if !flags[i] { 835 | continue; 836 | } 837 | if i == self.action_id { continue; } 838 | self.inputs[i] 839 | .write(format!("mpick {}\r\n", self.action_id).to_string().as_bytes()) 840 | .ok(); 841 | print(format!("Sent to {}: {}", i, format!("mpick {}\r\n", self.action_id))); 842 | self.inputs[i].flush().ok(); 843 | } 844 | self.last_tile = tile.clone(); 845 | self.stage = "out".to_string(); 846 | if flags[self.action_id] { 847 | println("$ Waiting for action".to_string()); 848 | self.last_time = PreciseTime::now(); 849 | } else { 850 | self.last_time = PreciseTime::now(); 851 | println(format!("$ Pass AI {} due to invalid operation", self.action_id)); 852 | let id = self.action_id; 853 | self.out(id, tile); 854 | } 855 | } 856 | } 857 | 858 | fn out(&mut self, id: usize, tile: String) { 859 | if self.stage != "out" || id != self.action_id { 860 | println(format!("$ {} sent invalid out", id)); 861 | return; 862 | } 863 | match self.tiles[id].hands.iter().position(|x| *x == tile) { 864 | Some(index) => { 865 | println(format!("$ {} discarded {}", id, tile)); 866 | self.log.write_fmt(format_args!("out {} {}\r\n",id,tile)).ok(); 867 | let duration = self.last_time.to(PreciseTime::now()).num_milliseconds(); 868 | if duration >= 1050 { 869 | let penalty = (duration - 950) / 100; 870 | self.score[id] -= penalty; 871 | println(format!("$ {} was fined {} due to timeout", id, penalty)); 872 | } 873 | self.tiles[id].hands.remove(index); 874 | for i in 0..4 { 875 | unsafe { 876 | if !flags[i] { 877 | continue; 878 | } 879 | } 880 | if i == self.action_id { continue; } 881 | self.inputs[i] 882 | .write(format!("mout {} {}\r\n", self.action_id, tile) 883 | .to_string() 884 | .as_bytes()) 885 | .ok(); 886 | print(format!("Sent to {}: {}", i, 887 | format!("mout {} {}\r\n", self.action_id, tile))); 888 | self.inputs[i].flush().ok(); 889 | } 890 | self.last_tile = tile; 891 | self.stage = "outwait".to_string(); 892 | self.messages.clear(); 893 | self.last_time = PreciseTime::now(); 894 | }, 895 | None => { 896 | println(format!("$ {} sent invalid out", id)); 897 | println(format!("$ {}'s tiles are: {:?}", id, self.tiles[id].hands)); 898 | self.shut_ai(id); 899 | let tile = self.last_tile.clone(); 900 | self.out(id, tile); 901 | } 902 | } 903 | } 904 | 905 | fn hu(&mut self, id: usize) -> bool { 906 | match cal_fan(self.tiles[id].clone(), self.last_tile.clone(), false) { 907 | Some(tuple) => { 908 | let (x, fans) = tuple; 909 | self.log.write_fmt(format_args!("hu {}\r\n",id)).ok(); 910 | self.log.write(b"\r\n").ok(); 911 | self.log.write_fmt(format_args!("win {} ron {}\r\n",id,self.action_id)).ok(); 912 | self.log.write_fmt(format_args!("fans {}\r\n",fans.len())).ok(); 913 | for (fan, value) in &fans { 914 | self.log.write_fmt(format_args!("{}:{}\r\n",fan,value)).ok(); 915 | } 916 | self.log.write_fmt(format_args!("score {}\r\n",x)).ok(); 917 | self.score[id] += 3 * self.base + x; 918 | for i in 0..4 { 919 | if i != id { 920 | self.score[i] -= self.base; 921 | } 922 | } 923 | self.score[self.action_id] -= x; 924 | return true; 925 | }, 926 | None => { 927 | println(format!("$ {} sent invalid hu", id)); 928 | println(format!("$ {}'s tiles are: {:?}", id, self.tiles[id].hands)); 929 | self.shut_ai(id); 930 | } 931 | } 932 | return false; 933 | } 934 | 935 | fn gang(&mut self, id: usize) -> bool { 936 | let tile = self.last_tile.clone(); 937 | if self.tiles[id].hands.iter().filter(|&x| *x == tile).count() == 3 { 938 | self.tiles[id].hands.retain(|x| x != &tile); 939 | println(format!("$ {} gang {}", id, tile)); 940 | self.log.write_fmt(format_args!("gang {} {} {}\r\n",id,self.action_id,tile)).ok(); 941 | self.tiles[id].kongs.push(tile); 942 | return true; 943 | } 944 | println(format!("$ {} sent invalid gang", id)); 945 | self.shut_ai(id); 946 | return false; 947 | } 948 | 949 | fn peng(&mut self, id: usize) -> bool { 950 | let tile = self.last_tile.clone(); 951 | if self.tiles[id].hands.iter().filter(|&x| *x == tile).count() >= 2 { 952 | for _ in 0..2 { 953 | let index = self.tiles[id].hands.iter().position(|x| *x == tile).unwrap(); 954 | self.tiles[id].hands.remove(index); 955 | } 956 | println(format!("$ {} peng {}", id, tile)); 957 | self.log.write_fmt(format_args!("peng {} {} {}\r\n",id,self.action_id,tile)).ok(); 958 | self.tiles[id].pungs.push(tile); 959 | return true; 960 | } 961 | println(format!("$ {} sent invalid peng", id)); 962 | self.shut_ai(id); 963 | return false; 964 | } 965 | 966 | fn chi(&mut self, id: usize, tile: &str) -> bool { 967 | if id != post_pos(self.action_id) { 968 | return false; 969 | } 970 | let mut set = HashSet::new(); 971 | set.insert(tile.to_string()); 972 | let second = match post(tile.to_string()) { 973 | Some(x) => x, 974 | None => return false 975 | }; 976 | set.insert(second.clone()); 977 | set.insert(match post(second) { 978 | Some(x) => x, 979 | None => return false 980 | }); 981 | set.remove(&self.last_tile); 982 | if self.tiles[id].hands.iter().filter(|&x| set.contains(x)).count() >= 2 { 983 | for i in set { 984 | let index = self.tiles[id].hands.iter().position(|x| *x == i).unwrap(); 985 | self.tiles[id].hands.remove(index); 986 | } 987 | self.tiles[id].chows.push(tile.to_string()); 988 | println(format!("$ {} chi {}", id, tile)); 989 | self.log.write_fmt(format_args!("chi {} {} {}\r\n",id,self.action_id,tile)).ok(); 990 | return true; 991 | } 992 | println(format!("$ {} sent invalid chi", id)); 993 | self.shut_ai(id); 994 | return false; 995 | } 996 | 997 | fn draw(&mut self) { 998 | unsafe { 999 | println("$ Draw game!".to_string()); 1000 | self.log.write_fmt(format_args!("draw\r\n\r\ndraw\r\n")).ok(); 1001 | for i in 0..4 { 1002 | flags[i] = false; 1003 | } 1004 | } 1005 | } 1006 | } 1007 | 1008 | #[derive(Clone, Debug)] 1009 | struct Message { 1010 | id: usize, 1011 | message: String 1012 | } 1013 | 1014 | fn post(tile: String) -> Option { 1015 | if tile.len() == 2 { 1016 | let mut chars = tile.chars(); 1017 | let num = chars.next().unwrap().to_digit(10).unwrap(); 1018 | let color = chars.next().unwrap(); 1019 | if num < 9 { 1020 | return Some(format!("{}{}", num + 1, color)); 1021 | } 1022 | } 1023 | return None; 1024 | } 1025 | 1026 | fn post_pos(pos: usize) -> usize { 1027 | return (pos + 1) % 4; 1028 | } 1029 | 1030 | fn combine(_tiles: Tiles) -> Vec { 1031 | let mut v = Vec::new(); 1032 | if _tiles.hands.len() == 2 { 1033 | if _tiles.hands[0] == _tiles.hands[1] { 1034 | v.push(_tiles); 1035 | return v; 1036 | } 1037 | } 1038 | let mut tiles = _tiles.clone(); 1039 | tiles.hands.sort_by(|a, b| { 1040 | if a.len() == 1 || b.len() == 1 { 1041 | a.cmp(b) 1042 | } else { 1043 | let _a = a.chars().last().unwrap(); 1044 | let _b = b.chars().last().unwrap(); 1045 | if _a == _b { 1046 | a.cmp(b) 1047 | } else { 1048 | _a.cmp(&_b) 1049 | } 1050 | } 1051 | }); 1052 | if _tiles.hands.len() == 14 { 1053 | let mut tile_count = HashMap::new(); 1054 | for tile in _tiles.clone().hands { 1055 | if tile_count.contains_key(&tile) { 1056 | let _tile_count = tile_count.clone(); 1057 | let count = _tile_count.get(&tile).unwrap(); 1058 | tile_count.insert(tile, count + 1); 1059 | } else { 1060 | tile_count.insert(tile, 1); 1061 | } 1062 | } 1063 | //七对 1064 | { 1065 | if tile_count.values().filter(|&x| x % 2 != 0).count() == 0 { 1066 | v.push(_tiles.clone()); 1067 | } 1068 | } 1069 | //十三幺 1070 | { 1071 | let mut yao = HashSet::new(); 1072 | yao.insert("1M".to_string()); 1073 | yao.insert("1S".to_string()); 1074 | yao.insert("1T".to_string()); 1075 | yao.insert("9M".to_string()); 1076 | yao.insert("9S".to_string()); 1077 | yao.insert("9T".to_string()); 1078 | yao.insert("E".to_string()); 1079 | yao.insert("S".to_string()); 1080 | yao.insert("W".to_string()); 1081 | yao.insert("N".to_string()); 1082 | yao.insert("Z".to_string()); 1083 | yao.insert("F".to_string()); 1084 | yao.insert("B".to_string()); 1085 | if _tiles.hands.iter().filter(|&x| !yao.contains(x)).count() == 0 && 1086 | tile_count.values().filter(|&x| *x > 1).count() == 1 { 1087 | v.push(_tiles.clone()); 1088 | } 1089 | } 1090 | } 1091 | let mut last_tile = String::new(); 1092 | for t in tiles.hands.clone() { 1093 | if t == last_tile { 1094 | continue; 1095 | } 1096 | let mut _tiles = tiles.clone(); 1097 | last_tile = t.clone(); 1098 | match post(t.clone()) { 1099 | Some(t2) => { 1100 | match post(t2.clone()) { 1101 | Some(t3) => { 1102 | match _tiles.hands.iter().position(|x| *x == t) { 1103 | Some(index) => { 1104 | _tiles.hands.remove(index); 1105 | match _tiles.hands.iter().position(|x| *x == t2) { 1106 | Some(index) => { 1107 | _tiles.hands.remove(index); 1108 | match _tiles.hands.iter().position(|x| *x == t3) { 1109 | Some(index) => { 1110 | _tiles.hands.remove(index); 1111 | _tiles.cchows.push(t.clone()); 1112 | for x in combine(_tiles) { 1113 | v.push(x); 1114 | } 1115 | }, 1116 | _ => () 1117 | } 1118 | }, 1119 | _ => () 1120 | } 1121 | }, 1122 | _ => () 1123 | } 1124 | }, 1125 | _ => () 1126 | } 1127 | }, 1128 | _ => () 1129 | } 1130 | let mut _tiles = tiles.clone(); 1131 | if _tiles.hands.iter().filter(|&x| *x == t.clone()).count() >= 3 { 1132 | for _ in 0..3 { 1133 | let index = tiles.hands.iter().position(|x| *x == t.clone()).unwrap(); 1134 | _tiles.hands.remove(index); 1135 | } 1136 | _tiles.cpungs.push(t.clone()); 1137 | for x in combine(_tiles) { 1138 | v.push(x); 1139 | } 1140 | } 1141 | } 1142 | return v; 1143 | } 1144 | 1145 | fn cal_fan(tiles: Tiles, add: String, tsumo: bool) -> Option<(i64, HashMap)> { 1146 | let mut _tiles = tiles.clone(); 1147 | if !tsumo { 1148 | _tiles.hands.push(add.clone()); 1149 | } 1150 | let combs = combine(_tiles.clone()); 1151 | if combs.len() == 0 { 1152 | return None; 1153 | } 1154 | let mut fans = HashMap::new(); 1155 | let mut result = -1; 1156 | for comb in combs { 1157 | let (_fans, _result) = _cal_fan(comb, tsumo); 1158 | if _result > result { 1159 | fans = _fans; 1160 | result = _result; 1161 | } 1162 | } 1163 | // 单调将 1164 | { 1165 | if !fans.contains_key("十三幺") && !fans.contains_key("七对") { 1166 | let mut tiles = _tiles.clone(); 1167 | let _tiles = vec![ 1168 | "1M", "2M", "3M", "4M", "5M", "6M", "7M", "8M", "9M", "1S", "2S", "3S", "4S", "5S", 1169 | "6S", "7S", "8S", "9S", "1T", "2T", "3T", "4T", "5T", "6T", "7T", "8T", "9T", "E", 1170 | "S", "W", "N", "Z", "F", "B" 1171 | ]; 1172 | let index = tiles.hands.iter().position(|x| *x == add).unwrap(); 1173 | tiles.hands.remove(index); 1174 | let mut flag = true; 1175 | for tile in _tiles { 1176 | if tile == add { 1177 | continue; 1178 | } 1179 | let mut tiles = tiles.clone(); 1180 | tiles.hands.push(tile.to_string()); 1181 | if combine(tiles).len() > 0 { 1182 | flag = false; 1183 | break; 1184 | } 1185 | } 1186 | if flag { 1187 | result += 1; 1188 | fans.insert("单调将".to_string(), 1); 1189 | } 1190 | } 1191 | } 1192 | println(format!("Tiles are: {:?}", tiles)); 1193 | print("Fans are: ".to_string()); 1194 | for fan in fans.keys() { 1195 | print(format!("{} ", fan)); 1196 | } 1197 | println(String::new()); 1198 | println(format!("Altogether {}!", result)); 1199 | return Some((result, fans)); 1200 | } 1201 | 1202 | fn _cal_fan(tiles: Tiles, tsumo: bool) -> (HashMap, i64) { 1203 | let mut result: i64 = 0; 1204 | let mut fans = HashMap::new(); 1205 | let mut all_tiles = tiles.hands.clone(); 1206 | for tile in tiles.chows.clone() { 1207 | all_tiles.push(tile.clone()); 1208 | let next = post(tile).unwrap(); 1209 | all_tiles.push(next.clone()); 1210 | all_tiles.push(post(next).unwrap()); 1211 | } 1212 | for tile in tiles.pungs.clone() { 1213 | all_tiles.push(tile); 1214 | } 1215 | for tile in tiles.kongs.clone() { 1216 | all_tiles.push(tile); 1217 | } 1218 | for tile in tiles.ckongs.clone() { 1219 | all_tiles.push(tile); 1220 | } 1221 | for tile in tiles.cchows.clone() { 1222 | all_tiles.push(tile.clone()); 1223 | let next = post(tile).unwrap(); 1224 | all_tiles.push(next.clone()); 1225 | all_tiles.push(post(next).unwrap()); 1226 | } 1227 | for tile in tiles.cpungs.clone() { 1228 | all_tiles.push(tile); 1229 | } 1230 | let mut all_chows = tiles.chows.clone(); 1231 | for chow in tiles.cchows.clone() { 1232 | all_chows.push(chow); 1233 | } 1234 | let mut yao = HashSet::new(); 1235 | yao.insert("1M".to_string()); 1236 | yao.insert("1S".to_string()); 1237 | yao.insert("1T".to_string()); 1238 | yao.insert("9M".to_string()); 1239 | yao.insert("9S".to_string()); 1240 | yao.insert("9T".to_string()); 1241 | yao.insert("E".to_string()); 1242 | yao.insert("S".to_string()); 1243 | yao.insert("W".to_string()); 1244 | yao.insert("N".to_string()); 1245 | yao.insert("Z".to_string()); 1246 | yao.insert("F".to_string()); 1247 | yao.insert("B".to_string()); 1248 | let mut jian = HashSet::new(); 1249 | jian.insert("Z".to_string()); 1250 | jian.insert("F".to_string()); 1251 | jian.insert("B".to_string()); 1252 | let mut all_pungs = tiles.pungs.clone(); 1253 | for pung in tiles.kongs.clone() { 1254 | all_pungs.push(pung); 1255 | } 1256 | for pung in tiles.ckongs.clone() { 1257 | all_pungs.push(pung); 1258 | } 1259 | for pung in tiles.cpungs.clone() { 1260 | all_pungs.push(pung); 1261 | } 1262 | let mut feng = HashSet::new(); 1263 | feng.insert("E".to_string()); 1264 | feng.insert("S".to_string()); 1265 | feng.insert("W".to_string()); 1266 | feng.insert("N".to_string()); 1267 | let mut tile_count = HashMap::new(); 1268 | for tile in tiles.clone().hands { 1269 | if tile_count.contains_key(&tile) { 1270 | let _tile_count = tile_count.clone(); 1271 | let count = _tile_count.get(&tile).unwrap(); 1272 | tile_count.insert(tile, count + 1); 1273 | } else { 1274 | tile_count.insert(tile, 1); 1275 | } 1276 | } 1277 | //十三幺 1278 | { 1279 | if tiles.hands.len() == 14 && 1280 | all_tiles.iter().filter(|&x| !yao.contains(x)).count() == 0 && 1281 | tile_count.values().filter(|&x| *x > 1).count() == 1 { 1282 | result += 88; 1283 | fans.insert("十三幺".to_string(), 88); 1284 | } 1285 | } 1286 | // 大四喜 1287 | { 1288 | if all_pungs.iter().filter(|&x| feng.contains(x)).count() == 4 { 1289 | result += 88; 1290 | fans.insert("大四喜".to_string(), 88); 1291 | } 1292 | } 1293 | // 大三元 1294 | { 1295 | if all_pungs.iter().filter(|&x| jian.contains(x)).count() == 3 { 1296 | result += 88; 1297 | fans.insert("大三元".to_string(), 88); 1298 | } 1299 | } 1300 | // 绿一色 1301 | { 1302 | let mut set = HashSet::new(); 1303 | set.insert("2S".to_string()); 1304 | set.insert("3S".to_string()); 1305 | set.insert("4S".to_string()); 1306 | set.insert("6S".to_string()); 1307 | set.insert("8S".to_string()); 1308 | set.insert("F".to_string()); 1309 | if all_tiles.iter().filter(|&x| !set.contains(x)).count() == 0 { 1310 | result += 88; 1311 | fans.insert("绿一色".to_string(), 88); 1312 | } 1313 | } 1314 | // 四杠 1315 | { 1316 | if tiles.kongs.len() + tiles.ckongs.len() == 4 { 1317 | result += 88; 1318 | fans.insert("四杠".to_string(), 88); 1319 | } 1320 | } 1321 | // 小四喜 1322 | { 1323 | let mut set = HashSet::new(); 1324 | set.insert("E".to_string()); 1325 | set.insert("S".to_string()); 1326 | set.insert("W".to_string()); 1327 | set.insert("N".to_string()); 1328 | if all_pungs.iter().filter(|&x| set.contains(x)).count() == 3 && 1329 | tiles.hands.iter().filter(|&x| set.contains(x)).count() == 2 { 1330 | result += 64; 1331 | fans.insert("小四喜".to_string(), 64); 1332 | } 1333 | } 1334 | // 小三元 1335 | { 1336 | 1337 | if all_pungs.iter().filter(|&x| jian.contains(x)).count() == 2 && 1338 | tiles.hands.iter().filter(|&x| jian.contains(x)).count() == 2 { 1339 | result += 64; 1340 | fans.insert("小三元".to_string(), 64); 1341 | } 1342 | } 1343 | // 字一色 1344 | { 1345 | if all_tiles.iter().filter(|&x| x.len() == 2).count() == 0 { 1346 | result += 64; 1347 | fans.insert("字一色".to_string(), 64); 1348 | } 1349 | } 1350 | // 四暗刻 1351 | { 1352 | if tiles.cpungs.len() + tiles.ckongs.len() == 4 { 1353 | result += 64; 1354 | fans.insert("四暗刻".to_string(), 64); 1355 | } 1356 | } 1357 | // 清幺九 1358 | { 1359 | let mut set = HashSet::new(); 1360 | set.insert("1M".to_string()); 1361 | set.insert("1S".to_string()); 1362 | set.insert("1T".to_string()); 1363 | set.insert("9M".to_string()); 1364 | set.insert("9S".to_string()); 1365 | set.insert("9T".to_string()); 1366 | if all_tiles.iter().filter(|&x| !set.contains(x)).count() == 0 && tiles.chows.len() == 0 && 1367 | tiles.cchows.len() == 0 { 1368 | result += 64; 1369 | fans.insert("清幺九".to_string(), 64); 1370 | } 1371 | } 1372 | // 一色四同顺 1373 | { 1374 | for chow in all_chows.clone() { 1375 | if all_chows.iter().filter(|&x| *x == chow).count() == 4 { 1376 | result += 48; 1377 | fans.insert("一色四同顺".to_string(), 48); 1378 | break; 1379 | } 1380 | } 1381 | } 1382 | // 三杠 1383 | { 1384 | if tiles.kongs.len() + tiles.ckongs.len() == 3 { 1385 | result += 32; 1386 | fans.insert("三杠".to_string(), 32); 1387 | } 1388 | } 1389 | // 混幺九 1390 | { 1391 | if !fans.contains_key("清幺九") && !fans.contains_key("字一色") { 1392 | if all_tiles.iter().filter(|&x| !yao.contains(x)).count() == 0 && 1393 | tiles.chows.len() == 0 && tiles.cchows.len() == 0 { 1394 | result += 32; 1395 | fans.insert("混幺九".to_string(), 32); 1396 | } 1397 | } 1398 | } 1399 | // 清一色 1400 | { 1401 | let sample: Vec<_> = all_tiles[0].clone().chars().collect(); 1402 | if sample.len() == 2 { 1403 | let color = sample[1]; 1404 | if all_tiles.iter() 1405 | .filter(|&x| x.len() != 2 || x.chars().last().unwrap() != color) 1406 | .count() == 0 { 1407 | result += 24; 1408 | fans.insert("清一色".to_string(), 24); 1409 | } 1410 | } 1411 | } 1412 | //七对 1413 | { 1414 | if tiles.hands.len() == 14 && tile_count.values().filter(|&x| x % 2 != 0).count() == 0 { 1415 | result += 24; 1416 | fans.insert("七对".to_string(), 24); 1417 | } 1418 | } 1419 | // 一色三同顺 1420 | { 1421 | for chow in all_chows.clone() { 1422 | if all_chows.iter().filter(|&x| *x == chow).count() == 3 { 1423 | result += 24; 1424 | fans.insert("一色三同顺".to_string(), 24); 1425 | break; 1426 | } 1427 | } 1428 | } 1429 | // 三同刻 1430 | { 1431 | for pung in all_pungs.clone() { 1432 | if pung.len() == 1 { 1433 | continue; 1434 | } 1435 | let ord = pung.chars().next().unwrap(); 1436 | if all_pungs.iter().filter(|&x| x.chars().next().unwrap() == ord).count() == 3 { 1437 | result += 16; 1438 | fans.insert("三同刻".to_string(), 16); 1439 | break; 1440 | } 1441 | } 1442 | } 1443 | // 三暗刻 1444 | { 1445 | if tiles.cpungs.len() + tiles.ckongs.len() == 3 { 1446 | result += 16; 1447 | fans.insert("三暗刻".to_string(), 16); 1448 | } 1449 | } 1450 | // 三色三同顺 1451 | { 1452 | for chow in all_chows.clone() { 1453 | let ord = chow.chars().next().unwrap(); 1454 | if all_chows.contains(&format!("{}M", ord)) && 1455 | all_chows.contains(&format!("{}S", ord)) && 1456 | all_chows.contains(&format!("{}T", ord)) { 1457 | result += 8; 1458 | fans.insert("三色三同顺".to_string(), 8); 1459 | break; 1460 | } 1461 | } 1462 | } 1463 | // 碰碰和 1464 | { 1465 | if !fans.contains_key("大四喜") && !fans.contains_key("四杠") && !fans.contains_key("字一色") && 1466 | !fans.contains_key("四暗刻") && !fans.contains_key("清幺九") && !fans.contains_key("混幺九") { 1467 | if tiles.chows.len() + tiles.cchows.len() == 0 && all_pungs.len() > 0 { 1468 | result += 6; 1469 | fans.insert("碰碰和".to_string(), 6); 1470 | } 1471 | } 1472 | } 1473 | // 混一色 1474 | { 1475 | if !fans.contains_key("字一色") && !fans.contains_key("清一色") { 1476 | let mut all_tiles = all_tiles.clone(); 1477 | all_tiles.retain(|x| x.len() == 2); 1478 | let color = all_tiles[0].chars().last().unwrap(); 1479 | if all_tiles.iter().filter(|&x| x.chars().last().unwrap() != color).count() == 0 { 1480 | result += 6; 1481 | fans.insert("混一色".to_string(), 6); 1482 | } 1483 | } 1484 | } 1485 | // 五门齐 1486 | { 1487 | if !fans.contains_key("十三幺") { 1488 | let mut set1 = HashSet::new(); 1489 | let mut set2 = HashSet::new(); 1490 | set1.insert("E".to_string()); 1491 | set1.insert("S".to_string()); 1492 | set1.insert("W".to_string()); 1493 | set1.insert("N".to_string()); 1494 | set2.insert("Z".to_string()); 1495 | set2.insert("F".to_string()); 1496 | set2.insert("B".to_string()); 1497 | if all_tiles.iter() 1498 | .filter(|&x| x.len() == 2 && x.chars().last().unwrap() == 'M') 1499 | .count() != 0 && 1500 | all_tiles.iter() 1501 | .filter(|&x| x.len() == 2 && x.chars().last().unwrap() == 'S') 1502 | .count() != 0 && 1503 | all_tiles.iter() 1504 | .filter(|&x| x.len() == 2 && x.chars().last().unwrap() == 'T') 1505 | .count() != 0 && 1506 | all_tiles.iter().filter(|&x| set1.contains(x)).count() != 0 && 1507 | all_tiles.iter().filter(|&x| set2.contains(x)).count() != 0 { 1508 | result += 6; 1509 | fans.insert("五门齐".to_string(), 6); 1510 | } 1511 | } 1512 | } 1513 | // 门前清 1514 | { 1515 | if !fans.contains_key("十三幺") && !fans.contains_key("七对") { 1516 | if tiles.chows.len() + tiles.pungs.len() + tiles.kongs.len() == 0 { 1517 | result += 2; 1518 | fans.insert("门前清".to_string(), 2); 1519 | } 1520 | } 1521 | } 1522 | // 断幺 1523 | { 1524 | if all_tiles.iter().filter(|&x| yao.contains(x)).count() == 0 { 1525 | result += 2; 1526 | fans.insert("断幺".to_string(), 2); 1527 | } 1528 | } 1529 | // 平和 1530 | { 1531 | if tiles.pungs.len() + tiles.kongs.len() + tiles.ckongs.len() + tiles.cpungs.len() == 0 && 1532 | tiles.chows.len() + tiles.cchows.len() > 0 { 1533 | result += 2; 1534 | fans.insert("平和".to_string(), 2); 1535 | } 1536 | } 1537 | // 箭刻 1538 | { 1539 | if !fans.contains_key("大三元") && !fans.contains_key("小三元") { 1540 | let count = all_pungs.iter().filter(|&x| jian.contains(x)).count() as i64; 1541 | if count > 0 { 1542 | result += 2 * count; 1543 | if count == 1 { 1544 | fans.insert("箭刻".to_string(), 2); 1545 | } else { 1546 | fans.insert(format!("箭刻×{}", count).to_string(), count * 2); 1547 | } 1548 | } 1549 | } 1550 | } 1551 | // 暗杠 1552 | { 1553 | let count = tiles.ckongs.len() as i64; 1554 | if count > 0 { 1555 | result += 2 * count; 1556 | if count == 1 { 1557 | fans.insert("暗杠".to_string(), 2); 1558 | } else { 1559 | fans.insert(format!("暗杠×{}", count).to_string(), count * 2); 1560 | } 1561 | } 1562 | } 1563 | // 自摸 1564 | { 1565 | if tsumo { 1566 | result += 1; 1567 | fans.insert("自摸".to_string(), 1); 1568 | } 1569 | } 1570 | // 一般高 1571 | { 1572 | if !fans.contains_key("一色四同顺") && !fans.contains_key("一色三同顺") { 1573 | let mut chow_count = HashMap::new(); 1574 | for chow in all_chows.clone() { 1575 | if chow_count.contains_key(&chow) { 1576 | let _chow_count = chow_count.clone(); 1577 | let count = _chow_count.get(&chow).unwrap(); 1578 | chow_count.insert(chow, count + 1); 1579 | } else { 1580 | chow_count.insert(chow, 1); 1581 | } 1582 | } 1583 | let count = chow_count.values().filter(|&x| *x == 2).count() as i64; 1584 | if count > 0 { 1585 | result += count; 1586 | if count == 1 { 1587 | fans.insert("一般高".to_string(), 1); 1588 | } else { 1589 | fans.insert(format!("一般高×{}", count).to_string(), count); 1590 | } 1591 | } 1592 | } 1593 | } 1594 | // 喜相逢 1595 | { 1596 | if !fans.contains_key("三色三同顺") { 1597 | let mut set = HashSet::new(); 1598 | for chow in all_chows.clone() { 1599 | set.insert(chow); 1600 | } 1601 | let mut count = 0i64; 1602 | for chow in set.clone() { 1603 | let ord = chow.chars().next().unwrap(); 1604 | let color = chow.chars().last().unwrap(); 1605 | if set.iter() 1606 | .filter(|&x| x.chars().next().unwrap() == ord && 1607 | x.chars().last().unwrap() != 1608 | color) 1609 | .count() != 0 { 1610 | count += 1; 1611 | } 1612 | } 1613 | count /= 2; 1614 | if count > 0 { 1615 | result += count; 1616 | if count == 1 { 1617 | fans.insert("喜相逢".to_string(), 1); 1618 | } else { 1619 | fans.insert(format!("喜相逢×{}", count).to_string(), count); 1620 | } 1621 | } 1622 | } 1623 | } 1624 | // 明杠 1625 | { 1626 | let count = tiles.kongs.len() as i64; 1627 | if count > 0 && count < 3 { 1628 | result += count; 1629 | if count == 1 { 1630 | fans.insert("明杠".to_string(), 1); 1631 | } else { 1632 | fans.insert(format!("明杠×{}", count).to_string(), count); 1633 | } 1634 | } 1635 | } 1636 | return (fans, result); 1637 | } --------------------------------------------------------------------------------