diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 7c06bd1..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,383 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "cc" -version = "1.0.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clap" -version = "4.0.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" -dependencies = [ - "bitflags", - "clap_derive", - "clap_lex", - "is-terminal", - "once_cell", - "strsim", - "termcolor", -] - -[[package]] -name = "clap_derive" -version = "4.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "getrandom" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "heck" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" - -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "is-terminal" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" -dependencies = [ - "hermit-abi", - "io-lifetimes", - "rustix", - "windows-sys", -] - -[[package]] -name = "libc" -version = "0.2.138" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" - -[[package]] -name = "linux-raw-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" - -[[package]] -name = "minmax" -version = "0.1.0" -dependencies = [ - "clap", - "rand", -] - -[[package]] -name = "once_cell" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" - -[[package]] -name = "os_str_bytes" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rustix" -version = "0.36.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "unicode-ident" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index ce58d84..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "minmax" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -clap = { version = "4.0.29", features = ["derive"] } -rand = "0.8.5" - - -[profile.dev] -opt-level = 3 diff --git a/README.md b/README.md new file mode 100644 index 0000000..4810cac --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Connect Four Template + +A start of a project implementing a variant of the game "[Connect Four](https://de.wikipedia.org/wiki/Vier_gewinnt)". + +## Mission + +1. Extend the existing code such that the GreedyPlayer works. +2. Implement a strong opponent using MinMax/Negamax with a fixed depth (like 10). +3. Implement a perfect Player and optimize it for speed. + +## Resources + +- https://de.wikipedia.org/wiki/Minimax-Algorithmus#Implementierung +- https://de.wikipedia.org/wiki/Alpha-Beta-Suche#Implementierung +- https://en.wikipedia.org/wiki/Zobrist_hashing diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..c062119 --- /dev/null +++ b/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'java' +} + +group = 'ch.bbw.m411' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1' + testImplementation 'org.assertj:assertj-core:3.23.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.1' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/build.rs b/build.rs deleted file mode 100644 index eb40c49..0000000 --- a/build.rs +++ /dev/null @@ -1,164 +0,0 @@ -//! Builds the board state table -//! -//! The board is encoded as an 18 bit integer, two bits for each position. -//! The position are in the bits row by row with the first position being the -//! least significant two bits. -//! ```text -//! 0 => X -//! 1 => O -//! 2 => Empty -//! 3 => INVALID -//! ``` -//! -//! Then, this integer is used as an index into the winner table. -//! Each byte of the winner table contains the information about the game state. -//! ```text -//! 0 => X -//! 1 => O -//! 2 => In Progress -//! 3 => Draw -//! _ => INVALID -//! ``` - -use std::{fs::File, io::Write, path::PathBuf}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Player { - X, - O, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum State { - Winner(Player), - InProgress, - Draw, -} - -impl Player { - fn from_u8(num: u8) -> Option { - match num { - 0 => Some(Player::X), - 1 => Some(Player::O), - 2 => None, - _ => panic!("Invalid value {num}"), - } - } -} - -#[derive(Clone, Copy)] -struct Board(u32); - -impl Board { - fn new(num: u32) -> Option { - for i in 0..16 { - let next_step = num >> (i * 2); - let mask = 0b11; - let pos = next_step & mask; - if pos == 3 { - return None; - } - } - - Some(Self(num)) - } - - fn validate(&self) { - let board = self.0; - for i in 0..16 { - let next_step = board >> (i * 2); - let mask = 0b11; - let pos = next_step & mask; - if pos >= 3 { - panic!("Invalid bits, self: {board:0X}, bits: {pos:0X}"); - } - } - } - - pub fn get(&self, index: usize) -> Option { - self.validate(); - debug_assert!(index < 9); - - let board = self.0; - - let shifted = board >> (index * 2); - let masked = shifted & 0b11; - - Player::from_u8(masked as u8) - } - - pub fn iter(&self) -> impl Iterator> { - let mut i = 0; - let this = self.clone(); - std::iter::from_fn(move || { - let result = (i < 9).then(|| this.get(i)); - i += 1; - result - }) - } -} - -fn result(board: Board) -> State { - fn won_row(a: Option, b: Option, c: Option) -> Option { - if a == Some(Player::X) && b == Some(Player::X) && c == Some(Player::X) { - Some(Player::X) - } else if a == Some(Player::O) && b == Some(Player::O) && c == Some(Player::O) { - Some(Player::O) - } else { - None - } - } - - macro_rules! test_row { - ($a:literal, $b:literal, $c:literal) => { - match won_row(board.get($a), board.get($b), board.get($c)) { - Some(player) => return State::Winner(player), - None => {} - } - }; - } - - if board.iter().all(|x| x.is_some()) { - return State::Draw; - } - - test_row!(0, 1, 2); - test_row!(3, 4, 5); - test_row!(6, 7, 8); - - test_row!(0, 3, 6); - test_row!(1, 4, 7); - test_row!(2, 5, 8); - - test_row!(0, 4, 8); - test_row!(2, 4, 6); - State::InProgress -} - -fn calculate_win_table(file: &mut impl Write) { - for board in 0..(2u32.pow(18)) { - let byte = match Board::new(board) { - Some(board) => { - let winner = result(board); - match winner { - State::Winner(Player::X) => 0, - State::Winner(Player::O) => 1, - State::InProgress => 2, - State::Draw => 3, - } - } - None => 0, - }; - file.write_all(&[byte]).expect("write file"); - } -} - -fn main() { - let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR"); - let win_table_path = PathBuf::from(out_dir).join("win_table"); - let mut win_table_file = File::create(win_table_path).expect("create win table file"); - - calculate_win_table(&mut win_table_file); - - win_table_file.flush().expect("flushing file"); -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b916c04 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e69de29 diff --git a/src/connect4/board.rs b/src/connect4/board.rs deleted file mode 100644 index 5c7c39f..0000000 --- a/src/connect4/board.rs +++ /dev/null @@ -1,271 +0,0 @@ -use std::{ - fmt::{Display, Write}, - ops::{Index, IndexMut}, -}; - -use crate::{Game, Player, Score, State}; - -type Position = Option; - -const WIDTH: usize = 7; -const HEIGTH: usize = 4; -const BOARD_POSITIONS: usize = WIDTH * HEIGTH; - -/// 0 1 2 3 4 5 6 -/// 7 8 9 10 11 12 13 -/// 14 15 16 17 18 19 20 -/// 21 22 23 24 25 26 27 -#[derive(Clone)] -pub struct Connect4 { - positions: [Position; BOARD_POSITIONS], -} - -impl Connect4 { - pub fn new() -> Self { - Self { - positions: [None; BOARD_POSITIONS], - } - } - - pub fn result(&self) -> State { - match self.check_board() { - State::Winner(winner) => State::Winner(winner), - State::InProgress if self.positions.iter().all(|position| position.is_some()) => { - State::Draw - } - State::InProgress => State::InProgress, - State::Draw => unreachable!("check_board cannot tell a draw"), - } - } - - fn check_board(&self) -> State { - self.check_columns()?; - self.check_rows()?; - self.check_diagonals() - } - - fn check_columns(&self) -> State { - for i in 0..WIDTH { - self.check_four(i, i + WIDTH, i + 2 * WIDTH, i + 3 * WIDTH)?; - } - - State::InProgress - } - - fn check_rows(&self) -> State { - for row_start in 0..HEIGTH { - for offset in 0..4 { - let start = (row_start * WIDTH) + offset; - self.check_four(start, start + 1, start + 2, start + 3)?; - } - } - - State::InProgress - } - - fn check_diagonals(&self) -> State { - // */* - for start in 3..WIDTH { - const DIFF: usize = WIDTH - 1; - self.check_four(start, start + DIFF, start + 2 * DIFF, start + 3 * DIFF)?; - } - - // *\* - for start in 0..4 { - const DIFF: usize = WIDTH + 1; - self.check_four(start, start + DIFF, start + 2 * DIFF, start + 3 * DIFF)?; - } - State::InProgress - } - - fn check_four(&self, a: usize, b: usize, c: usize, d: usize) -> State { - self[a] - .map(|player| { - if player == self[a] && player == self[b] && player == self[c] && player == self[d] - { - State::Winner(player) - } else { - State::InProgress - } - }) - .unwrap_or(State::InProgress) - } - - fn rate(&self, player: Player) -> Score { - #[rustfmt::skip] - const WIN_COUNT_TABLE: [i32; BOARD_POSITIONS] = [ - 3, 4, 6, 7, 6, 4, 3, - 2, 4, 6, 7, 6, 4, 2, - 2, 4, 6, 7, 6, 4, 2, - 3, 4, 6, 7, 6, 4, 2, - ]; - - let score_player = |player: Player| { - self.positions - .iter() - .enumerate() - .filter(|(_, state)| **state == Some(player)) - .map(|(pos, _)| WIN_COUNT_TABLE[pos]) - .sum::() - }; - - Score::new(score_player(player) - score_player(player.opponent())) - } -} - -impl Index for Connect4 { - type Output = Position; - - fn index(&self, index: usize) -> &Self::Output { - &self.positions[index] - } -} - -impl IndexMut for Connect4 { - fn index_mut(&mut self, index: usize) -> &mut Self::Output { - &mut self.positions[index] - } -} - -impl Game for Connect4 { - type Move = usize; - - const REASONABLE_SEARCH_DEPTH: Option = Some(7); - - fn empty() -> Self { - Self::new() - } - - fn possible_moves(&self) -> impl Iterator { - let board = self.clone(); - (0..WIDTH).filter(move |col| board[*col].is_none()) - } - - fn result(&self) -> State { - Connect4::result(&self) - } - - fn make_move(&mut self, position: Self::Move, player: Player) { - for i in 0..3 { - let prev = position + (i * WIDTH); - let next = position + ((i + 1) * WIDTH); - - if self[next].is_some() { - self[prev] = Some(player); - return; - } - } - - let bottom = position + (3 * WIDTH); - self[bottom] = Some(player); - } - - fn undo_move(&mut self, position: Self::Move) { - for i in 0..4 { - let pos = position + (i * WIDTH); - - if self[pos].is_some() { - self[pos] = None; - return; - } - } - } - - fn rate(&self, player: Player) -> Score { - Connect4::rate(&self, player) - } -} - -impl Display for Connect4 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for i in 0..HEIGTH { - for j in 0..WIDTH { - let index = (i * WIDTH) + j; - match self[index] { - Some(Player::X) => { - write!(f, "\x1B[31m X\x1B[0m ")?; - } - Some(Player::O) => { - write!(f, "\x1B[34m O\x1B[0m ")?; - } - None => { - write!(f, "\x1B[35m{index:3 }\x1B[0m ")?; - } - } - } - f.write_char('\n')?; - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use crate::{Player, State}; - - use super::Connect4; - - fn parse_board(board: &str) -> Connect4 { - let positions = board - .chars() - .filter(|char| !char.is_whitespace()) - .map(|char| match char { - 'X' => Some(Player::X), - 'O' => Some(Player::O), - '_' => None, - char => panic!("Invalid char in board: `{char}`"), - }) - .collect::>() - .try_into() - .expect(&format!( - "not enough positions provided: {}", - board.chars().filter(|c| !c.is_whitespace()).count() - )); - - Connect4 { positions } - } - - fn test(board: &str, state: State) { - let board = parse_board(board); - assert_eq!(board.result(), state); - } - - #[test] - fn draw() { - test( - " - XOOOXOX - XOOOXOX - OXXXOXO - XOOOXXX - ", - State::Draw, - ); - } - - #[test] - fn full_winner() { - test( - " - XOOOXOX - XOOOXOX - OXXXOXO - XOOOXOX - ", - State::Winner(Player::O), - ); - } - - #[test] - fn three_rows() { - test( - " - XXX_OOO - _XXX___ - X_OOO__ - OOO____ - ", - State::InProgress, - ); - } -} diff --git a/src/connect4/mod.rs b/src/connect4/mod.rs deleted file mode 100644 index 1e8a90c..0000000 --- a/src/connect4/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -use self::board::Connect4; - -pub use player::HumanPlayer; - -pub mod board; -pub mod player; diff --git a/src/connect4/player.rs b/src/connect4/player.rs deleted file mode 100644 index 4cf8b73..0000000 --- a/src/connect4/player.rs +++ /dev/null @@ -1,35 +0,0 @@ -use std::io::Write; - -use crate::{Game, GamePlayer, Player}; - -use super::Connect4; - -#[derive(Clone, Default)] -pub struct HumanPlayer; - -impl GamePlayer for HumanPlayer { - fn next_move(&mut self, board: &mut Connect4, this_player: Player) { - loop { - print!("{board}where to put the next {this_player}? (0-7): "); - - std::io::stdout().flush().unwrap(); - let mut buf = String::new(); - std::io::stdin().read_line(&mut buf).unwrap(); - - match buf.trim().parse() { - Ok(number) if number < 7 => match board[number] { - None => { - board.make_move(number, this_player); - return; - } - Some(_) => { - println!("Field is occupied already.") - } - }, - Ok(_) | Err(_) => { - println!("Invalid input.") - } - } - } - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 9dd3cfe..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,108 +0,0 @@ -#![feature( - never_type, - try_trait_v2, - return_position_impl_trait_in_trait, - let_chains -)] -#![allow(incomplete_features)] - -pub mod connect4; -mod minmax; -pub mod tic_tac_toe; - -mod player; - -use std::{fmt::Display, ops::Neg}; - -pub use self::minmax::PerfectPlayer; -pub use player::{Player, State}; - -pub trait GamePlayer { - fn next_move(&mut self, board: &mut G, this_player: Player); -} - -impl + ?Sized> GamePlayer for &mut P { - fn next_move(&mut self, board: &mut G, this_player: Player) { - P::next_move(self, board, this_player) - } -} - - -impl + ?Sized> GamePlayer for Box

{ - fn next_move(&mut self, board: &mut G, this_player: Player) { - P::next_move(self, board, this_player) - } -} - -pub trait Game: Display { - type Move: Copy; - - const REASONABLE_SEARCH_DEPTH: Option; - - fn empty() -> Self; - - fn possible_moves(&self) -> impl Iterator; - - fn result(&self) -> State; - - /// Only called if [`GameBoard::REASONABLE_SEARCH_DEPTH`] is `Some`. - fn rate(&self, player: Player) -> Score; - - fn make_move(&mut self, position: Self::Move, player: Player); - - fn undo_move(&mut self, position: Self::Move); - - fn play, B: GamePlayer>( - &mut self, - x: &mut A, - o: &mut B, - ) -> Option { - let mut current_player = Player::X; - - loop { - if current_player == Player::X { - x.next_move(self, current_player); - } else { - o.next_move(self, current_player); - } - - match self.result() { - State::Winner(player) => return Some(player), - State::Draw => { - return None; - } - State::InProgress => {} - } - - current_player = current_player.opponent(); - } - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Score(i32); - -impl Score { - const MIN: Self = Self(i32::MIN); - const LOST: Self = Self(-100); - const TIE: Self = Self(0); - const WON: Self = Self(100); - - pub fn new(int: i32) -> Self { - Self(int) - } - - fn randomize(self) -> Self { - let score = self.0 as f32; - let rand = rand::thread_rng(); - self - } -} - -impl Neg for Score { - type Output = Self; - - fn neg(self) -> Self::Output { - Self(-self.0) - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 7c38d22..0000000 --- a/src/main.rs +++ /dev/null @@ -1,156 +0,0 @@ -#![feature(let_chains)] - -use std::{fmt::Display, str::FromStr, time::SystemTime}; - -use clap::{Parser, ValueEnum}; -use minmax::{ - connect4::{self, board::Connect4}, - tic_tac_toe::{self, TicTacToe}, - Game, GamePlayer, PerfectPlayer, Player, -}; - -#[derive(Debug, Clone)] -enum PlayerConfig { - Human, - Perfect { depth: Option }, -} - -impl FromStr for PlayerConfig { - type Err = String; - - fn from_str(s: &str) -> Result { - let mut parts = s.split(":"); - let mut player = match parts - .next() - .ok_or_else(|| "No player name provided".to_owned())? - { - "human" | "h" => Self::Human, - "perfect" | "p" | "ai" | "minmax" => Self::Perfect { depth: None }, - string => { - return Err(format!( - "Invalid player: {string}. Available players: human,perfect" - )) - } - }; - - if let Some(depth) = parts.next() - && let Self::Perfect { depth: player_depth } = &mut player - { - match depth.parse() { - Ok(depth) => *player_depth = Some(depth), - Err(err) => return Err(format!("Invalid depth: {depth}. {err}")), - } - } - - Ok(player) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] -enum GameType { - TicTacToe, - Connect4, -} - -#[derive(Debug, Parser)] -#[command(author, version, about)] -struct Args { - #[arg(short, long)] - game: GameType, - #[arg(short)] - x: PlayerConfig, - #[arg(short)] - o: PlayerConfig, - #[arg(long)] - no_print_time: bool, -} - -fn main() { - let args = Args::parse(); - - match args.game { - GameType::Connect4 => { - let get_player = |player| -> Box> { - match player { - PlayerConfig::Human => Box::new(connect4::HumanPlayer), - PlayerConfig::Perfect { depth } => { - Box::new(PerfectPlayer::new(!args.no_print_time).with_max_depth(depth)) - } - } - }; - - let player_a = get_player(args.o); - let player_b = get_player(args.x); - - play_with_players(player_a, player_b); - } - GameType::TicTacToe => { - let get_player = |player| -> Box> { - match player { - PlayerConfig::Human => Box::new(tic_tac_toe::HumanPlayer), - PlayerConfig::Perfect { depth } => { - Box::new(PerfectPlayer::new(!args.no_print_time).with_max_depth(depth)) - } - } - }; - - let player_a = get_player(args.o); - let player_b = get_player(args.x); - - play_with_players(player_a, player_b); - } - } -} - -#[allow(dead_code)] -fn tic_tac_toe_stats() { - let mut results = [0, 0, 0]; - - let start = SystemTime::now(); - - for _ in 0..100 { - let result = play::, tic_tac_toe::GreedyPlayer, _>(false); - let idx = Player::as_u8(result); - results[idx as usize] += 1; - } - - println!("Winner counts"); - println!(" X: {}", results[0]); - println!(" O: {}", results[1]); - println!(" Draw: {}", results[2]); - - let time = start.elapsed().unwrap(); - - println!("Completed in {}ms", time.as_millis()); -} - -fn play_with_players, O: GamePlayer>(mut x: X, mut o: O) { - let mut board = G::empty(); - let result = board.play(&mut x, &mut o); - - print_result(result, board); -} - -fn play + Default, O: GamePlayer + Default, G: Game>( - print: bool, -) -> Option { - let mut board = G::empty(); - let result = board.play(&mut X::default(), &mut O::default()); - if print { - print_result(result, board); - } - result -} - -fn print_result(result: Option, board: impl Display) { - println!("{board}"); - - match result { - Some(winner) => { - println!("player {winner} won!"); - } - None => { - println!("a draw...") - } - } -} diff --git a/src/main/java/ch/bbw/m411/connect4/Connect4ArenaMain.java b/src/main/java/ch/bbw/m411/connect4/Connect4ArenaMain.java new file mode 100644 index 0000000..3101d8c --- /dev/null +++ b/src/main/java/ch/bbw/m411/connect4/Connect4ArenaMain.java @@ -0,0 +1,194 @@ +package ch.bbw.m411.connect4; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Scanner; + +/** + * Plays a game of Connect Four on a 4x7 board (a variation of the original 6x7 board). + * The pieces fall straight down, occupying the lowest available space within the column. + */ +public class Connect4ArenaMain { + + static final int WIDTH = 7; + + static final int HEIGHT = 4; + + static final int NOMOVE = -1; + + public static void main(String[] args) { + new Connect4ArenaMain().play(new HumanPlayer(), new GreedyPlayer()); + } + + static String toDebugString(Stone[] board) { + var sb = new StringBuilder(); + for (int r = 0; r < HEIGHT; r++) { + for (int c = 0; c < WIDTH; c++) { + var value = board[r * WIDTH + c]; + sb.append(value == null ? "." : (value == Stone.RED ? "X" : "O")); + } + sb.append("-"); + } + return sb.toString(); + } + + Connect4Player play(Connect4Player red, Connect4Player blue) { + if (red == blue) { + throw new IllegalStateException("must be different players (simply create two instances)"); + } + var board = new Stone[WIDTH * HEIGHT]; + red.initialize(Arrays.copyOf(board, board.length), Stone.RED); + blue.initialize(Arrays.copyOf(board, board.length), Stone.BLUE); + var lastMove = NOMOVE; + var currentPlayer = red; + for (int round = 0; round < board.length; round++) { + var currentColor = currentPlayer == red ? Stone.RED : Stone.BLUE; + System.out.println(HumanPlayer.toPrettyString(board) + currentColor + " to play next..."); + lastMove = currentPlayer.play(lastMove); + if (lastMove < 0 || lastMove >= WIDTH * HEIGHT) { + throw new IllegalStateException("move is outside of valid range: " + lastMove); + } + if (board[lastMove] != null) { + throw new IllegalStateException("position " + lastMove + " is already occupied @" + toDebugString(board)); + } + if (lastMove > WIDTH && board[lastMove - WIDTH] == null) { + throw new IllegalStateException("position " + lastMove + " is mid-air @" + toDebugString(board)); + } + board[lastMove] = currentColor; + if (isWinning(board, currentColor)) { + System.out.println( + HumanPlayer.toPrettyString(board) + "...and the winner is: " + currentColor + " @ " + toDebugString(board)); + return currentPlayer; + } + currentPlayer = currentPlayer == red ? blue : red; + } + System.out.println(HumanPlayer.toPrettyString(board) + "...it's a DRAW @ " + toDebugString(board)); + return null; // null implies a draw + } + + boolean isWinning(Stone[] board, Stone forColor) { + // TODO: provide an implementation + throw new IllegalStateException("Not implemented yet"); + } + + public enum Stone { + RED, BLUE; + + public Stone opponent() { + return this == RED ? BLUE : RED; + } + } + + public interface Connect4Player { + + /** + * Called before the game starts and guaranteed to only be called once per livetime of the player. + * + * @param board the starting board, usually an empty board. + * @param colorToPlay the color of this player + */ + void initialize(Stone[] board, Stone colorToPlay); + + /** + * Perform a next move, will only be called if the Game is not over yet. + * Each player has to keep an internal state of the 4x7 board, wher the 0-index is on the bottom row. + * The index-layout looks as: + *

+		 * 21 22 23 24 25 26 27
+		 * 14 15 16 17 18 19 20
+		 *  7  8  9 10 11 12 13
+		 *  0  1  2  3  4  5  6
+		 * 
+ * + * @param opponendPlayed the last index where the opponent played to (in range 0 - width*height exclusive) + * or -1 if this is the first move. + * @return an index to play to (in range 0 - width*height exclusive) + */ + int play(int opponendPlayed); + } + + /** + * An abstract helper class to keep track of a board (and whatever we or the opponent played). + */ + public abstract static class DefaultPlayer implements Connect4Player { + + Stone[] board; + + Stone myColor; + + @Override + public void initialize(Stone[] board, Stone colorToPlay) { + this.board = board; + myColor = colorToPlay; + } + + @Override + public int play(int opponendPlayed) { + if (opponendPlayed != NOMOVE) { + board[opponendPlayed] = myColor.opponent(); + } + var playTo = play(); + board[playTo] = myColor; + return playTo; + } + + /** + * Givent the current {@link #board}, find a suitable position-index to play to. + * @return the position to play to as defined by {@link Connect4Player#play(int)}. + */ + abstract int play(); + + } + + public static class HumanPlayer extends DefaultPlayer { + + static String toPrettyString(Stone[] board) { + var sb = new StringBuilder(); + for (int r = HEIGHT - 1; r >= 0; r--) { + for (int c = 0; c < WIDTH; c++) { + var index = r * WIDTH + c; + if (board[index] == null) { + if (index < WIDTH || board[index - WIDTH] != null) { + sb.append("\033[37m" + index + "\033[0m "); + if (index < 10) { + sb.append(" "); + } + } else { + sb.append("\033[37m.\033[0m "); + } + } else if (board[index] == Stone.RED) { + sb.append("\033[1;31mX\033[0m "); + } else { + sb.append("\033[1;34mO\033[0m "); + } + } + sb.append("\n"); + } + return sb.toString(); + } + @Override + int play() { + System.out.println("where to to put the next " + myColor + "?"); + var scanner = new Scanner(System.in, StandardCharsets.UTF_8); + return Integer.parseInt(scanner.nextLine()); + } + + } + + public static class GreedyPlayer extends DefaultPlayer { + + @Override + int play() { + for (int c = 0; c < WIDTH; c++) { + for (int r = 0; r < HEIGHT; r++) { + var index = r * WIDTH + c; + if (board[index] == null) { + return index; + } + } + } + throw new IllegalStateException("cannot play at all"); + } + } + +} diff --git a/src/minmax.rs b/src/minmax.rs deleted file mode 100644 index 07ed8ff..0000000 --- a/src/minmax.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::time::Instant; - -use crate::{Game, GamePlayer, Player, Score, State}; - -#[derive(Clone)] -pub struct PerfectPlayer { - best_move: Option, - max_depth: Option, - print_time: bool, -} - -impl Default for PerfectPlayer { - fn default() -> Self { - Self::new(true) - } -} - -impl PerfectPlayer { - pub fn new(print_time: bool) -> Self { - Self { - best_move: None, - max_depth: G::REASONABLE_SEARCH_DEPTH, - print_time, - } - } - - pub fn with_max_depth(mut self, max_depth: Option) -> Self { - self.max_depth = max_depth; - self - } - - fn minmax(&mut self, board: &mut G, player: Player, depth: usize) -> Score { - if let Some(max_depth) = self.max_depth && depth >= max_depth { - return board.rate(player); - } - - match board.result() { - State::Winner(winner) => { - if winner == player { - Score::WON - } else { - Score::LOST - } - } - State::Draw => Score::TIE, - State::InProgress => { - let mut max_value = Score::MIN; - - for pos in board.possible_moves() { - board.make_move(pos, player); - let value = -self.minmax(board, player.opponent(), depth + 1); - - board.undo_move(pos); - - if value > max_value { - max_value = value; - if depth == 0 { - self.best_move = Some(pos); - } - } - } - - max_value - } - } - } -} - -impl GamePlayer for PerfectPlayer { - fn next_move(&mut self, board: &mut G, this_player: Player) { - let start = Instant::now(); - self.best_move = None; - self.minmax(board, this_player, 0); - - board.make_move(self.best_move.expect("could not make move"), this_player); - - if self.print_time { - let duration = start.elapsed(); - println!("Move took {duration:?}"); - } - } -} diff --git a/src/player.rs b/src/player.rs deleted file mode 100644 index 8f36caf..0000000 --- a/src/player.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::{ - fmt::Display, - ops::{ControlFlow, Try}, -}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Player { - X, - O, -} - -impl PartialEq> for Player { - fn eq(&self, other: &Option) -> bool { - match (self, other) { - (Player::X, Some(Player::X)) => true, - (Player::O, Some(Player::O)) => true, - _ => false, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum State { - Winner(Player), - InProgress, - Draw, -} - -impl Player { - pub fn opponent(self) -> Self { - match self { - Self::X => Self::O, - Self::O => Self::X, - } - } - - pub fn from_u8(num: u8) -> Result, ()> { - Ok(match num { - 0 => Some(Player::X), - 1 => Some(Player::O), - 2 => None, - _ => return Err(()), - }) - } - - pub fn as_u8(this: Option) -> u8 { - match this { - Some(Player::X) => 0, - Some(Player::O) => 1, - None => 2, - } - } -} - -impl Display for Player { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - Self::X => "X", - Self::O => "O", - }) - } -} - -impl std::ops::FromResidual for State { - fn from_residual(residual: ::Residual) -> Self { - residual - } -} - -impl Try for State { - // InProgress - type Output = Self; - - type Residual = Self; - - fn from_output(_: Self::Output) -> Self { - Self::InProgress - } - - fn branch(self) -> ControlFlow { - match self { - Self::InProgress => ControlFlow::Continue(self), - Self::Winner(_) | Self::Draw => ControlFlow::Break(self), - } - } -} diff --git a/src/test/java/ch/bbw/m411/connect4/Connect4MainTest.java b/src/test/java/ch/bbw/m411/connect4/Connect4MainTest.java new file mode 100644 index 0000000..b0c21c5 --- /dev/null +++ b/src/test/java/ch/bbw/m411/connect4/Connect4MainTest.java @@ -0,0 +1,64 @@ +package ch.bbw.m411.connect4; + +import java.util.List; + +import org.assertj.core.api.AbstractBooleanAssert; +import org.assertj.core.api.WithAssertions; +import org.junit.jupiter.api.Test; + +class Connect4MainTest implements WithAssertions { + + protected Connect4ArenaMain newInstance() { + return new Connect4ArenaMain(); + } + + Connect4ArenaMain.Stone[] fromString(String boardStr) { + var board = boardStr.codePoints() + .map(Character::toLowerCase) + .filter(x -> List.of('x', 'o', '.') + .contains((char) x)) + .mapToObj(x -> x == 'x' ? Connect4ArenaMain.Stone.RED : (x == 'o' ? Connect4ArenaMain.Stone.BLUE : null)) + .toArray(Connect4ArenaMain.Stone[]::new); + assertThat(board).hasSize(Connect4ArenaMain.WIDTH * Connect4ArenaMain.HEIGHT); + return board; + } + + AbstractBooleanAssert assertThatXWin(String boardStr) { + var board = fromString(boardStr); + return assertThat(newInstance().isWinning(board, Connect4ArenaMain.Stone.RED)).as(Connect4ArenaMain.toDebugString(board)); + } + + @Test + void isWin() { + assertThatXWin("xxxx... ....... ....... .......").isTrue(); + assertThatXWin(".xxxx.. ....... ....... .......").isTrue(); + assertThatXWin("..xxxx. ....... ....... .......").isTrue(); + assertThatXWin("...xxxx ....... ....... .......").isTrue(); + assertThatXWin("...x... ...x... ...x... ...x...").isTrue(); + assertThatXWin("......x ......x ......x ......x").isTrue(); + assertThatXWin("xooo... .xoo... ..xo... ...x...").isTrue(); + assertThatXWin(".ooxo.. .oxoo.. .xxxx.. .......").isTrue(); + assertThatXWin(".ooxo.x .oxoo.. .ooxx.. .xxxx..").isTrue(); + assertThatXWin("oooo... xxxx... ....... .......").isTrue(); + } + + @Test + void noWin() { + assertThatXWin("....... ....... ....... .......").isFalse(); + assertThatXWin("xxx.xx. ....... ....... .......").isFalse(); + assertThatXWin("xxx.xxx xxx.xxx xxx.xxx .......").isFalse(); + assertThatXWin("xx.x.xx xx.x.xx xx.x.xx .......").isFalse(); + assertThatXWin("ooo.ooo xxx.xxx xxx.xxx xxx.xxx").isFalse(); + assertThatXWin("oo.o.oo xx.x.xx xx.x.xx xx.x.xx").isFalse(); + assertThatXWin("oooo... ....... ....... .......").isFalse(); + assertThatXWin("xxx.xx. xxx.xx. xxx.... o......").isFalse(); + assertThatXWin("xxxo... x.x.... x.o.... o.x....").isFalse(); + } + + @Test + void inAGreedyBattleTheFirstPlayerWillWin() { + var red = new Connect4ArenaMain.GreedyPlayer(); + var blue = new Connect4ArenaMain.GreedyPlayer(); + assertThat(newInstance().play(red, blue)).isSameAs(red); + } +} diff --git a/src/tic_tac_toe/board.rs b/src/tic_tac_toe/board.rs deleted file mode 100644 index 8857494..0000000 --- a/src/tic_tac_toe/board.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::fmt::{Display, Write}; - -use crate::{Game, Player, Score, State}; - -#[derive(Clone)] -pub struct TicTacToe(u32); - -impl TicTacToe { - pub fn empty() -> Self { - // A = 1010 - // 18 bits - 9 * 2 bits - 4.5 nibbles - Self(0x0002AAAA) - } - - fn validate(&self) { - if cfg!(debug_assertions) { - let board = self.0; - for i in 0..16 { - let next_step = board >> (i * 2); - let mask = 0b11; - let pos = next_step & mask; - if pos >= 3 { - panic!("Invalid bits, self: {board:0X}, bits: {pos:0X}"); - } - } - } - } - - pub fn get(&self, index: usize) -> Option { - debug_assert!(index < 9); - - let board = self.0; - - let shifted = board >> (index * 2); - let masked = shifted & 0b11; - - // SAFETY: So uh, this is a bit unlucky. - // You see, there are two entire bits of information at our disposal for each position. - // This is really bad. We only have three valid states. So we need to do _something_ if it's invalid. - // We just hope that it will never be invalid which it really shouldn't be and also have a debug assertion - // here to make sure that it really is valid and then if it's not invalid we just mov it out and are happy. - self.validate(); - unsafe { Player::from_u8(masked as u8).unwrap_unchecked() } - } - - pub fn set(&mut self, index: usize, value: Option) { - debug_assert!(index < 9); - self.validate(); - - let value = Player::as_u8(value) as u32; - - let value = value << (index * 2); - let mask = 0b11 << (index * 2); - - let current_masked_off_new = self.0 & !mask; - let result = value | current_masked_off_new; - self.0 = result; - - self.validate(); - } - - pub fn iter(&self) -> impl Iterator> { - let mut i = 0; - let this = self.clone(); - std::iter::from_fn(move || { - let result = (i < 9).then(|| this.get(i)); - i += 1; - result - }) - } - - pub fn result(&self) -> State { - win_table::result(self) - } -} - -mod win_table { - use super::TicTacToe; - use crate::{Player, State}; - - const WIN_TABLE_SIZE: usize = 2usize.pow(2 * 9); - static WIN_TABLE: &[u8; WIN_TABLE_SIZE] = - include_bytes!(concat!(env!("OUT_DIR"), "/win_table")); - - pub fn result(board: &TicTacToe) -> State { - match WIN_TABLE[board.0 as usize] { - 0 => State::Winner(Player::X), - 1 => State::Winner(Player::X), - 2 => State::InProgress, - 3 => State::Draw, - n => panic!("Invalid value {n} in table"), - } - } -} - -impl Display for TicTacToe { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for i in 0..3 { - for j in 0..3 { - let index = i * 3 + j; - match self.get(index) { - Some(player) => { - write!(f, "\x1B[33m{player}\x1B[0m ")?; - } - None => { - write!(f, "\x1B[35m{index}\x1B[0m ")?; - } - } - } - f.write_char('\n')?; - } - Ok(()) - } -} - -impl Game for TicTacToe { - type Move = usize; - - const REASONABLE_SEARCH_DEPTH: Option = None; - - fn empty() -> Self { - Self::empty() - } - - fn possible_moves(&self) -> impl Iterator { - debug_assert!( - !self.iter().all(|x| x.is_some()), - "the board is full but state is InProgress" - ); - - self.iter() - .enumerate() - .filter(|(_, position)| position.is_none()) - .map(|(pos, _)| pos) - } - - fn result(&self) -> State { - TicTacToe::result(self) - } - - fn rate(&self, _: Player) -> Score { - unimplemented!("we always finish the board") - } - - fn make_move(&mut self, position: Self::Move, player: Player) { - self.set(position, Some(player)); - } - - fn undo_move(&mut self, position: Self::Move) { - self.set(position, None); - } -} - -#[cfg(test)] -mod tests { - use super::{Player, TicTacToe}; - - #[test] - fn board_field() { - let mut board = TicTacToe::empty(); - board.set(0, None); - board.set(8, Some(Player::X)); - board.set(4, Some(Player::O)); - board.set(5, Some(Player::X)); - - let expected = [ - None, - None, - None, - None, - Some(Player::O), - Some(Player::X), - None, - None, - Some(Player::X), - ]; - - board - .iter() - .zip(expected.into_iter()) - .enumerate() - .for_each(|(idx, (actual, expected))| assert_eq!(actual, expected, "Position {idx}")); - } -} diff --git a/src/tic_tac_toe/game.rs b/src/tic_tac_toe/game.rs deleted file mode 100644 index 6faeb88..0000000 --- a/src/tic_tac_toe/game.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::{GamePlayer, Player, State}; - -use super::TicTacToe; - -impl TicTacToe { - pub fn play, B: GamePlayer>( - &mut self, - x: &mut A, - o: &mut B, - ) -> Option { - let mut current_player = Player::X; - - for _ in 0..9 { - if current_player == Player::X { - x.next_move(self, current_player); - } else { - o.next_move(self, current_player); - } - - match self.result() { - State::Winner(player) => return Some(player), - State::Draw => { - return None; - } - State::InProgress => {} - } - - current_player = current_player.opponent(); - } - - None - } -} diff --git a/src/tic_tac_toe/mod.rs b/src/tic_tac_toe/mod.rs deleted file mode 100644 index e8325b9..0000000 --- a/src/tic_tac_toe/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -mod board; -mod game; -mod player; - -pub use {board::TicTacToe, player::*}; - -#[cfg(test)] -mod tests { - use crate::{minmax::PerfectPlayer, tic_tac_toe::board::TicTacToe, GamePlayer, Player}; - - use super::player::{GreedyPlayer, RandomPlayer}; - - fn assert_win_ratio, O: GamePlayer>( - runs: u64, - x_win_ratio: f64, - x: impl Fn() -> X, - o: impl Fn() -> O, - ) { - let mut results = [0u64, 0, 0]; - - for _ in 0..runs { - let result = TicTacToe::empty().play::(&mut x(), &mut o()); - let idx = Player::as_u8(result); - results[idx as usize] += 1; - } - - let total = results.iter().copied().sum::(); - - let ratio = (total as f64) / (results[0] as f64); - println!("{ratio} >= {x_win_ratio}"); - assert!(ratio >= x_win_ratio); - } - - #[test] - fn perfect_always_beats_greedy() { - assert_win_ratio(20, 1.0, || PerfectPlayer::new(false), || GreedyPlayer); - } - - #[test] - fn perfect_beats_random() { - assert_win_ratio(10, 0.95, || PerfectPlayer::new(false), || RandomPlayer); - } -} diff --git a/src/tic_tac_toe/player.rs b/src/tic_tac_toe/player.rs deleted file mode 100644 index 508c3d1..0000000 --- a/src/tic_tac_toe/player.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::io::Write; - -use rand::Rng; - -use crate::{GamePlayer, Player}; - -use super::TicTacToe; - -#[derive(Clone, Default)] -pub struct GreedyPlayer; - -impl GamePlayer for GreedyPlayer { - fn next_move(&mut self, board: &mut TicTacToe, this_player: Player) { - let first_free = board.iter().position(|p| p.is_none()).unwrap(); - board.set(first_free, Some(this_player)); - } -} - -#[derive(Clone, Default)] -pub struct HumanPlayer; - -impl GamePlayer for HumanPlayer { - fn next_move(&mut self, board: &mut TicTacToe, this_player: Player) { - loop { - print!("{board}where to put the next {this_player}? (0-8): "); - - std::io::stdout().flush().unwrap(); - let mut buf = String::new(); - std::io::stdin().read_line(&mut buf).unwrap(); - - match buf.trim().parse() { - Ok(number) if number < 9 => match board.get(number) { - None => { - board.set(number, Some(this_player)); - return; - } - Some(_) => { - println!("Field is occupied already.") - } - }, - Ok(_) | Err(_) => { - println!("Invalid input.") - } - } - } - } -} - -#[derive(Clone, Default)] -pub struct RandomPlayer; - -impl GamePlayer for RandomPlayer { - fn next_move(&mut self, board: &mut TicTacToe, this_player: Player) { - loop { - let next = rand::thread_rng().gen_range(0..9); - match board.get(next) { - Some(_) => {} - None => { - board.set(next, Some(this_player)); - return; - } - } - } - } -}