├── .gitignore ├── separo.png ├── .github └── workflows │ └── deploy.yml ├── tests └── app.rs ├── LICENSE ├── README.md ├── Cargo.toml ├── src ├── recorder.rs ├── instant.rs └── lib.rs └── static ├── index.html └── js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /separo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToruNiina/separo-rs/HEAD/separo.png -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: Ubuntu-20.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | submodules: true 16 | - name: Install tools 17 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 18 | - name: Copy static to public 19 | run: cp -r static public 20 | - name: Build 21 | run: wasm-pack build --target web --out-dir public/js 22 | - name: Remove .gitignore added from wasm-pack 23 | run: rm public/js/.gitignore 24 | - name: Publish 25 | uses: peaceiris/actions-gh-pages@v3 26 | with: 27 | github_token: ${{ secrets.GITHUB_TOKEN }} 28 | publish_branch: gh-pages 29 | publish_dir: ./public 30 | commit_message: ${{ github.event.head_commit.message }} 31 | -------------------------------------------------------------------------------- /tests/app.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen_test::{wasm_bindgen_test_configure, wasm_bindgen_test}; 2 | use futures::prelude::*; 3 | use wasm_bindgen::JsValue; 4 | use wasm_bindgen_futures::JsFuture; 5 | 6 | wasm_bindgen_test_configure!(run_in_browser); 7 | 8 | 9 | // This runs a unit test in native Rust, so it can only use Rust APIs. 10 | #[test] 11 | fn rust_test() { 12 | assert_eq!(1, 1); 13 | } 14 | 15 | 16 | // This runs a unit test in the browser, so it can use browser APIs. 17 | #[wasm_bindgen_test] 18 | fn web_test() { 19 | assert_eq!(1, 1); 20 | } 21 | 22 | 23 | // This runs a unit test in the browser, and in addition it supports asynchronous Future APIs. 24 | #[wasm_bindgen_test(async)] 25 | fn async_test() -> impl Future { 26 | // Creates a JavaScript Promise which will asynchronously resolve with the value 42. 27 | let promise = js_sys::Promise::resolve(&JsValue::from(42)); 28 | 29 | // Converts that Promise into a Future. 30 | // The unit test will wait for the Future to resolve. 31 | JsFuture::from(promise) 32 | .map(|x| { 33 | assert_eq!(x, 42); 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Toru Niina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Separo-rs 2 | 3 | blog post about SEPARO in [English](https://toruniina.github.io/posts/writing-board-game-ai/) 4 | 5 | Separoは[@gfngfn](https://github.com/gfngfn)氏によって考案された2人対戦用ボードゲームです。 6 | ルールの詳細は、@gfngfn氏の個人ウェブページ[佛陀ヶ谷](http://buddhagaja.soregashi.com/boardgame.html#jump_boardgame_separo)で配布されています。 7 | 8 | [![終局図1](https://github.com/ToruNiina/separo-rs/blob/master/separo.png)](https://toruniina.github.io/separo-rs/) 9 | 10 | Separo-rsは、Separoをプレイするソフトウェアと、Webインターフェースを提供します。 11 | ソフトウェアと対局できるほか、ソフトウェア同士の対局を見守ったり、人間が交互にプレイすることも可能です。 12 | 13 | 実装しているアルゴリズムは以下の通りです。Random以外は1秒間探索を行います。 14 | 15 | - Random 16 | - 可能な手から一様乱数で手を選びます。最弱です。 17 | - Naive MC 18 | - 原始モンテカルロアルゴリズムです。可能な手からランダムプレイアウトを行い、勝率が最大の手を選択します。 19 | - [Brügmann, Bernd (1993)](http://www.ideanest.com/vegos/MonteCarloGo.pdf) 20 | - UCT MC 21 | - 信頼上限(UCB1)スコアを用いたモンテカルロ木探索アルゴリズムです。 22 | - [Kocsis, Levente; Szepesvári, Csaba (2006)](https://doi.org/10.1007/11871842_29) 23 | 24 | このレポジトリは、[rustwasm/rust-webpack-template](https://github.com/rustwasm/rust-webpack-template)をもとに作成されています。 25 | 26 | ## Quick Start 27 | 28 | 1. https://toruniina.github.io/separo-rs/ をクリックします。 29 | 2. Human VS Random となるようにプルダウンメニューを設定します。あとはデフォルトのまま Start ボタンをクリックします。 30 | 3. 盤面の赤丸をクリック状態を継続して掴み、動かせるところ(点線の丸のガイドが表示されます)までひっぱりクリック状態を解除します。 31 | 4. 3.を繰り返します。 32 | 5. 4.が行えなくなった時点で勝敗が決定しています。Start ボタンの2行下に勝敗情報が表示されています。 33 | 34 | ## Build 35 | 36 | ### Prerequisites 37 | 38 | - Rust 39 | - [wasm-pack](https://github.com/rustwasm/wasm-pack) 40 | 41 | ### How to build and run locally 42 | 43 | static/js/以下にwasmと幾つかのファイルが生成されます。 44 | 45 | ``` 46 | $ wasm-pack build --target web --out-dir static/js 47 | ``` 48 | 49 | ページを確認する際はstatic/以下でHTTPサーバーを立ててください。 50 | 簡易なHTTPサーバーとしては[https](https://github.com/thecoshman/http)があります。 51 | 52 | ``` 53 | $ cd static && http 54 | ``` 55 | 56 | ## Disclaimer 57 | 58 | - JSのベストプラクティスに詳しくないので、万一、CPU使用率が上がりすぎたり、メモリを食い尽くして落ちたりしても責任は取りません。 59 | - 数回の対局を除き、デバッグをほぼしていないので、プレイ中に落ちるかも知れません。 60 | - その場合Issue報告を上げてくれると嬉しいです。 61 | 62 | ## Licensing terms 63 | 64 | MIT. 65 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # You must change these to your own details. 2 | [package] 3 | name = "separo-rs" 4 | description = "A boardgame" 5 | version = "0.1.0" 6 | authors = ["Toru Niina "] 7 | categories = ["wasm"] 8 | readme = "README.md" 9 | edition = "2018" 10 | 11 | [lib] 12 | crate-type = ["cdylib"] 13 | 14 | [profile.release] 15 | # This makes the compiled code faster and smaller, but it makes compiling slower, 16 | # so it's only enabled in release mode. 17 | lto = true 18 | 19 | [features] 20 | # If you uncomment this line, it will enable `wee_alloc`: 21 | #default = ["wee_alloc"] 22 | 23 | [dependencies] 24 | rand = "0.7" 25 | arrayvec = "0.5" 26 | serde = { version = "1.0", features = ["derive"] } 27 | serde_json = "1.0" 28 | serde_repr = "0.1" 29 | 30 | base64 = "0.12" 31 | png = "0.16" 32 | gif = "0.10" 33 | 34 | # The `wasm-bindgen` crate provides the bare minimum functionality needed 35 | # to interact with JavaScript. 36 | wasm-bindgen = "0.2.67" 37 | 38 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 39 | # compared to the default allocator's ~10K. However, it is slower than the default 40 | # allocator, so it's not enabled by default. 41 | wee_alloc = { version = "0.4.5", optional = true } 42 | 43 | # The `web-sys` crate allows you to interact with the various browser APIs, 44 | # like the DOM. 45 | [dependencies.web-sys] 46 | version = "0.3.44" 47 | features = ["console"] 48 | 49 | # The `console_error_panic_hook` crate provides better debugging of panics by 50 | # logging them with `console.error`. This is great for development, but requires 51 | # all the `std::fmt` and `std::panicking` infrastructure, so it's only enabled 52 | # in debug mode. 53 | [target."cfg(debug_assertions)".dependencies] 54 | console_error_panic_hook = "0.1.5" 55 | 56 | # These crates are used for running unit tests. 57 | [dev-dependencies] 58 | wasm-bindgen-test = "0.2.45" 59 | futures = "0.1.27" 60 | js-sys = "0.3.22" 61 | wasm-bindgen-futures = "0.3.22" 62 | 63 | [package.metadata.wasm-pack.profile.release] 64 | wasm-opt = false 65 | [package.metadata.wasm-pack.profile.dev] 66 | wasm-opt = false 67 | -------------------------------------------------------------------------------- /src/recorder.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use std::rc::Rc; 3 | use std::cell::RefCell; 4 | use gif::SetParameter; 5 | 6 | struct ToVecRefWriter { 7 | inner: Rc>>, 8 | } 9 | impl std::io::Write for ToVecRefWriter { 10 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 11 | self.inner.borrow_mut().write(buf) 12 | } 13 | fn flush(&mut self) -> std::io::Result<()> { 14 | self.inner.borrow_mut().flush() 15 | } 16 | } 17 | 18 | #[wasm_bindgen] 19 | pub struct GameGifRecorder { 20 | png_buffer: Vec, 21 | gif_buffer: Rc>>, 22 | gif_encoder: Option>, 23 | } 24 | 25 | #[wasm_bindgen] 26 | impl GameGifRecorder { 27 | pub fn new() -> Self { 28 | GameGifRecorder{ 29 | png_buffer: Vec::new(), 30 | gif_buffer: Rc::new(RefCell::new(Vec::new())), 31 | gif_encoder: None, 32 | } 33 | } 34 | pub fn add_frame(&mut self, img: String) { 35 | let content: Vec = base64::decode( 36 | img.trim_start_matches("data:image/png;base64,")).unwrap(); 37 | let decoder = png::Decoder::new(&content[..]); 38 | let (info, mut reader) = decoder.read_info().unwrap(); 39 | self.png_buffer.resize(info.buffer_size(), 0); 40 | reader.next_frame(&mut self.png_buffer).unwrap(); 41 | 42 | if self.gif_encoder.is_none() { 43 | self.gif_encoder = Some(gif::Encoder::new( 44 | ToVecRefWriter{inner: Rc::clone(&self.gif_buffer)}, 45 | info.width as u16, 46 | info.height as u16, 47 | &[]).unwrap()); 48 | self.gif_encoder.as_mut().unwrap().set(gif::Repeat::Infinite).unwrap(); 49 | } 50 | let mut frame = gif::Frame::from_rgba_speed( 51 | info.width as u16, info.height as u16, &mut self.png_buffer, 20); 52 | frame.delay = 100; 53 | 54 | self.gif_encoder.as_mut().unwrap().write_frame(&frame).unwrap(); 55 | } 56 | 57 | pub fn dump(&mut self) -> String { 58 | self.gif_encoder = None; // drop gif_encoder 59 | base64::encode(&*self.gif_buffer.borrow()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/instant.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, unused_imports)] 2 | 3 | // workaround for SystemTime::now in WASM 4 | // https://github.com/rust-lang/rust/issues/48564#issuecomment-505114709 5 | 6 | use wasm_bindgen::prelude::*; 7 | use std::convert::{TryInto}; 8 | use std::ops::{Add, Sub, AddAssign, SubAssign}; 9 | 10 | pub use std::time::*; 11 | 12 | #[cfg(not(target_arch = "wasm32"))] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Instant(std::time::Instant); 13 | #[cfg(not(target_arch = "wasm32"))] impl Instant { 14 | pub fn now() -> Self { Self(std::time::Instant::now()) } 15 | pub fn duration_since(&self, earlier: Instant) -> Duration { self.0.duration_since(earlier.0) } 16 | pub fn elapsed(&self) -> Duration { self.0.elapsed() } 17 | pub fn checked_add(&self, duration: Duration) -> Option { self.0.checked_add(duration).map(|i| Self(i)) } 18 | pub fn checked_sub(&self, duration: Duration) -> Option { self.0.checked_sub(duration).map(|i| Self(i)) } 19 | } 20 | 21 | #[cfg(target_arch = "wasm32")] #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = Date, js_name = now)] fn date_now() -> f64; } 22 | #[cfg(target_arch = "wasm32")] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Instant(u64); 23 | #[cfg(target_arch = "wasm32")] impl Instant { 24 | pub fn now() -> Self { Self(date_now() as u64) } 25 | pub fn duration_since(&self, earlier: Instant) -> Duration { Duration::from_millis(self.0 - earlier.0) } 26 | pub fn elapsed(&self) -> Duration { Self::now().duration_since(*self) } 27 | pub fn checked_add(&self, duration: Duration) -> Option { 28 | match duration.as_millis().try_into() { 29 | Ok(duration) => self.0.checked_add(duration).map(|i| Self(i)), 30 | Err(_) => None, 31 | } 32 | } 33 | pub fn checked_sub(&self, duration: Duration) -> Option { 34 | match duration.as_millis().try_into() { 35 | Ok(duration) => self.0.checked_sub(duration).map(|i| Self(i)), 36 | Err(_) => None, 37 | } 38 | } 39 | } 40 | 41 | impl Add for Instant { type Output = Instant; fn add(self, other: Duration) -> Instant { self.checked_add(other).unwrap() } } 42 | impl Sub for Instant { type Output = Instant; fn sub(self, other: Duration) -> Instant { self.checked_sub(other).unwrap() } } 43 | impl Sub for Instant { type Output = Duration; fn sub(self, other: Instant) -> Duration { self.duration_since(other) } } 44 | impl AddAssign for Instant { fn add_assign(&mut self, other: Duration) { *self = *self + other; } } 45 | impl SubAssign for Instant { fn sub_assign(&mut self, other: Duration) { *self = *self - other; } } 46 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Separo-rs 8 | 9 | 12 | 13 | 14 |
15 | 16 |
17 |

