mirror of
https://github.com/2OOP/pism.git
synced 2026-02-04 10:54:51 +00:00
Compare commits
3 Commits
e5ea838430
...
fb32bc6f8e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb32bc6f8e | ||
|
|
4c8bd89a35 | ||
|
|
f7b24edf1e |
@@ -14,9 +14,11 @@ import org.toop.app.widget.complex.ViewWidget;
|
|||||||
import org.toop.app.widget.popup.ErrorPopup;
|
import org.toop.app.widget.popup.ErrorPopup;
|
||||||
import org.toop.app.widget.tutorial.*;
|
import org.toop.app.widget.tutorial.*;
|
||||||
import org.toop.game.players.ai.MiniMaxAI;
|
import org.toop.game.players.ai.MiniMaxAI;
|
||||||
|
import org.toop.game.players.ai.RandomAI;
|
||||||
import org.toop.game.players.ai.mcts.MCTSAI1;
|
import org.toop.game.players.ai.mcts.MCTSAI1;
|
||||||
import org.toop.game.players.ai.mcts.MCTSAI3;
|
import org.toop.game.players.ai.mcts.MCTSAI3;
|
||||||
import org.toop.game.players.ai.mcts.MCTSAI4;
|
import org.toop.game.players.ai.mcts.MCTSAI4;
|
||||||
|
import org.toop.game.players.ai.mcts.OPMCTSAI;
|
||||||
import org.toop.local.AppContext;
|
import org.toop.local.AppContext;
|
||||||
|
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
@@ -82,7 +84,7 @@ public class LocalMultiplayerView extends ViewWidget {
|
|||||||
if (information.players[0].isHuman) {
|
if (information.players[0].isHuman) {
|
||||||
players[0] = new LocalPlayer(information.players[0].name);
|
players[0] = new LocalPlayer(information.players[0].name);
|
||||||
} else {
|
} else {
|
||||||
// players[0] = new ArtificialPlayer(new RandomAI<BitboardReversi>(), "Random AI");
|
// players[0] = new ArtificialPlayer(new RandomAI(), "Random AI");
|
||||||
players[0] = new ArtificialPlayer(new MCTSAI4(500, 4), "MCTS V4 AI");
|
players[0] = new ArtificialPlayer(new MCTSAI4(500, 4), "MCTS V4 AI");
|
||||||
}
|
}
|
||||||
if (information.players[1].isHuman) {
|
if (information.players[1].isHuman) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import org.toop.framework.gameFramework.model.game.TurnBasedGame;
|
|||||||
import org.toop.framework.gameFramework.model.player.Player;
|
import org.toop.framework.gameFramework.model.player.Player;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
|
|
||||||
// There is AI performance to be gained by getting rid of non-primitives and thus speeding up deepCopy
|
// There is AI performance to be gained by getting rid of non-primitives and thus speeding up deepCopy
|
||||||
public abstract class BitboardGame implements TurnBasedGame {
|
public abstract class BitboardGame implements TurnBasedGame {
|
||||||
@@ -18,8 +17,8 @@ public abstract class BitboardGame implements TurnBasedGame {
|
|||||||
private Player[] players;
|
private Player[] players;
|
||||||
|
|
||||||
// long is 64 bits. Every game has a limit of 64 cells maximum.
|
// long is 64 bits. Every game has a limit of 64 cells maximum.
|
||||||
private final long[] playerBitboard;
|
protected final long[] playerBitboard;
|
||||||
private int currentTurn = 0;
|
protected int currentTurn = 0;
|
||||||
private final int playerCount;
|
private final int playerCount;
|
||||||
|
|
||||||
public BitboardGame(int columnSize, int rowSize, int playerCount) {
|
public BitboardGame(int columnSize, int rowSize, int playerCount) {
|
||||||
@@ -75,6 +74,8 @@ public abstract class BitboardGame implements TurnBasedGame {
|
|||||||
return playerBitboard.length;
|
return playerBitboard.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getAmountOfTurns() { return currentTurn; }
|
||||||
|
|
||||||
public int getCurrentTurn() {
|
public int getCurrentTurn() {
|
||||||
return getCurrentPlayerIndex();
|
return getCurrentPlayerIndex();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.toop.framework.game.games.reversi;
|
|||||||
import org.toop.framework.game.BitboardGame;
|
import org.toop.framework.game.BitboardGame;
|
||||||
import org.toop.framework.gameFramework.GameState;
|
import org.toop.framework.gameFramework.GameState;
|
||||||
import org.toop.framework.gameFramework.model.game.PlayResult;
|
import org.toop.framework.gameFramework.model.game.PlayResult;
|
||||||
|
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
|
||||||
import org.toop.framework.gameFramework.model.player.Player;
|
import org.toop.framework.gameFramework.model.player.Player;
|
||||||
import org.toop.framework.game.BitboardGame;
|
import org.toop.framework.game.BitboardGame;
|
||||||
|
|
||||||
@@ -368,4 +369,11 @@ public class BitboardReversi extends BitboardGame {
|
|||||||
|
|
||||||
return bestMove;
|
return bestMove;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFrom(long player1, long player2, int turn) {
|
||||||
|
this.playerBitboard[0] = player1;
|
||||||
|
this.playerBitboard[1] = player2;
|
||||||
|
this.currentTurn = turn;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package org.toop.framework.game.games.tictactoe;
|
|||||||
|
|
||||||
import org.toop.framework.gameFramework.GameState;
|
import org.toop.framework.gameFramework.GameState;
|
||||||
import org.toop.framework.gameFramework.model.game.PlayResult;
|
import org.toop.framework.gameFramework.model.game.PlayResult;
|
||||||
|
import org.toop.framework.gameFramework.model.game.TurnBasedGame;
|
||||||
import org.toop.framework.gameFramework.model.player.Player;
|
import org.toop.framework.gameFramework.model.player.Player;
|
||||||
import org.toop.framework.game.BitboardGame;
|
import org.toop.framework.game.BitboardGame;
|
||||||
|
|
||||||
@@ -120,4 +121,8 @@ public class BitboardTicTacToe extends BitboardGame {
|
|||||||
public long heuristicMove(long legalMoves) {
|
public long heuristicMove(long legalMoves) {
|
||||||
return legalMoves;
|
return legalMoves;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setFrom(long player1, long player2, int turn) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,4 +16,6 @@ public interface TurnBasedGame extends DeepCopyable<TurnBasedGame> {
|
|||||||
|
|
||||||
float rateMove(long move);
|
float rateMove(long move);
|
||||||
long heuristicMove(long legalMoves);
|
long heuristicMove(long legalMoves);
|
||||||
|
|
||||||
|
void setFrom(long player1, long player2, int turn);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,8 +107,7 @@ public abstract class MCTSAI extends AbstractAI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected Node selection(Node root) {
|
protected Node selection(Node root) {
|
||||||
// while (Float.isNaN(root.solved) && root.isFullyExpanded() && !root.state.isTerminal()) {
|
while (Float.isNaN(root.solved) && root.isFullyExpanded() && !root.state.isTerminal()) {
|
||||||
while (root.isFullyExpanded() && !root.state.isTerminal()) {
|
|
||||||
root = root.bestUCTChild();
|
root = root.bestUCTChild();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ public class MCTSAI1 extends MCTSAI {
|
|||||||
|
|
||||||
final long endTime = System.nanoTime() + milliseconds * 1_000_000L;
|
final long endTime = System.nanoTime() + milliseconds * 1_000_000L;
|
||||||
|
|
||||||
// while (Float.isNaN(root.solved) && System.nanoTime() < endTime) {
|
while (Float.isNaN(root.solved) && System.nanoTime() < endTime) {
|
||||||
while (System.nanoTime() < endTime) {
|
|
||||||
Node leaf = selection(root);
|
Node leaf = selection(root);
|
||||||
leaf = expansion(leaf);
|
leaf = expansion(leaf);
|
||||||
final float value = simulation(leaf);
|
final float value = simulation(leaf);
|
||||||
@@ -32,6 +31,7 @@ public class MCTSAI1 extends MCTSAI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastIterations = root.visits;
|
lastIterations = root.visits;
|
||||||
|
IO.println("V1: " + lastIterations);
|
||||||
|
|
||||||
final Node mostVisitedChild = mostVisitedChild(root);
|
final Node mostVisitedChild = mostVisitedChild(root);
|
||||||
return mostVisitedChild.move;
|
return mostVisitedChild.move;
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ public class MCTSAI2 extends MCTSAI {
|
|||||||
|
|
||||||
final long endTime = System.nanoTime() + milliseconds * 1_000_000L;
|
final long endTime = System.nanoTime() + milliseconds * 1_000_000L;
|
||||||
|
|
||||||
// while (Float.isNaN(root.solved) && System.nanoTime() < endTime) {
|
while (Float.isNaN(root.solved) && System.nanoTime() < endTime) {
|
||||||
while (System.nanoTime() < endTime) {
|
|
||||||
Node leaf = selection(root);
|
Node leaf = selection(root);
|
||||||
leaf = expansion(leaf);
|
leaf = expansion(leaf);
|
||||||
final float value = simulation(leaf);
|
final float value = simulation(leaf);
|
||||||
@@ -38,6 +37,7 @@ public class MCTSAI2 extends MCTSAI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastIterations = root.visits;
|
lastIterations = root.visits;
|
||||||
|
IO.println("V2: " + lastIterations);
|
||||||
|
|
||||||
final Node mostVisitedChild = mostVisitedChild(root);
|
final Node mostVisitedChild = mostVisitedChild(root);
|
||||||
final long move = mostVisitedChild.move;
|
final long move = mostVisitedChild.move;
|
||||||
|
|||||||
@@ -41,8 +41,7 @@ public class MCTSAI3 extends MCTSAI {
|
|||||||
tasks.add(() -> {
|
tasks.add(() -> {
|
||||||
final Node localRoot = new Node(game.deepCopy());
|
final Node localRoot = new Node(game.deepCopy());
|
||||||
|
|
||||||
// while (Float.isNaN(localRoot.solved) && System.nanoTime() < endTime) {
|
while (Float.isNaN(localRoot.solved) && System.nanoTime() < endTime) {
|
||||||
while (System.nanoTime() < endTime) {
|
|
||||||
Node leaf = selection(localRoot);
|
Node leaf = selection(localRoot);
|
||||||
leaf = expansion(leaf);
|
leaf = expansion(leaf);
|
||||||
final float value = simulation(leaf);
|
final float value = simulation(leaf);
|
||||||
@@ -79,6 +78,7 @@ public class MCTSAI3 extends MCTSAI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastIterations = root.visits;
|
lastIterations = root.visits;
|
||||||
|
IO.println("V3: " + lastIterations);
|
||||||
|
|
||||||
final Node mostVisitedChild = mostVisitedChild(root);
|
final Node mostVisitedChild = mostVisitedChild(root);
|
||||||
return mostVisitedChild.move;
|
return mostVisitedChild.move;
|
||||||
|
|||||||
@@ -50,8 +50,7 @@ public class MCTSAI4 extends MCTSAI {
|
|||||||
tasks.add(() -> {
|
tasks.add(() -> {
|
||||||
final Node localRoot = threadRoots[threadIndex];
|
final Node localRoot = threadRoots[threadIndex];
|
||||||
|
|
||||||
// while (Float.isNaN(localRoot.solved) && System.nanoTime() < endTime) {
|
while (Float.isNaN(localRoot.solved) && System.nanoTime() < endTime) {
|
||||||
while (System.nanoTime() < endTime) {
|
|
||||||
Node leaf = selection(localRoot);
|
Node leaf = selection(localRoot);
|
||||||
leaf = expansion(leaf);
|
leaf = expansion(leaf);
|
||||||
final float value = simulation(leaf);
|
final float value = simulation(leaf);
|
||||||
@@ -88,6 +87,7 @@ public class MCTSAI4 extends MCTSAI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastIterations = root.visits;
|
lastIterations = root.visits;
|
||||||
|
IO.println("V4: " + lastIterations);
|
||||||
|
|
||||||
final Node mostVisitedChild = mostVisitedChild(root);
|
final Node mostVisitedChild = mostVisitedChild(root);
|
||||||
final long move = mostVisitedChild.move;
|
final long move = mostVisitedChild.move;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import org.junit.jupiter.api.*;
|
|||||||
import org.toop.framework.game.games.reversi.BitboardReversi;
|
import org.toop.framework.game.games.reversi.BitboardReversi;
|
||||||
import org.toop.framework.game.players.ArtificialPlayer;
|
import org.toop.framework.game.players.ArtificialPlayer;
|
||||||
import org.toop.game.players.ai.MCTSAI;
|
import org.toop.game.players.ai.MCTSAI;
|
||||||
import org.toop.game.players.ai.RandomAI;
|
|
||||||
import org.toop.game.players.ai.mcts.MCTSAI1;
|
import org.toop.game.players.ai.mcts.MCTSAI1;
|
||||||
import org.toop.game.players.ai.mcts.MCTSAI2;
|
import org.toop.game.players.ai.mcts.MCTSAI2;
|
||||||
import org.toop.game.players.ai.mcts.MCTSAI3;
|
import org.toop.game.players.ai.mcts.MCTSAI3;
|
||||||
@@ -21,15 +20,16 @@ public class AITest {
|
|||||||
|
|
||||||
private static List<Matchup> matchupList = new ArrayList<Matchup>();
|
private static List<Matchup> matchupList = new ArrayList<Matchup>();
|
||||||
private static List<AIData> dataList = new ArrayList<AIData>();
|
private static List<AIData> dataList = new ArrayList<AIData>();
|
||||||
|
private static List<GameData> gameDataList = new ArrayList<GameData>();
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
public static void init() {
|
public static void init() {
|
||||||
|
|
||||||
var versions = new ArtificialPlayer[4];
|
var versions = new ArtificialPlayer[2];
|
||||||
versions[0] = new ArtificialPlayer(new MCTSAI1(100), "MCTS V1");
|
// versions[0] = new ArtificialPlayer(new MCTSAI1(10), "MCTS V1");
|
||||||
versions[1] = new ArtificialPlayer(new MCTSAI2(100), "MCTS V2");
|
// versions[1] = new ArtificialPlayer(new MCTSAI2(10), "MCTS V2");
|
||||||
versions[2] = new ArtificialPlayer(new MCTSAI3(100, 8), "MCTS V3");
|
versions[0] = new ArtificialPlayer(new MCTSAI3(10, 8), "MCTS V3");
|
||||||
versions[3] = new ArtificialPlayer(new MCTSAI4(100, 8), "MCTS V4");
|
versions[1] = new ArtificialPlayer(new MCTSAI4(10, 8), "MCTS V4");
|
||||||
for (int i = 0; i < versions.length; i++) {
|
for (int i = 0; i < versions.length; i++) {
|
||||||
for (int j = i + 1; j < versions.length; j++) {
|
for (int j = i + 1; j < versions.length; j++) {
|
||||||
final int playerIndex1 = i % versions.length;
|
final int playerIndex1 = i % versions.length;
|
||||||
@@ -44,18 +44,26 @@ public class AITest {
|
|||||||
matchupList.add(new Matchup(v1, v2));
|
matchupList.add(new Matchup(v1, v2));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addData(AIData data) {
|
public void addAIData(AIData data) {
|
||||||
dataList.add(data);
|
dataList.add(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addGameData(GameData data) {
|
||||||
|
gameDataList.add(data);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testAIvsAI() {
|
public void testAIvsAI() {
|
||||||
for (Matchup m : matchupList) {
|
for (Matchup m : matchupList) {
|
||||||
playGame(m);
|
for (int i = 0; i < 6; i++) {
|
||||||
|
playGame(m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void playGame(Matchup m) {
|
public void playGame(Matchup m) {
|
||||||
|
long millisecondscounterAI1 = 0L;
|
||||||
|
long millisecondscounterAI2 = 0L;
|
||||||
List<Integer> iterationsAI1 = new ArrayList<>();
|
List<Integer> iterationsAI1 = new ArrayList<>();
|
||||||
List<Integer> iterationsAI2 = new ArrayList<>();
|
List<Integer> iterationsAI2 = new ArrayList<>();
|
||||||
final BitboardReversi match = new BitboardReversi();
|
final BitboardReversi match = new BitboardReversi();
|
||||||
@@ -65,20 +73,46 @@ public class AITest {
|
|||||||
match.init(players);
|
match.init(players);
|
||||||
while (!match.isTerminal()) {
|
while (!match.isTerminal()) {
|
||||||
final int currentAI = match.getCurrentTurn();
|
final int currentAI = match.getCurrentTurn();
|
||||||
|
final long startTime = System.nanoTime();
|
||||||
final long move = players[currentAI].getMove(match);
|
final long move = players[currentAI].getMove(match);
|
||||||
|
final long endTime = System.nanoTime();
|
||||||
if (players[currentAI].getAi() instanceof MCTSAI) {
|
if (players[currentAI].getAi() instanceof MCTSAI) {
|
||||||
final int lastIterations = ((MCTSAI) players[currentAI].getAi()).getLastIterations();
|
final int lastIterations = ((MCTSAI) players[currentAI].getAi()).getLastIterations();
|
||||||
if (currentAI == 0) {
|
if (currentAI == 0) {
|
||||||
iterationsAI1.add(lastIterations);
|
iterationsAI1.add(lastIterations);
|
||||||
|
millisecondscounterAI1 += (endTime - startTime);
|
||||||
} else {
|
} else {
|
||||||
iterationsAI2.add(lastIterations);
|
iterationsAI2.add(lastIterations);
|
||||||
|
millisecondscounterAI2 += (endTime - startTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match.play(move);
|
match.play(move);
|
||||||
}
|
}
|
||||||
|
generateMatchData(m.getPlayer1().getName(), m.getPlayer2().getName(), match, millisecondscounterAI1, millisecondscounterAI2);
|
||||||
generateData(m, match, iterationsAI1, iterationsAI2);
|
generateData(m, match, iterationsAI1, iterationsAI2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void generateMatchData(String AI1, String AI2, BitboardReversi match, long millisecondscounterAI1, long millisecondscounterAI2) {
|
||||||
|
addGameData(new GameData(
|
||||||
|
AI1,
|
||||||
|
AI2,
|
||||||
|
getWinnerForMatch(AI1, AI2, match
|
||||||
|
millisecondscounterAI1,
|
||||||
|
millisecondscounterAI2
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWinnerForMatch(String AI1, String AI2, BitboardReversi match) {
|
||||||
|
if (match.getWinner() == 0) {
|
||||||
|
return AI1;
|
||||||
|
}
|
||||||
|
if (match.getWinner() == 1) {
|
||||||
|
return AI2;
|
||||||
|
} else {
|
||||||
|
return "TIE";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void generateData(Matchup matchup, BitboardReversi match, List<Integer> iterationsAI1, List<Integer> iterationsAI2) {
|
public void generateData(Matchup matchup, BitboardReversi match, List<Integer> iterationsAI1, List<Integer> iterationsAI2) {
|
||||||
boolean matchup1Found = false;
|
boolean matchup1Found = false;
|
||||||
boolean matchup2Found = false;
|
boolean matchup2Found = false;
|
||||||
@@ -90,10 +124,10 @@ public class AITest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!(matchup1Found)) {
|
if (!(matchup1Found)) {
|
||||||
addData(new AIData(matchup.getPlayer1().getName(), 0, 0, 0, 0, 0, 0));
|
addAIData(new AIData(matchup.getPlayer1().getName(), 0, 0, 0, 0, 0, 0));
|
||||||
}
|
}
|
||||||
if (!(matchup2Found)) {
|
if (!(matchup2Found)) {
|
||||||
addData(new AIData(matchup.getPlayer2().getName(), 0, 0, 0, 0, 0, 0));
|
addAIData(new AIData(matchup.getPlayer2().getName(), 0, 0, 0, 0, 0, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (AIData aiData : dataList) { // set data for player 1
|
for (AIData aiData : dataList) { // set data for player 1
|
||||||
@@ -150,13 +184,31 @@ public class AITest {
|
|||||||
@AfterAll
|
@AfterAll
|
||||||
public static void writeAfterTests() {
|
public static void writeAfterTests() {
|
||||||
try {
|
try {
|
||||||
writeToCsv("Data.csv", dataList);
|
writeAIToCsv("Data.csv", dataList);
|
||||||
|
writeGamesToCSV("Games.csv", gameDataList);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void writeToCsv(String filepath, List<AIData> dataList) throws IOException {
|
public static void writeGamesToCSV(String filepath, List<GameData> gameDataList) throws IOException {
|
||||||
|
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filepath))) {
|
||||||
|
writer.write("Black,White,Winner,Turns Played,Total Time AI1, Total Time AI2");
|
||||||
|
writer.newLine();
|
||||||
|
for (GameData gameData : gameDataList) {
|
||||||
|
writer.write(
|
||||||
|
gameData.AI1() + "," +
|
||||||
|
gameData.AI2() + "," +
|
||||||
|
gameData.winner() + "," +
|
||||||
|
gameData.turns() + "," +
|
||||||
|
(gameData.millisecondsAI1() / 1_000_000L) + "," +
|
||||||
|
(gameData.millisecondsAI2() / 1_000_000L));
|
||||||
|
writer.newLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeAIToCsv(String filepath, List<AIData> dataList) throws IOException {
|
||||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filepath))) {
|
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filepath))) {
|
||||||
writer.write("AI Name,Games Played,Winrate,Average Iterations,Average Iterations 0-10, Average Iterations 11-20, Average Iterations 20-30");
|
writer.write("AI Name,Games Played,Winrate,Average Iterations,Average Iterations 0-10, Average Iterations 11-20, Average Iterations 20-30");
|
||||||
writer.newLine();
|
writer.newLine();
|
||||||
|
|||||||
4
game/src/test/java/research/GameData.java
Normal file
4
game/src/test/java/research/GameData.java
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package research;
|
||||||
|
|
||||||
|
public record GameData(String AI1, String AI2, String winner, int turns, long millisecondsAI1, long millisecondsAI2) {}
|
||||||
|
|
||||||
Reference in New Issue
Block a user