This commit is contained in:
nora 2023-01-14 16:03:33 +01:00
parent 3d90a3ea0a
commit d415bb7ae9
6 changed files with 84 additions and 86 deletions

16
Cargo.lock generated
View file

@ -203,6 +203,14 @@ dependencies = [
"rand", "rand",
] ]
[[package]]
name = "minmax_wrapper"
version = "0.1.0"
dependencies = [
"jni",
"minmax",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.17.0" version = "1.17.0"
@ -293,14 +301,6 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "rs-wrapper"
version = "0.1.0"
dependencies = [
"jni",
"minmax",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.36.6" version = "0.36.6"

21
build_and_run.sh Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env bash
if ! which rustc > /dev/null;
then
echo "ERROR: Rust must be installed: https://www.rust-lang.org/tools/install"
exit 1
fi
echo "${BASH_SOURCE[0]}"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
echo "Building Rust library..."
cargo build --release "--manifest-path=$SCRIPT_DIR/Cargo.toml"
export LD_LIBRARY_PATH="$SCRIPT_DIR/target/release"
echo "Setting LD_LIBRARY_PATH to $LD_LIBRARY_PATH"
echo "Running Java tests. If this fails, there's something wrong :/"
cd "$SCRIPT_DIR/minmax-java"
./gradlew build

View file

@ -67,65 +67,7 @@ public class Connect4ArenaMain {
} }
boolean isWinning(Stone[] board, Stone forColor) { boolean isWinning(Stone[] board, Stone forColor) {
var increment = 0; return RustPlayer.isWinning(board, forColor);
for (int i = 27; i >= 0; i--) {
if (forColor != board[i]) {
increment = 0;
continue;
}
// Horizontal reset
if (i == 20 || i == 13 || i == 6) {
increment = 0;
}
increment++;
// Horizontal
if (increment == 4) {
return true;
}
if (i > 20) {
// Vertical
var vertical = true;
for (int y = i - 7; y >= 0; y -= 7) {
if (board[y] != forColor) {
vertical = false;
break;
}
}
if (vertical) {
return true;
}
// Diagonally
if (i <= 24) {
var diagonal = true;
for (int y = i; y >= 0; y -= 7 - 1) {
if (board[y] != forColor) {
diagonal = false;
break;
}
}
if (diagonal) {
return true;
}
}
if (i >= 24) {
var diagonal = true;
for (int y = i; y >= 0; y -= 7 + 1) {
if (board[y] != forColor) {
diagonal = false;
break;
}
}
if (diagonal) {
return true;
}
}
}
}
return false;
} }
public enum Stone { public enum Stone {

View file

@ -2,12 +2,13 @@ package ch.bbw.m411.connect4;
public class RustPlayer extends Connect4ArenaMain.DefaultPlayer { public class RustPlayer extends Connect4ArenaMain.DefaultPlayer {
private static native int rustPlay(byte player, byte[] board); private static native int rustPlay(byte player, byte[] board);
private static native int rustBoardWinner(byte[] board);
static { static {
// This actually loads the shared object that we'll be creating. // This actually loads the shared object that we'll be creating.
// The actual location of the .so or .dll may differ based on your // The actual location of the .so or .dll may differ based on your
// platform. // platform.
System.loadLibrary("rs_wrapper"); System.loadLibrary("minmax_wrapper");
} }
static byte[] encodeBoard(Connect4ArenaMain.Stone[] board) { static byte[] encodeBoard(Connect4ArenaMain.Stone[] board) {
@ -27,6 +28,16 @@ public class RustPlayer extends Connect4ArenaMain.DefaultPlayer {
return boardBuf; return boardBuf;
} }
public static boolean isWinning(Connect4ArenaMain.Stone[] board, Connect4ArenaMain.Stone forColor) {
byte[] boardBuf = RustPlayer.encodeBoard(board);
byte expectedPlayer = switch (forColor) {
case BLUE -> 1;
case RED -> 0;
};
int winner = RustPlayer.rustBoardWinner(boardBuf);
return winner == expectedPlayer;
}
@Override @Override
protected int play() { protected int play() {
byte player = switch (this.myColor) { byte player = switch (this.myColor) {

View file

@ -1,5 +1,5 @@
[package] [package]
name = "rs-wrapper" name = "minmax_wrapper"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"

View file

@ -2,7 +2,7 @@ use jni::objects::{JClass, JObject, ReleaseMode};
use jni::sys::{jbyte, jint}; use jni::sys::{jbyte, jint};
use jni::JNIEnv; use jni::JNIEnv;
use minmax::{connect4::board::Connect4, GamePlayer}; use minmax::{connect4::board::Connect4, GamePlayer};
use minmax::{Game, PerfectPlayer, Player}; use minmax::{Game, PerfectPlayer, Player, State};
/// We need to map the board. /// We need to map the board.
/// Rust: /// Rust:
@ -28,7 +28,16 @@ fn map_idx(i: usize) -> usize {
} }
} }
fn crate_board(java_board: &[i8]) -> Connect4 { unsafe fn create_board(env: JNIEnv<'_>, board: JObject<'_>) -> Connect4 {
let board_size = env.get_array_length(board.into_raw()).unwrap();
assert_eq!(board_size, 28);
let byte_array = env
.get_byte_array_elements(board.into_raw(), ReleaseMode::NoCopyBack)
.unwrap();
let java_board = unsafe { std::slice::from_raw_parts(byte_array.as_ptr().cast::<u8>(), 28) };
let mut board = Connect4::new(); let mut board = Connect4::new();
for i in 0..28 { for i in 0..28 {
@ -37,7 +46,9 @@ fn crate_board(java_board: &[i8]) -> Connect4 {
0 => Some(Player::X), 0 => Some(Player::X),
1 => Some(Player::O), 1 => Some(Player::O),
2 => None, 2 => None,
_ => unreachable!(), _ => unreachable!(
"Java code passed an invalid value as the byte of the board: {java_int}"
),
}; };
let rust_index = map_idx(i); let rust_index = map_idx(i);
@ -51,17 +62,8 @@ fn crate_board(java_board: &[i8]) -> Connect4 {
// 0 -> RED -> X // 0 -> RED -> X
// 1 -> BLUE -> O // 1 -> BLUE -> O
// 2 -> empty // 2 -> empty
pub fn wrap_player(env: JNIEnv<'_>, current_player: i8, board: JObject<'_>) -> i32 { pub fn play_move(env: JNIEnv<'_>, current_player: i8, board: JObject<'_>) -> i32 {
let board_size = env.get_array_length(board.into_raw()).unwrap(); let mut board = unsafe { create_board(env, board) };
assert_eq!(board_size, 28);
let byte_array = env
.get_byte_array_elements(board.into_raw(), ReleaseMode::NoCopyBack)
.unwrap();
let slice = unsafe { std::slice::from_raw_parts(byte_array.as_ptr() as *const _, 28) };
let mut board = crate_board(slice);
let mut player = PerfectPlayer::new(false); let mut player = PerfectPlayer::new(false);
@ -82,8 +84,18 @@ pub fn wrap_player(env: JNIEnv<'_>, current_player: i8, board: JObject<'_>) -> i
java_idx java_idx
} }
// This keeps Rust from "mangling" the name and making it unique for this fn board_winner(env: JNIEnv<'_>, board: JObject<'_>) -> i32 {
// crate. let board = unsafe { create_board(env, board) };
let state = board.result();
match state {
State::Draw => 2,
State::InProgress => 2,
State::Winner(Player::X) => 0,
State::Winner(Player::O) => 1,
}
}
#[no_mangle] #[no_mangle]
pub extern "system" fn Java_ch_bbw_m411_connect4_RustPlayer_rustPlay( pub extern "system" fn Java_ch_bbw_m411_connect4_RustPlayer_rustPlay(
env: JNIEnv<'_>, env: JNIEnv<'_>,
@ -94,6 +106,18 @@ pub extern "system" fn Java_ch_bbw_m411_connect4_RustPlayer_rustPlay(
player: jbyte, player: jbyte,
board: JObject<'_>, board: JObject<'_>,
) -> jint { ) -> jint {
std::panic::catch_unwind(|| wrap_player(env, player, board)) std::panic::catch_unwind(|| play_move(env, player, board))
.unwrap_or_else(|_| std::process::abort()) .unwrap_or_else(|_| std::process::abort())
} }
#[no_mangle]
pub extern "system" fn Java_ch_bbw_m411_connect4_RustPlayer_rustBoardWinner(
env: JNIEnv<'_>,
// This is the class that owns our static method. It's not going to be used,
// but still must be present to match the expected signature of a static
// native method.
_: JClass<'_>,
board: JObject<'_>,
) -> jint {
std::panic::catch_unwind(|| board_winner(env, board)).unwrap_or_else(|_| std::process::abort())
}