18 | Separoは@gfngfn氏が考案した2人対戦ボードゲームです。 19 | ルールはページ下部の簡易的な記述か、@gfngfn氏のサイトを参照してください。 20 |

21 |

22 | ゲームを始める際は、下のプルダウンメニューからアルゴリズム(か、Human)を選んでStartを押してください。 23 | 各アルゴリズムについては、GitHubレポジトリを参照してください。 24 |

25 |
26 | 27 |
28 |
29 |
30 | 37 |
38 | VS 39 |
40 | 47 |
48 |
49 | 50 |
51 | 52 |
53 | 54 |
55 | sec 56 |
57 |
58 |
59 | 60 |
61 | 62 |
63 | 64 |
65 |
66 | 67 |
68 |
69 | 70 | 71 |
72 |
73 | 74 |
75 |
76 | 77 |
78 |
79 | 80 |
81 | 82 |
83 | 84 |
85 |
86 |

87 | このゲームは、赤が先手、青が後手となり、順に「石」を置きながら「根」を張って、最終的に盤面をより多くの領域に分割した方が勝ちとなるゲームです。 88 | ただし、分割された領域のうち、面積が格子の単位正方形1つ分以下となる領域はカウントされません。 89 |

90 |

91 | 手番は二つのフェーズから成ります。 92 | 第一フェーズでは、すでにある自分の石を選び、そこから斜めの方向にあるまだ石のない交点を選んで根を伸ばします。 93 | 第二フェーズでは、第一フェーズで置いた点から、第一フェーズで伸ばした線と135度の角度をなす縦横いずれかの線を選び、根を伸ばします。 94 | 第二フェーズで選んだ点にすでに自分の石がある場合、接続できます。 95 | 全てのフェーズで、すでにある自分の根と45度以下の角度で交わるような根は伸ばすことができません。 96 | 手がない場合、パスができます。両者ともに着手不可能になれば終了です。 97 |

98 |

99 | 遊ぶときは、第一フェーズを始める石をクリックし、ドラッグして続きの石を選択してください。 100 | ボタンを離した瞬間にいたグリッドの石が第二フェーズ終わりの石となります。 101 | スマホやタブレットの場合は、第一フェーズを始める石をタップしてから、第二フェーズの支点と終点の石をタップしてください。 102 | ルール違反となる手は適用されません。間違えて選択した場合は遠く離れたところでボタンを離すなどでルール違反の手を作り、キャンセルしてください。 103 |

104 |
105 |
106 | 107 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /static/js/index.js: -------------------------------------------------------------------------------- 1 | const canvas_header = 100; 2 | const canvas_width = 540; 3 | const canvas_height = 640; 4 | const board_margin = 30; 5 | const board_width = 480; 6 | let board_size = 9; 7 | let grid_width = board_width / (board_size - 1); 8 | let stone_radius = grid_width * 0.3; 9 | let scale = 1.0; 10 | const stone_stroke = 2; 11 | const root_stroke = 5; 12 | const RED = 0; 13 | const BLUE = 1; 14 | 15 | const UCT_exploration_weight_coeff = 2.0; 16 | const UCT_tree_expansion_threshold = 3; 17 | 18 | const board_color = "rgb(255,255,255)"; 19 | const grid_color = "rgb(0,0,0)"; 20 | const fill_colors = ['rgba(255,128,128,0.95)', 'rgba(128,128,255,0.95)']; 21 | const stroke_colors = ['rgb(255,0,0)', 'rgb(0,0,255)']; 22 | 23 | const guide_checkbox = document.getElementById("guide"); 24 | let draw_guide = guide_checkbox.checked; 25 | let is_running = false; 26 | 27 | function sleep(ms) { 28 | return new Promise(resolve => setTimeout(resolve, ms)); 29 | } 30 | 31 | function mouseevent_to_xy(e) { 32 | return offset_to_xy(e.offsetX, e.offsetY); 33 | } 34 | 35 | function touchevent_to_xy(e) { 36 | const touchObject = e.changedTouches[0]; 37 | const touchX = touchObject.pageX; 38 | const touchY = touchObject.pageY; 39 | 40 | const element = touchObject.target; 41 | const elemRect = element.getBoundingClientRect(); 42 | const originX = elemRect.left + window.pageXOffset; 43 | const originY = elemRect.top + window.pageYOffset; 44 | 45 | return offset_to_xy(touchX - originX, touchY - originY); 46 | } 47 | 48 | function offset_to_xy(offsetX, offsetY) { 49 | return { 50 | x: Math.floor((scale * offsetX + grid_width / 2 - board_margin) / grid_width), 51 | y: Math.floor((scale * offsetY + grid_width / 2 - board_margin - canvas_header) / grid_width), 52 | }; 53 | } 54 | 55 | function xy_to_pixel(xy) { 56 | const pix_x = xy.x * grid_width + board_margin; 57 | const pix_y = xy.y * grid_width + board_margin + canvas_header; 58 | return {x:pix_x, y:pix_y}; 59 | } 60 | 61 | function update_board_size() { 62 | board_size = Math.floor(document.getElementById("board-size").valueAsNumber); 63 | grid_width = board_width / (board_size - 1); 64 | stone_radius = grid_width * 0.3; 65 | } 66 | 67 | document.getElementById("board-size").addEventListener('input', function(e) { 68 | if(is_running) { 69 | return; // ignore change while playing a game 70 | } 71 | update_board_size(); 72 | }); 73 | 74 | let gif_base64 = "" 75 | document.getElementById("download-button").addEventListener('click', function(e) { 76 | let element = document.createElement("a"); 77 | element.setAttribute("href", gif_base64); 78 | element.setAttribute("download", "separo.gif"); 79 | element.style.display = "none"; 80 | document.body.appendChild(element); 81 | element.click(); 82 | document.body.removeChild(element); 83 | }); 84 | 85 | 86 | async function run(module) { 87 | if(is_running) {return;} 88 | is_running = true; 89 | 90 | // check current board size 91 | update_board_size(); 92 | 93 | let separo = module.Board.new(board_size); 94 | let canvas = document.getElementById("separo-board"); 95 | 96 | scale = canvas_width / canvas.offsetWidth; 97 | canvas.width = canvas_width; 98 | canvas.height = canvas_height; 99 | 100 | let context = canvas.getContext('2d'); 101 | 102 | const time_limit = Math.floor(document.getElementById("time-limit").valueAsNumber); 103 | 104 | const player_setting_R = document.getElementById("red player" ); 105 | const player_setting_B = document.getElementById("blue player"); 106 | const player_R = player_setting_R.options[player_setting_R.selectedIndex].value; 107 | const player_B = player_setting_B.options[player_setting_B.selectedIndex].value; 108 | 109 | console.log("player red = ", player_R); 110 | console.log("player blue = ", player_B); 111 | 112 | drawBoard(context, separo, player_R, player_B, "Select players and Click \"Start\""); 113 | guide_checkbox.addEventListener('input', function(e) { 114 | draw_guide = guide_checkbox.checked; 115 | drawBoard(context, separo, player_R, player_B, draw_guide ? "guide turned on" : "guide turned off"); 116 | }); 117 | 118 | if(player_R == "NotSelected" || player_B == "NotSelected") { 119 | is_running = false; 120 | return; 121 | } 122 | 123 | let playerR; 124 | let playerB; 125 | 126 | let gen_seed = function() { 127 | return Math.floor(Math.random() * 4294967296) 128 | } 129 | 130 | // to check human's move (There should be more sophisticated way...) 131 | let is_humans_turn = false; 132 | let humans_move = [null, null, null]; 133 | let humans_possible_moves = []; 134 | let turn_color = "Red"; 135 | let is_canceled = false; 136 | 137 | function list_up_possible_moves(board, color) { 138 | humans_possible_moves = []; 139 | JSON.parse(board.possible_moves_as_json()).forEach(function(root) { 140 | if (root["color"] == color) { 141 | humans_possible_moves.push(root["stones"]); 142 | } 143 | }); 144 | } 145 | 146 | function filter_possible_moves(index, next_move) { 147 | return humans_possible_moves.filter(move => { 148 | return move[index].x == next_move.x && move[index].y == next_move.y; 149 | }); 150 | } 151 | 152 | let md = new MobileDetect(window.navigator.userAgent); 153 | if (md.mobile() || md.tablet()) { 154 | canvas.addEventListener('touchend', function(e) { 155 | if (is_humans_turn) { 156 | if (humans_move[0] == null) { 157 | humans_move[0] = touchevent_to_xy(e); 158 | humans_possible_moves = filter_possible_moves(0, humans_move[0]); 159 | if (humans_possible_moves.length == 0) { 160 | is_canceled = true; 161 | return; 162 | } 163 | let pix = xy_to_pixel(humans_move[0]); 164 | drawTemporaryStone(context, pix); 165 | } else if (humans_move[1] == null) { 166 | humans_move[1] = touchevent_to_xy(e); 167 | humans_possible_moves = filter_possible_moves(1, humans_move[1]); 168 | if (humans_possible_moves.length == 0) { 169 | is_canceled = true; 170 | return; 171 | } 172 | let pix = xy_to_pixel(humans_move[1]); 173 | drawTemporaryStone(context, pix); 174 | } else if (humans_move[2] == null) { 175 | humans_move[2] = touchevent_to_xy(e); 176 | humans_possible_moves = filter_possible_moves(2, humans_move[2]); 177 | if (humans_possible_moves.length == 0) { 178 | is_canceled = true; 179 | return; 180 | } 181 | let pix = xy_to_pixel(humans_move[2]); 182 | drawTemporaryStone(context, pix); 183 | } 184 | } 185 | }); 186 | } else { 187 | canvas.addEventListener('mousemove', function(e) { 188 | if (!is_humans_turn || humans_move[0] == null || humans_move[2] != null) { 189 | return; 190 | } 191 | 192 | if (humans_move[1] == null) { 193 | let current_pos = mouseevent_to_xy(e); 194 | filtered = filter_possible_moves(1, current_pos); 195 | if (filtered.length == 0) { 196 | return; 197 | } 198 | 199 | humans_move[1] = current_pos; 200 | humans_possible_moves = filtered; 201 | 202 | // draw temporary 203 | let pix = xy_to_pixel(humans_move[1]); 204 | drawTemporaryStone(context, pix); 205 | } else { 206 | // just show the last stone while chosing it. 207 | // It does not actually move a stone. 208 | let current_pos = mouseevent_to_xy(e); 209 | filtered = filter_possible_moves(2, current_pos); 210 | if (filtered.length == 0) { 211 | return; 212 | } 213 | 214 | drawBoard(context, separo, player_R, player_B, turn_color + "'s turn"); 215 | drawTemporaryStone(context, xy_to_pixel(humans_move[0])); 216 | drawTemporaryStone(context, xy_to_pixel(humans_move[1])); 217 | drawTemporaryStone(context, xy_to_pixel(current_pos)); 218 | } 219 | }); 220 | 221 | canvas.addEventListener('mousedown', function(e) { 222 | if (!is_humans_turn || humans_move[0] != null) { 223 | return; 224 | } 225 | 226 | let current_pos = mouseevent_to_xy(e); 227 | filtered = filter_possible_moves(0, current_pos); 228 | if (filtered.length == 0) { 229 | return; 230 | } 231 | 232 | humans_move[0] = current_pos; 233 | humans_possible_moves = filtered; 234 | let pix = xy_to_pixel(humans_move[0]); 235 | drawTemporaryStone(context, pix); 236 | }); 237 | 238 | canvas.addEventListener('mouseup', function(e) { 239 | if (!is_humans_turn) { 240 | return; 241 | } 242 | 243 | if (humans_move[0] == null || humans_move[1] == null) { 244 | is_canceled = true; 245 | return; 246 | } 247 | 248 | let current_pos = mouseevent_to_xy(e); 249 | filtered = filter_possible_moves(2, current_pos); 250 | if (filtered.length == 0) { 251 | is_canceled = true; 252 | return; 253 | } 254 | 255 | humans_move[2] = current_pos; 256 | humans_possible_moves = filtered; 257 | let pix = xy_to_pixel(humans_move[2]); 258 | drawTemporaryStone(context, pix); 259 | }) 260 | 261 | canvas.addEventListener('mouseleave', function(e) { 262 | if (!is_humans_turn) { 263 | return; 264 | } 265 | 266 | is_canceled = true; 267 | }) 268 | } 269 | 270 | const human_player = function(color) { 271 | return async function(board) { 272 | if(!board.can_move(color)) { 273 | return board; 274 | } 275 | is_humans_turn = true; 276 | 277 | drawBoard(context, board, player_R, player_B, turn_color + "'s turn"); 278 | list_up_possible_moves(board, color); 279 | while (true) { 280 | await sleep(200); 281 | 282 | if (is_canceled) { 283 | humans_move = [null, null, null]; 284 | list_up_possible_moves(board, color); 285 | drawBoard(context, board, player_R, player_B, turn_color + "'s turn"); 286 | is_canceled = false; 287 | continue; 288 | } 289 | 290 | if (humans_move.includes(null)) { 291 | continue; 292 | } 293 | 294 | if(board.apply_move_if_possible( 295 | humans_move[0].x, humans_move[0].y, 296 | humans_move[1].x, humans_move[1].y, 297 | humans_move[2].x, humans_move[2].y, color)) { 298 | break; 299 | } 300 | 301 | humans_move = [null, null, null]; 302 | drawBoard(context, board, player_R, player_B, turn_color + "'s turn"); 303 | } 304 | humans_move = [null, null, null]; 305 | is_humans_turn = false; 306 | return board; 307 | } 308 | }; 309 | 310 | if(player_R == "Random") { 311 | playerR = module.RandomPlayer.new(RED, gen_seed(), gen_seed()); 312 | } else if (player_R == "Naive MC") { 313 | playerR = module.NaiveMonteCarlo.new(RED, gen_seed(), gen_seed(), time_limit); 314 | } else if (player_R == "UCT MC") { 315 | playerR = module.UCTMonteCarlo.new(RED, gen_seed(), gen_seed(), time_limit, 316 | UCT_exploration_weight_coeff, UCT_tree_expansion_threshold, board_size); 317 | } else { 318 | playerR = {play: human_player(RED)}; 319 | } 320 | if(player_B == "Random") { 321 | playerB = module.RandomPlayer.new(BLUE, gen_seed(), gen_seed()); 322 | } else if (player_B == "Naive MC") { 323 | playerB = module.NaiveMonteCarlo.new(BLUE, gen_seed(), gen_seed(), time_limit); 324 | } else if (player_B == "UCT MC") { 325 | playerB = module.UCTMonteCarlo.new(BLUE, gen_seed(), gen_seed(), time_limit, 326 | UCT_exploration_weight_coeff, UCT_tree_expansion_threshold, board_size); 327 | } else { 328 | playerB = {play: human_player(BLUE)}; 329 | } 330 | 331 | let gif_recorder = module.GameGifRecorder.new(); 332 | gif_recorder.add_frame(canvas.toDataURL('image/png')); 333 | while(!separo.is_gameover()) { 334 | if(separo.can_move(RED)) { 335 | turn_color = "Red"; 336 | separo = await playerR.play(separo); 337 | 338 | drawBoard(context, separo, player_R, player_B, "Blue's turn"); 339 | gif_recorder.add_frame(canvas.toDataURL('image/png')); 340 | } 341 | await sleep(100); 342 | // ------------------------------------------------------------------- 343 | if(separo.can_move(BLUE)) { 344 | turn_color = "Blue"; 345 | separo = await playerB.play(separo); 346 | 347 | drawBoard(context, separo, player_R, player_B, "Red's turn"); 348 | gif_recorder.add_frame(canvas.toDataURL('image/png')); 349 | } 350 | await sleep(100); 351 | } 352 | 353 | let last_score_red = separo.score(RED); 354 | let last_score_blue = separo.score(BLUE); 355 | let result = "draw!"; 356 | if (last_score_blue < last_score_red) { 357 | result = "Red wins!"; 358 | } else if (last_score_red < last_score_blue) { 359 | result = "Blue wins!"; 360 | } 361 | drawBoard(context, separo, player_R, player_B, result); 362 | 363 | gif_recorder.add_frame(canvas.toDataURL('image/png')); 364 | gif_base64 = "data:image/gif;base64," + gif_recorder.dump(); 365 | 366 | is_running = false; 367 | return; 368 | } 369 | 370 | function drawBoard(context, board, red_name, blue_name, msg) { 371 | 372 | const board_state = JSON.parse(board.to_json()); 373 | const red_score = board.score(RED); 374 | const blue_score = board.score(BLUE); 375 | 376 | const stones = board_state["stones"]; 377 | const roots = board_state["roots"]; 378 | 379 | // clear the board before redraw 380 | context.clearRect(0, 0, canvas_width, canvas_height); 381 | context.fillStyle=board_color; 382 | context.fillRect(0, 0, canvas_width, canvas_height); 383 | 384 | // show current score 385 | context.font = "20px sans-serif" 386 | let metrics = context.measureText(" | "); 387 | context.fillStyle = stroke_colors[RED]; 388 | context.textAlign = "right"; 389 | context.fillText(`${red_name}: ${red_score}`, 390 | (canvas_width - metrics.width)/ 2, 40.0); 391 | 392 | context.fillStyle = stroke_colors[BLUE]; 393 | context.textAlign = "left"; 394 | context.fillText(`${blue_name}: ${blue_score}`, 395 | (canvas_width + metrics.width)/ 2, 40.0); 396 | 397 | context.fillStyle = "rgb(0,0,0)" 398 | context.textAlign = "center" 399 | context.fillText("|", canvas_width / 2, 40.0); 400 | context.fillText(msg, canvas_width / 2, 70.0); 401 | 402 | // draw grid 403 | context.strokeStyle=grid_color; 404 | context.lineWidth=2.0 405 | 406 | context.beginPath(); 407 | for(let x=0; x { 32 | web_sys::console::log_1(&format!( $($arg)* ).into()) 33 | } 34 | } 35 | 36 | // Grid position. left-top: (0,0), right-bottom: (N,N). 37 | // We will never use 256x256 board. The max size would be 19x19. u8 is enough. 38 | #[wasm_bindgen] 39 | #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize)] 40 | pub struct Coord { 41 | x: i8, 42 | y: i8, 43 | } 44 | 45 | impl Coord { 46 | fn new(x: i8, y: i8) -> Self { 47 | Self { x, y } 48 | } 49 | } 50 | 51 | #[wasm_bindgen] 52 | #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize)] 53 | pub struct Move (Coord, Coord, Coord); 54 | 55 | #[wasm_bindgen] 56 | #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize_repr)] 57 | #[repr(u8)] 58 | pub enum Color { 59 | Red = 0, 60 | Blue = 1, 61 | } 62 | 63 | fn opponent_of(color: Color) -> Color { 64 | match color { 65 | Color::Red => Color::Blue, 66 | _ => Color::Red 67 | } 68 | } 69 | 70 | // To count number of separated regions, we will consider the following graph. 71 | // Each root cuts the edges that intersect with the root. The number of 72 | // connected components in the resulting graph. 73 | // The "ineffectual" region is a connected component that has less than 4 74 | // nodes in the following representation. 75 | // | 76 | // stone | 77 | // +----------(+)----------+ | 78 | // | o |`. o | | 79 | // | .' '. | `. '. | | 80 | // | o' 'o---o `. 'o--- ... | 81 | // | '. .' | '. `. | | 82 | // | 'o' | 'o `. | | 83 | // +-----|-----+-----|----(+) | 84 | // | o | o stone | 85 | // | .' '. | .' '. | | 86 | // | o' 'o---o' 'o--- ... | 87 | // | '. .' | '. .' | | 88 | // | 'o' | 'o' | | 89 | // +-----|-----+-----|-----+ | 90 | // ... ... | 91 | // | 92 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 93 | enum NodePos {N, E, S, W} 94 | 95 | #[derive(Debug, PartialEq, Eq, Clone)] 96 | struct Node { 97 | region: Option, // Region ID. u8::MAX < 19x19x4 < u16::MAX 98 | edges: ArrayVec<[(Coord, NodePos);3]>, 99 | } 100 | impl Node { 101 | pub fn new() -> Self { 102 | Node{region: None, edges: ArrayVec::new()} 103 | } 104 | } 105 | #[derive(Debug, Clone, PartialEq, Eq)] 106 | struct Graph { 107 | ngrids: u8, // width of the board (# of lines) - 1 108 | nodes: Vec, 109 | } 110 | impl Graph { 111 | pub fn new(width: usize) -> Self { 112 | assert!(3 < width); // board with only 3 lines? crazy. 113 | assert!(width < 20); 114 | 115 | let ngrids = width - 1; 116 | let nodes = vec![Node::new(); ngrids * ngrids * 4]; 117 | let mut graph = Graph{ngrids: ngrids as u8, nodes}; 118 | 119 | let x_max = ngrids as i8 - 1; 120 | let y_max = ngrids as i8 - 1; 121 | 122 | // construct the whole graph 123 | for x in 0..ngrids as i8 { 124 | for y in 0..ngrids as i8 { 125 | let crd = Coord::new(x, y); 126 | graph.at_mut(crd, NodePos::N).edges.push((crd, NodePos::E)); 127 | graph.at_mut(crd, NodePos::N).edges.push((crd, NodePos::W)); 128 | 129 | graph.at_mut(crd, NodePos::E).edges.push((crd, NodePos::N)); 130 | graph.at_mut(crd, NodePos::E).edges.push((crd, NodePos::S)); 131 | 132 | graph.at_mut(crd, NodePos::S).edges.push((crd, NodePos::E)); 133 | graph.at_mut(crd, NodePos::S).edges.push((crd, NodePos::W)); 134 | 135 | graph.at_mut(crd, NodePos::W).edges.push((crd, NodePos::N)); 136 | graph.at_mut(crd, NodePos::W).edges.push((crd, NodePos::S)); 137 | 138 | if x != 0 { 139 | graph.at_mut(Coord::new(x,y), NodePos::W).edges 140 | .push((Coord::new(x-1,y), NodePos::E)); 141 | } 142 | if x != x_max { 143 | graph.at_mut(Coord::new(x,y), NodePos::E).edges 144 | .push((Coord::new(x+1,y), NodePos::W)); 145 | } 146 | if y != 0 { 147 | graph.at_mut(Coord::new(x,y), NodePos::N).edges 148 | .push((Coord::new(x,y-1), NodePos::S)); 149 | } 150 | if y != y_max { 151 | graph.at_mut(Coord::new(x,y), NodePos::S).edges 152 | .push((Coord::new(x,y+1), NodePos::N)); 153 | } 154 | } 155 | } 156 | graph 157 | } 158 | 159 | pub fn apply_move(&mut self, next_move: Move) { 160 | // remove edges from self 161 | let Move(stone1, stone2, stone3) = next_move; 162 | 163 | // first root 164 | let dx = stone2.x - stone1.x; 165 | let dy = stone2.y - stone1.y; 166 | match (dx, dy) { 167 | (1, 1) => { 168 | self.remove_edge(stone1, NodePos::N, stone1, NodePos::W); 169 | self.remove_edge(stone1, NodePos::S, stone1, NodePos::E); 170 | } 171 | (1, -1) => { 172 | let pos = Coord::new(stone1.x, stone1.y - 1); 173 | if 0 <= pos.y { 174 | self.remove_edge(pos, NodePos::N, pos, NodePos::E); 175 | self.remove_edge(pos, NodePos::S, pos, NodePos::W); 176 | } 177 | } 178 | (-1, 1) => { 179 | let pos = Coord::new(stone1.x - 1, stone1.y); 180 | if 0 <= pos.x { 181 | self.remove_edge(pos, NodePos::N, pos, NodePos::E); 182 | self.remove_edge(pos, NodePos::S, pos, NodePos::W); 183 | } 184 | } 185 | (-1, -1) => { 186 | self.remove_edge(stone2, NodePos::N, stone2, NodePos::W); 187 | self.remove_edge(stone2, NodePos::S, stone2, NodePos::E); 188 | } 189 | _ => { 190 | assert!(false); 191 | } 192 | } 193 | // second root 194 | let dx = stone3.x - stone2.x; 195 | let dy = stone3.y - stone2.y; 196 | 197 | let upper = self.ngrids as i8; 198 | match (dx, dy) { 199 | (1, 0) => { 200 | if stone2.x < upper && 1 <= stone2.y && stone2.y < upper { 201 | self.remove_edge(Coord::new(stone2.x, stone2.y-1), NodePos::S, 202 | stone2, NodePos::N); 203 | } 204 | } 205 | (-1, 0) => { 206 | if stone3.x < upper && 1 <= stone3.y && stone3.y < upper { 207 | self.remove_edge(Coord::new(stone3.x, stone3.y-1), NodePos::S, 208 | stone3, NodePos::N); 209 | } 210 | } 211 | (0, 1) => { 212 | if 1 <= stone2.x && stone2.x < upper && stone2.y < upper { 213 | self.remove_edge(Coord::new(stone2.x-1, stone2.y), NodePos::E, 214 | stone2, NodePos::W); 215 | } 216 | } 217 | (0, -1) => { 218 | if 1 <= stone3.x && stone3.x < upper && stone3.y < upper { 219 | self.remove_edge(Coord::new(stone3.x-1, stone3.y), NodePos::E, 220 | stone3, NodePos::W); 221 | } 222 | } 223 | _ => { 224 | assert!(false); 225 | } 226 | } 227 | } 228 | 229 | // 19x19 < u16::MAX. 230 | pub fn score(&mut self) -> u16 { 231 | for node in self.nodes.iter_mut() { 232 | node.region = None; 233 | } 234 | 235 | let mut score: u16 = 0; 236 | let mut region:u16 = 0; 237 | while let Some(idx) = self.nodes.iter().position(|n| n.region == None) { 238 | let pos = match idx % 4 { 239 | 0 => NodePos::N, 240 | 1 => NodePos::E, 241 | 2 => NodePos::S, 242 | _ => NodePos::W, // 3 243 | }; 244 | let x = idx / 4 / self.ngrids as usize; 245 | let y = idx / 4 % self.ngrids as usize; 246 | assert!(x <= i8::MAX as usize); 247 | assert!(y <= i8::MAX as usize); 248 | 249 | let crd = Coord::new(x as i8, y as i8); 250 | if 4 < self.find_connected_component(region, crd, pos) { 251 | score += 1; 252 | } 253 | region += 1; 254 | } 255 | score 256 | } 257 | 258 | fn find_connected_component(&mut self, region: u16, crd: Coord, pos: NodePos) -> u16 { 259 | let mut num: u16 = 0; 260 | let mut queue = Vec::new(); 261 | queue.push((crd, pos)); 262 | 263 | while let Some((crd, pos)) = queue.pop() { 264 | num += 1; 265 | self.at_mut(crd, pos).region = Some(region); 266 | for (n_crd, n_pos) in self.at(crd, pos).edges.iter() { 267 | if self.at(*n_crd, *n_pos).region == None { 268 | queue.push((*n_crd, *n_pos)); 269 | } 270 | } 271 | } 272 | num 273 | } 274 | 275 | fn remove_edge(&mut self, crd1: Coord, pos1: NodePos, 276 | crd2: Coord, pos2: NodePos) { 277 | assert!(self.at(crd1, pos1).edges.contains(&(crd2, pos2))); 278 | assert!(self.at(crd2, pos2).edges.contains(&(crd1, pos1))); 279 | 280 | self.at_mut(crd1, pos1).edges.retain(|x| *x != (crd2, pos2)); 281 | self.at_mut(crd2, pos2).edges.retain(|x| *x != (crd1, pos1)); 282 | } 283 | 284 | fn at(&self, coord: Coord, pos: NodePos) -> &Node { 285 | let idx = ((coord.x as usize) * (self.ngrids as usize) + 286 | (coord.y as usize)) * 4 + match pos { 287 | NodePos::N => 0, 288 | NodePos::E => 1, 289 | NodePos::S => 2, 290 | NodePos::W => 3, 291 | }; 292 | 293 | &self.nodes[idx] 294 | } 295 | fn at_mut(&mut self, coord: Coord, pos: NodePos) -> &mut Node { 296 | let idx = ((coord.x as usize) * (self.ngrids as usize) + 297 | (coord.y as usize)) * 4 + match pos { 298 | NodePos::N => 0, 299 | NodePos::E => 1, 300 | NodePos::S => 2, 301 | NodePos::W => 3, 302 | }; 303 | &mut self.nodes[idx] 304 | } 305 | } 306 | 307 | // root direction. Note that the y axis is upside down (left-top is the origin) 308 | // 309 | // (-1,-1) (0,-1) (1,-1) 310 | // o-----+-----+ 311 | // |`. | .'| 312 | // | `. | .' | 313 | // (-1,0) +----`o'----o (1,0) 314 | // | .'|`. | 315 | // | .' | `. | 316 | // +'----+----`+ 317 | // (-1, 1) (0,1) (1,1) 318 | // 319 | // There are only 8 patterns shown above. i8 is already too much. 320 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 321 | pub struct Dir (i8, i8); 322 | 323 | // the roots cannot form 45 degrees. So the minimum angle is 90 degree. 324 | // 360/90 = 4, so we would have up to 4 edges per a grid. 325 | #[derive(Debug, PartialEq, Eq, Clone)] 326 | pub struct Grid { 327 | color: Option, 328 | roots: ArrayVec<[Dir; 4]>, 329 | } 330 | 331 | impl Grid { 332 | fn new() -> Self { 333 | Grid{color: None, roots: ArrayVec::new()} 334 | } 335 | // Note that it checks the direction of the roots but not the color. 336 | fn is_valid_root(&self, dir: Dir) -> bool { 337 | self.roots.iter() 338 | .find(|d| i8::abs(dir.0 - d.0) + i8::abs(dir.1 - d.1) <= 1) 339 | .is_none() 340 | } 341 | } 342 | 343 | 344 | #[wasm_bindgen] 345 | #[derive(Debug, Clone, PartialEq, Eq)] 346 | pub struct Board { 347 | width: u8, // normally, 9 (9x9 board) upto 19x19 348 | grids: Vec, // 9x9 grids there (if width == 9) 349 | red: Graph, // to calculate score 350 | blue: Graph, // to calculate score 351 | } 352 | 353 | #[wasm_bindgen] 354 | impl Board { 355 | pub fn new(width: usize) -> Board { 356 | let mut board = Board{ 357 | width: width as u8, 358 | grids: vec![Grid::new(); width*width], 359 | red: Graph::new(width), 360 | blue: Graph::new(width) 361 | }; 362 | 363 | let lower = 0; 364 | let upper = width - 1; 365 | board.grids[lower * width + lower].color = Some(Color::Red); 366 | board.grids[lower * width + upper].color = Some(Color::Blue); 367 | board.grids[upper * width + lower].color = Some(Color::Blue); 368 | board.grids[upper * width + upper].color = Some(Color::Red); 369 | 370 | board 371 | } 372 | 373 | fn possible_moves(&self, turn: Color) -> Vec { 374 | let mut moves = Vec::new(); 375 | for (idx1, grid) in self.grids.iter().enumerate() { 376 | if grid.color == Some(turn) { // the same color 377 | 378 | // coordinate of the first stone 379 | let x1 = (idx1 / self.width as usize) as i8; 380 | let y1 = (idx1 % self.width as usize) as i8; 381 | 382 | for dir1 in [Dir(1,1), Dir(-1,1), Dir(-1,-1), Dir(1,-1)].iter() { 383 | 384 | // check root collision at the first stone 385 | // skip roots that forms 45 degree 386 | if !grid.is_valid_root(*dir1) { 387 | continue; 388 | } 389 | 390 | // check middle stone collision 391 | let x2 = x1 + dir1.0; 392 | let y2 = y1 + dir1.1; 393 | 394 | if x2 < 0 || y2 < 0 || self.width as i8 <= x2 || self.width as i8 <= y2 { 395 | continue; 396 | } 397 | 398 | let idx2 = x2 as usize * self.width as usize + y2 as usize; 399 | if self.grids[idx2].color.is_some() { 400 | continue; 401 | } 402 | // check root collision at the second stone 403 | // Note that the direction from the second stone is opposite 404 | // in sign. 405 | if !self.grids[idx2].is_valid_root(Dir(-dir1.0, -dir1.1)) { 406 | continue; 407 | } 408 | // possible next roots 409 | { 410 | let dir2 = Dir(dir1.0, 0); 411 | // check root collision at the second stone 412 | if !self.grids[idx2].is_valid_root(dir2) { 413 | continue; 414 | } 415 | // check stone/root collision at the third stone 416 | let x3 = x2 + dir2.0; 417 | let y3 = y2 + dir2.1; 418 | 419 | if !(x3 < 0 || y3 < 0 || self.width as i8 <= x3 || self.width as i8 <= y3) { 420 | let idx3 = x3 as usize * self.width as usize + y3 as usize; 421 | if (self.grids[idx3].color == None || self.grids[idx3].color == Some(turn)) && 422 | self.grids[idx3].is_valid_root(Dir(-dir2.0, -dir2.1)) { 423 | moves.push(Move(Coord::new(x1, y1), Coord::new(x2, y2), Coord::new(x3, y3))); 424 | } 425 | } 426 | } 427 | { 428 | let dir2 = Dir(0, dir1.1); 429 | // check root collision at the second stone 430 | if !self.grids[idx2].is_valid_root(dir2) { 431 | continue; 432 | } 433 | // check stone/root collision at the third stone 434 | let x3 = x2 + dir2.0; 435 | let y3 = y2 + dir2.1; 436 | 437 | if !(x3 < 0 || y3 < 0 || self.width as i8 <= x3 || self.width as i8 <= y3) { 438 | let idx3 = x3 as usize * self.width as usize + y3 as usize; 439 | if (self.grids[idx3].color == None || self.grids[idx3].color == Some(turn)) && 440 | self.grids[idx3].is_valid_root(Dir(-dir2.0, -dir2.1)) { 441 | moves.push(Move(Coord::new(x1, y1), Coord::new(x2, y2), Coord::new(x3, y3))); 442 | } 443 | } 444 | } 445 | } 446 | } 447 | } 448 | moves 449 | } 450 | 451 | pub fn apply_move_if_possible(&mut self, 452 | x1: i32, y1: i32, x2: i32, y2: i32, x3: i32, y3: i32, color: Color 453 | ) -> bool { 454 | let next_move = Move(Coord::new(x1 as i8, y1 as i8), 455 | Coord::new(x2 as i8, y2 as i8), 456 | Coord::new(x3 as i8, y3 as i8)); 457 | if self.is_valid_move(next_move, color) { 458 | self.apply_move(next_move, color); 459 | true 460 | } else { 461 | false 462 | } 463 | } 464 | 465 | pub fn can_move(&self, turn: Color) -> bool { 466 | !self.possible_moves(turn).is_empty() 467 | } 468 | 469 | pub fn is_gameover(&self) -> bool { 470 | self.possible_moves(Color::Red ).is_empty() && 471 | self.possible_moves(Color::Blue).is_empty() 472 | } 473 | 474 | fn is_valid_move(&self, next_move: Move, turn: Color) -> bool { 475 | self.possible_moves(turn).contains(&next_move) 476 | } 477 | 478 | fn apply_move(&mut self, next_move: Move, turn: Color) { 479 | debug_assert!(self.is_valid_move(next_move, turn)); 480 | // apply next_move to the board 481 | 482 | let Move(stone1, stone2, stone3) = next_move; 483 | 484 | let idx1 = stone1.x as usize * self.width as usize + stone1.y as usize; 485 | let idx2 = stone2.x as usize * self.width as usize + stone2.y as usize; 486 | let idx3 = stone3.x as usize * self.width as usize + stone3.y as usize; 487 | 488 | self.grids[idx2].color = Some(turn); 489 | self.grids[idx3].color = Some(turn); 490 | 491 | self.grids[idx1].roots.push(Dir(stone2.x - stone1.x, stone2.y - stone1.y)); 492 | self.grids[idx2].roots.push(Dir(stone1.x - stone2.x, stone1.y - stone2.y)); 493 | 494 | self.grids[idx2].roots.push(Dir(stone3.x - stone2.x, stone3.y - stone2.y)); 495 | self.grids[idx3].roots.push(Dir(stone2.x - stone3.x, stone2.y - stone3.y)); 496 | 497 | // apply next_move to internal graph 498 | match turn { 499 | Color::Red => {self.red .apply_move(next_move)} 500 | Color::Blue => {self.blue.apply_move(next_move)} 501 | } 502 | } 503 | 504 | pub fn score(&mut self, color: Color) -> u16 { 505 | match color { 506 | Color::Red => {self.red .score()} 507 | Color::Blue => {self.blue.score()} 508 | } 509 | } 510 | 511 | pub fn to_json(&self) -> String { 512 | #[derive(Serialize)] 513 | struct Stone { 514 | x: i8, 515 | y: i8, 516 | color: i8, 517 | } 518 | 519 | #[derive(Serialize)] 520 | struct Root { 521 | x1: i8, 522 | y1: i8, 523 | x2: i8, 524 | y2: i8, 525 | color: i8, 526 | } 527 | 528 | #[derive(Serialize)] 529 | struct BoardJson { 530 | stones: Vec, 531 | roots: Vec, 532 | } 533 | 534 | let mut stones = Vec::new(); 535 | let mut roots = Vec::new(); 536 | for x in 0..self.width as i8 { 537 | for y in 0..self.width as i8 { 538 | let idx = (x as usize) * (self.width as usize) + (y as usize); 539 | if let Some(color) = self.grids[idx].color { 540 | let color = color as i8; 541 | stones.push(Stone { x, y, color }); 542 | 543 | for dir in self.grids[idx].roots.iter() { 544 | roots.push(Root { 545 | x1: x, 546 | y1: y, 547 | x2: x + dir.0, 548 | y2: y + dir.1, 549 | color, 550 | }); 551 | } 552 | } 553 | } 554 | } 555 | 556 | serde_json::to_string(&BoardJson { stones, roots }).unwrap() 557 | } 558 | pub fn possible_moves_as_json(&self) -> String { 559 | #[derive(Serialize)] 560 | struct PossibleMove { 561 | stones: Move, 562 | color: Color, 563 | } 564 | 565 | let mut moves = Vec::new(); 566 | for stones in self.possible_moves(Color::Red) { 567 | moves.push(PossibleMove { 568 | stones: stones, 569 | color: Color::Red, 570 | }); 571 | } 572 | for stones in self.possible_moves(Color::Blue) { 573 | moves.push(PossibleMove { 574 | stones: stones, 575 | color: Color::Blue, 576 | }); 577 | } 578 | 579 | serde_json::to_string(&moves).unwrap() 580 | } 581 | 582 | fn playout(&mut self, init_turn: Color, rng: &mut R) -> Option { 583 | let next_turn = opponent_of(init_turn); 584 | while !self.is_gameover() { 585 | { 586 | let moves = self.possible_moves(init_turn); 587 | if !moves.is_empty() { 588 | self.apply_move(moves[rng.gen_range(0, moves.len())], init_turn); 589 | } 590 | } 591 | { 592 | let moves = self.possible_moves(next_turn); 593 | if !moves.is_empty() { 594 | self.apply_move(moves[rng.gen_range(0, moves.len())], next_turn); 595 | } 596 | } 597 | } 598 | let red_score = self.score(Color::Red); 599 | let blue_score = self.score(Color::Blue); 600 | if blue_score < red_score { 601 | Some(Color::Red) 602 | } else if red_score < blue_score { 603 | Some(Color::Blue) 604 | } else { 605 | None 606 | } 607 | } 608 | } 609 | 610 | fn convert_seed(seed0: u32, seed1: u32) -> u64 { 611 | (seed0 as u64) + ((seed1 as u64) << 32) 612 | } 613 | 614 | #[wasm_bindgen] 615 | pub struct RandomPlayer { 616 | pub color: Color, 617 | rng: rand::rngs::StdRng, 618 | } 619 | 620 | #[wasm_bindgen] 621 | impl RandomPlayer { 622 | pub fn new(color: Color, seed0: u32, seed1: u32) -> Self { 623 | let seed = convert_seed(seed0, seed1); 624 | RandomPlayer{color, rng: rand::rngs::StdRng::seed_from_u64(seed)} 625 | } 626 | pub fn play(&mut self, mut board: Board) -> Board { 627 | let moves = board.possible_moves(self.color); 628 | if !moves.is_empty() { 629 | board.apply_move(moves[self.rng.gen_range(0, moves.len())], self.color); 630 | } 631 | board 632 | } 633 | } 634 | 635 | #[wasm_bindgen] 636 | pub struct NaiveMonteCarlo { 637 | pub color: Color, 638 | rng: rand::rngs::StdRng, 639 | time_limit: Duration, 640 | } 641 | 642 | #[wasm_bindgen] 643 | impl NaiveMonteCarlo { 644 | pub fn new(color: Color, seed0: u32, seed1: u32, timelimit: u32) -> Self { 645 | let seed = convert_seed(seed0, seed1); 646 | NaiveMonteCarlo{ 647 | color, 648 | rng: rand::rngs::StdRng::seed_from_u64(seed), 649 | time_limit: Duration::from_secs(timelimit as u64), 650 | } 651 | } 652 | 653 | pub fn play(&mut self, board: Board) -> Board { 654 | 655 | let mut candidates = Vec::<(_, _, u32)>::new(); 656 | for possible_move in board.possible_moves(self.color).iter() { 657 | let mut cand_board = board.clone(); 658 | cand_board.apply_move(*possible_move, self.color); 659 | candidates.push((*possible_move, cand_board, 0)); 660 | } 661 | 662 | if candidates.is_empty() { 663 | return board 664 | } 665 | 666 | // console_log!("{} possible moves are there", candidates.len()); 667 | let stop = Instant::now() + self.time_limit; 668 | let mut samples: usize = 0; 669 | while Instant::now() < stop { 670 | for candidate in candidates.iter_mut() { 671 | let mut tmp = candidate.1.clone(); 672 | if tmp.playout(self.color, &mut self.rng) == Some(self.color) { 673 | candidate.2 += 1; 674 | } 675 | } 676 | samples += 1; 677 | } 678 | // console_log!("{} samples simulated for each {} moves. in total: {}", 679 | // samples, candidates.len(), samples * candidates.len()); 680 | // console_log!("win_rates = {:?}", candidates.iter() 681 | // .map(|x| x.2 as f64 / samples as f64).collect::>()); 682 | 683 | candidates.sort_by_key(|x| x.2); 684 | console_log!("{:?}, estimated win rate = {}.", self.color, 685 | candidates.last().unwrap().2 as f64 / samples as f64); 686 | candidates.pop().unwrap().1 687 | } 688 | } 689 | 690 | #[wasm_bindgen] 691 | pub struct UCTMonteCarlo { 692 | pub color: Color, 693 | rng: rand::rngs::StdRng, 694 | time_limit: Duration, 695 | ucb1_coeff: f64, 696 | expand_threshold: u32, 697 | root: Rc>, 698 | } 699 | 700 | #[derive(Debug, Clone)] 701 | struct UCTNode { 702 | win: u32, 703 | lose: u32, 704 | samples: u32, 705 | children: Vec>>, 706 | parent: Weak>, 707 | color: Color, 708 | board: Board, 709 | } 710 | 711 | impl UCTNode { 712 | fn new(color: Color, board: Board) -> Self { 713 | UCTNode{win: 0, lose: 0, samples: 0, children: Vec::new(), parent: Weak::new(), color, board} 714 | } 715 | 716 | fn win_rate(&self) -> f64 { 717 | if self.samples == 0 { // avoid NaN 718 | 0.5 // no information, half-half. 719 | } else { 720 | self.win as f64 / self.samples as f64 721 | } 722 | } 723 | fn lose_rate(&self) -> f64 { 724 | if self.samples == 0 { // avoid NaN 725 | 0.5 // no information, half-half. 726 | } else { 727 | self.lose as f64 / self.samples as f64 728 | } 729 | } 730 | 731 | fn ucb1(&self, coef: f64, logn: f64) -> f64 { 732 | if self.samples == 0 { 733 | f64::INFINITY 734 | } else { 735 | self.win_rate() + coef * f64::sqrt(logn / self.samples as f64) 736 | } 737 | } 738 | } 739 | 740 | fn expand_node(node_ptr: &Rc>) { 741 | let mut node: RefMut = node_ptr.borrow_mut(); 742 | let possible_moves = node.board.possible_moves(node.color); 743 | for possible_move in possible_moves.iter() { 744 | let mut possible_board: Board = node.board.clone(); 745 | possible_board.apply_move(*possible_move, node.color); 746 | 747 | // child node represents opponent's turn 748 | let child = Rc::new(RefCell::new( 749 | UCTNode::new(opponent_of(node.color), possible_board))); 750 | 751 | child.borrow_mut().parent = Rc::downgrade(node_ptr); 752 | node.children.push(child); 753 | } 754 | 755 | // handle passed turn 756 | if node.children.is_empty() { 757 | // if passed, the same board is passed to opponent 758 | let child = Rc::new(RefCell::new( 759 | UCTNode::new(opponent_of(node.color), node.board.clone()))); 760 | child.borrow_mut().parent = Rc::downgrade(node_ptr); 761 | node.children.push(child); 762 | } 763 | assert!(0 < node.children.len()); 764 | } 765 | 766 | // fn count_node_and_depth(root: Rc>, depth: usize) -> (usize, usize) { 767 | // if root.borrow().children.is_empty() { 768 | // return (1, depth); 769 | // } 770 | // root.borrow().children.iter() 771 | // .map(|node| count_node_and_depth(Rc::clone(node), depth+1)) 772 | // .fold((1, depth), |(n1, d1), (n2, d2)| (n1+n2, usize::max(d1,d2))) 773 | // } 774 | 775 | #[wasm_bindgen] 776 | impl UCTMonteCarlo { 777 | pub fn new(color: Color, seed0: u32, seed1: u32, timelimit: u32, ucb1_coeff: f64, expand_threshold: u32, board_width: usize) -> Self { 778 | let seed = convert_seed(seed0, seed1); 779 | let root = if color == Color::Red { 780 | // we need to init root with a board before starting... 781 | // play() function re-use the previous estimation. 782 | let ancester = Rc::new(RefCell::new( 783 | UCTNode::new(Color::Blue, Board::new(board_width)))); 784 | let root = Rc::new(RefCell::new( 785 | UCTNode::new(Color::Red, Board::new(board_width)))); 786 | ancester.borrow_mut().children.push(root); 787 | ancester 788 | } else { 789 | let root = Rc::new(RefCell::new( 790 | UCTNode::new(Color::Red, Board::new(board_width)))); 791 | expand_node(&root); 792 | root 793 | }; 794 | UCTMonteCarlo{ 795 | color, 796 | rng: rand::rngs::StdRng::seed_from_u64(seed), 797 | time_limit: Duration::from_secs(timelimit as u64), 798 | ucb1_coeff, 799 | expand_threshold, 800 | root, 801 | } 802 | } 803 | 804 | pub fn play(&mut self, board: Board) -> Board { 805 | if !board.can_move(self.color) { 806 | return board; 807 | } 808 | 809 | // find the current state from the children of root node 810 | // (means: find opponent's move) 811 | let tmp = Rc::clone(self.root.borrow().children.iter() 812 | .find(|x| x.borrow().board.grids == board.grids).unwrap()); 813 | self.root = tmp; 814 | self.root.borrow_mut().parent = Weak::new(); // discard ancesters 815 | assert_eq!(self.root.borrow().color, self.color); 816 | 817 | // search and expand the tree 818 | let stop = Instant::now() + self.time_limit; 819 | while Instant::now() < stop { 820 | let mut node = Rc::clone(&self.root); 821 | let mut depth = 0; 822 | let logn = f64::ln(node.borrow().samples as f64); 823 | while !node.borrow().children.is_empty() { 824 | let tmp = Rc::clone(node.borrow_mut().children.iter() 825 | .max_by(|a, b| a.borrow().ucb1(self.ucb1_coeff, logn) 826 | .partial_cmp(&b.borrow().ucb1(self.ucb1_coeff, logn)) 827 | .unwrap_or(std::cmp::Ordering::Less)) 828 | .unwrap()); 829 | node = tmp; 830 | depth += 1; 831 | } 832 | let wins = node.borrow().board.clone() 833 | .playout(node.borrow().color, &mut self.rng); 834 | 835 | if wins == Some(opponent_of(node.borrow().color)) { 836 | node.borrow_mut().win += 1; 837 | } else if wins == Some(node.borrow().color) { 838 | // to distinguish draw and lose, it counts both wins and loses 839 | node.borrow_mut().lose += 1; 840 | } 841 | node.borrow_mut().samples += 1; 842 | 843 | // do this after `samples += 1` 844 | if self.expand_threshold <= node.borrow().samples { 845 | if !node.borrow().board.is_gameover() { 846 | expand_node(&node); 847 | } 848 | } 849 | 850 | while let Some(parent) = Rc::clone(&node).borrow().parent.upgrade() { 851 | depth -= 1; 852 | parent.borrow_mut().samples += 1; 853 | if wins == Some(opponent_of(parent.borrow().color)) { 854 | parent.borrow_mut().win += 1; 855 | } else if wins == Some(parent.borrow().color) { 856 | parent.borrow_mut().lose += 1; 857 | } 858 | node = parent; 859 | } 860 | assert_eq!(depth, 0); 861 | } 862 | 863 | // performance log 864 | // { 865 | // console_log!("{} samples used to estimate win/lose rate", 866 | // self.root.borrow().samples); 867 | // let (n, d) = count_node_and_depth(Rc::clone(&self.root), 1); 868 | // console_log!("{} nodes with depth {} is used", n, d); 869 | // } 870 | 871 | // choose the next root by chosing the node with max win rate 872 | let tmp = Rc::clone(self.root.borrow_mut().children.iter() 873 | .max_by(|a, b| (a.borrow().win_rate() - a.borrow().lose_rate()) 874 | .partial_cmp(&(b.borrow().win_rate() - b.borrow().lose_rate())) 875 | .unwrap_or(std::cmp::Ordering::Less)) 876 | .unwrap()); 877 | self.root = tmp; 878 | self.root.borrow_mut().parent = Weak::new(); // discard ancesters 879 | console_log!("{:?}, estimated win rate = {}, lose rate = {}.", self.color, 880 | self.root.borrow().win_rate(), self.root.borrow().lose_rate()); 881 | 882 | if self.root.borrow().children.is_empty() && 883 | !self.root.borrow().board.is_gameover() { 884 | console_log!("root.children is empty. Too short time limit?"); 885 | } 886 | assert!(!self.root.borrow().children.is_empty() || 887 | self.root.borrow().board.is_gameover()); 888 | // return the board in the root node 889 | self.root.borrow().board.clone() 890 | } 891 | } 892 | 893 | #[cfg(test)] 894 | mod tests { 895 | use super::*; 896 | use serde_json::{json, Value}; 897 | 898 | #[test] 899 | fn possible_moves() { 900 | let board = Board::new(9); 901 | let possibles = board.possible_moves(Color::Red); 902 | assert_eq!(possibles.len(), 4); 903 | assert!(possibles.contains(&Move(Coord::new(0, 0), Coord::new(1, 1), Coord::new(1, 2)))); 904 | assert!(possibles.contains(&Move(Coord::new(0, 0), Coord::new(1, 1), Coord::new(2, 1)))); 905 | assert!(possibles.contains(&Move(Coord::new(8, 8), Coord::new(7, 7), Coord::new(7, 6)))); 906 | assert!(possibles.contains(&Move(Coord::new(8, 8), Coord::new(7, 7), Coord::new(6, 7)))); 907 | } 908 | 909 | #[test] 910 | fn possible_moves_as_json() { 911 | let board = Board::new(9); 912 | 913 | let red_moves = board.possible_moves(Color::Red); 914 | let blue_moves = board.possible_moves(Color::Blue); 915 | 916 | let moves_from_json: Value = serde_json::from_str(&board.possible_moves_as_json()).unwrap(); 917 | 918 | for (i, Move(s1, s2, s3)) in red_moves.iter().enumerate() { 919 | let stone1 = &moves_from_json[i]["stones"][0]; 920 | let stone2 = &moves_from_json[i]["stones"][1]; 921 | let stone3 = &moves_from_json[i]["stones"][2]; 922 | let color = &moves_from_json[i]["color"]; 923 | 924 | assert_eq!(stone1["x"].as_i64().unwrap(), s1.x as i64); 925 | assert_eq!(stone1["y"].as_i64().unwrap(), s1.y as i64); 926 | assert_eq!(stone2["x"].as_i64().unwrap(), s2.x as i64); 927 | assert_eq!(stone2["y"].as_i64().unwrap(), s2.y as i64); 928 | assert_eq!(stone3["x"].as_i64().unwrap(), s3.x as i64); 929 | assert_eq!(stone3["y"].as_i64().unwrap(), s3.y as i64); 930 | assert_eq!(color.as_i64().unwrap(), Color::Red as i64); 931 | } 932 | 933 | for (i, Move(s1, s2, s3)) in blue_moves.iter().enumerate() { 934 | let i = i + red_moves.len(); 935 | let stone1 = &moves_from_json[i]["stones"][0]; 936 | let stone2 = &moves_from_json[i]["stones"][1]; 937 | let stone3 = &moves_from_json[i]["stones"][2]; 938 | let color = &moves_from_json[i]["color"]; 939 | 940 | assert_eq!(stone1["x"].as_i64().unwrap(), s1.x as i64); 941 | assert_eq!(stone1["y"].as_i64().unwrap(), s1.y as i64); 942 | assert_eq!(stone2["x"].as_i64().unwrap(), s2.x as i64); 943 | assert_eq!(stone2["y"].as_i64().unwrap(), s2.y as i64); 944 | assert_eq!(stone3["x"].as_i64().unwrap(), s3.x as i64); 945 | assert_eq!(stone3["y"].as_i64().unwrap(), s3.y as i64); 946 | assert_eq!(color.as_i64().unwrap(), Color::Blue as i64); 947 | } 948 | } 949 | 950 | #[test] 951 | fn to_json() { 952 | let mut board = Board::new(9); 953 | 954 | board.apply_move(Move(Coord::new(0, 0), Coord::new(1, 1), Coord::new(1, 2)), Color::Red); 955 | board.apply_move(Move(Coord::new(0, 8), Coord::new(1, 7), Coord::new(1, 6)), Color::Blue); 956 | 957 | let board_from_json: Value = serde_json::from_str(&board.to_json()).unwrap(); 958 | let stones = &board_from_json["stones"].as_array().unwrap(); 959 | let roots = &board_from_json["roots"].as_array().unwrap(); 960 | 961 | assert_eq!(stones.len(), 8); 962 | let correct_stones = vec![ 963 | json!({"x": 0, "y": 0, "color": Color::Red}), 964 | json!({"x": 1, "y": 1, "color": Color::Red}), 965 | json!({"x": 1, "y": 2, "color": Color::Red}), 966 | json!({"x": 8, "y": 8, "color": Color::Red}), 967 | json!({"x": 0, "y": 8, "color": Color::Blue}), 968 | json!({"x": 1, "y": 7, "color": Color::Blue}), 969 | json!({"x": 1, "y": 6, "color": Color::Blue}), 970 | json!({"x": 8, "y": 0, "color": Color::Blue}), 971 | ]; 972 | for correct in correct_stones { 973 | assert!(stones.contains(&correct)); 974 | } 975 | 976 | assert_eq!(roots.len(), 4*2); 977 | let correct_roots = vec![ 978 | json!({"x1": 0, "y1": 0, "x2": 1, "y2": 1, "color": Color::Red}), 979 | json!({"x1": 1, "y1": 1, "x2": 0, "y2": 0, "color": Color::Red}), 980 | json!({"x1": 1, "y1": 1, "x2": 1, "y2": 2, "color": Color::Red}), 981 | json!({"x1": 1, "y1": 2, "x2": 1, "y2": 1, "color": Color::Red}), 982 | json!({"x1": 0, "y1": 8, "x2": 1, "y2": 7, "color": Color::Blue}), 983 | json!({"x1": 1, "y1": 7, "x2": 0, "y2": 8, "color": Color::Blue}), 984 | json!({"x1": 1, "y1": 7, "x2": 1, "y2": 6, "color": Color::Blue}), 985 | json!({"x1": 1, "y1": 6, "x2": 1, "y2": 7, "color": Color::Blue}), 986 | ]; 987 | for correct in correct_roots { 988 | assert!(roots.contains(&correct)); 989 | } 990 | } 991 | } 992 | --------------------------------------------------------------------------------