diff --git a/.idea/compiler.xml b/.idea/compiler.xml index dcffce8..d801bf4 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -7,6 +7,7 @@ + diff --git a/app/pom.xml b/app/pom.xml index 962806c..ca89a53 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -6,12 +6,12 @@ 0.1 - org.toop.Main 25 25 UTF-8 + org.toop @@ -25,6 +25,12 @@ 0.1 compile + + + org.openjfx + javafx-controls + 25 + @@ -38,24 +44,6 @@ 25 25 UTF-8 - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/java/org/toop/Main.java b/app/src/main/java/org/toop/Main.java index 30d8bb3..9d9bbfb 100644 --- a/app/src/main/java/org/toop/Main.java +++ b/app/src/main/java/org/toop/Main.java @@ -1,18 +1,16 @@ package org.toop; -import org.toop.app.gui.LocalServerSelector; +import org.toop.app.App; import org.toop.framework.networking.NetworkingClientManager; import org.toop.framework.networking.NetworkingInitializationException; - public class Main { - public static void main(String[] args) { + static void main(String[] args) { initSystems(); - javax.swing.SwingUtilities.invokeLater(LocalServerSelector::new); + App.run(args); } private static void initSystems() throws NetworkingInitializationException { new NetworkingClientManager(); } - } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java new file mode 100644 index 0000000..da9a955 --- /dev/null +++ b/app/src/main/java/org/toop/app/App.java @@ -0,0 +1,62 @@ +package org.toop.app; + +import org.toop.app.menu.MainMenu; +import org.toop.app.menu.Menu; +import org.toop.app.menu.QuitMenu; + +import javafx.application.Application; +import javafx.scene.layout.StackPane; +import javafx.scene.Scene; +import javafx.stage.Stage; + +public class App extends Application { + private static Stage stage; + private static Scene scene; + private static StackPane root; + + public static void run(String[] args) { + launch(args); + } + + @Override + public void start(Stage stage) throws Exception { + final StackPane root = new StackPane(new MainMenu().getPane()); + final Scene scene = new Scene(root); + + stage.setTitle("pism"); + stage.setMinWidth(1080); + stage.setMinHeight(720); + + stage.setOnCloseRequest(event -> { + event.consume(); + push(new QuitMenu()); + }); + + stage.setScene(scene); + stage.setResizable(false); + + stage.show(); + + App.stage = stage; + App.scene = scene; + App.root = root; + } + + public static void activate(Menu menu) { + scene.setRoot(menu.getPane()); + } + + public static void push(Menu menu) { + root.getChildren().add(menu.getPane()); + } + + public static void pop() { + root.getChildren().removeLast(); + } + + public static void quit() { + stage.close(); + } + + public static StackPane getRoot() { return root; } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/gui/BackgroundPanel.java b/app/src/main/java/org/toop/app/gui/BackgroundPanel.java deleted file mode 100644 index bd5c6d2..0000000 --- a/app/src/main/java/org/toop/app/gui/BackgroundPanel.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.toop.app.gui; - -import java.awt.*; -import javax.swing.*; - -public class BackgroundPanel extends JPanel { - private Image backgroundImage; - - public void setBackgroundImage(Image image) { - this.backgroundImage = image; - repaint(); - } - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - if (backgroundImage != null) { - g.drawImage(backgroundImage, 0, 0, getWidth(), getHeight(), this); - } - } -} diff --git a/app/src/main/java/org/toop/app/gui/LocalGameSelector.form b/app/src/main/java/org/toop/app/gui/LocalGameSelector.form deleted file mode 100644 index 0816519..0000000 --- a/app/src/main/java/org/toop/app/gui/LocalGameSelector.form +++ /dev/null @@ -1,37 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/app/src/main/java/org/toop/app/gui/LocalGameSelector.java b/app/src/main/java/org/toop/app/gui/LocalGameSelector.java deleted file mode 100644 index cf4a44b..0000000 --- a/app/src/main/java/org/toop/app/gui/LocalGameSelector.java +++ /dev/null @@ -1,107 +0,0 @@ -package org.toop.app.gui; - -import java.awt.*; -import javax.swing.*; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.toop.tictactoe.LocalTicTacToe; -import org.toop.tictactoe.gui.UIGameBoard; - -public class LocalGameSelector extends JFrame { - private static final Logger logger = LogManager.getLogger(LocalGameSelector.class); - - private JPanel panel1; - private JComboBox gameSelectionComboBox; - private JButton startGame; - private JComboBox playerTypeSelectionBox; - private JButton deleteSave; - - private JPanel cards; // CardLayout panel - private CardLayout cardLayout; - - private UIGameBoard tttBoard; - - public LocalGameSelector() { - setTitle("Local Game Selector"); - setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - setSize(1920, 1080); - setLocationRelativeTo(null); - - // Setup CardLayout - cardLayout = new CardLayout(); - cards = new JPanel(cardLayout); - setContentPane(cards); - - // --- Main menu panel --- - panel1 = new JPanel(); - panel1.setLayout(new FlowLayout()); - gameSelectionComboBox = new JComboBox<>(); - gameSelectionComboBox.addItem("Tic Tac Toe"); - gameSelectionComboBox.addItem("Reversi"); - - playerTypeSelectionBox = new JComboBox<>(); - playerTypeSelectionBox.addItem("Player vs Player"); - playerTypeSelectionBox.addItem("Player vs AI"); - playerTypeSelectionBox.addItem("AI vs Player"); - - panel1.add(gameSelectionComboBox); - panel1.add(playerTypeSelectionBox); - - startGame = new JButton("Start Game"); - panel1.add(startGame); - - deleteSave = new JButton("Delete Save"); - panel1.add(deleteSave); - deleteSave.setEnabled(false); - deleteSave.addActionListener( - e -> { - tttBoard = null; - deleteSave.setEnabled(false); - }); - - cards.add(panel1, "MainMenu"); - - // Start button action - startGame.addActionListener(e -> startGameClicked()); - - setVisible(true); - } - - private void startGameClicked() { - String playerTypes = (String) playerTypeSelectionBox.getSelectedItem(); - String selectedGame = (String) gameSelectionComboBox.getSelectedItem(); - - LocalTicTacToe lttt = null; - - if (playerTypes.equals("Player vs Player")) { - logger.info("Player vs Player"); - lttt = LocalTicTacToe.createLocal(new boolean[] {false, false}); - } else { - if (playerTypes.equals("Player vs AI")) { - logger.info("Player vs AI"); - lttt = LocalTicTacToe.createLocal(new boolean[] {false, true}); - } else { - logger.info("AI vs Player"); - lttt = LocalTicTacToe.createLocal(new boolean[] {true, false}); - } - } - - if ("Tic Tac Toe".equalsIgnoreCase(selectedGame)) { - if (tttBoard == null) { - tttBoard = new UIGameBoard(lttt, this); - cards.add(tttBoard.getTTTPanel(), "TicTacToe"); - } - cardLayout.show(cards, "TicTacToe"); - } - lttt.startThreads(); - } - - public void showMainMenu() { - cardLayout.show(cards, "MainMenu"); - gameSelectionComboBox.setSelectedIndex(0); - playerTypeSelectionBox.setSelectedIndex(0); - if (tttBoard != null) { - deleteSave.setEnabled(true); - } - } -} diff --git a/app/src/main/java/org/toop/app/gui/LocalServerSelector.form b/app/src/main/java/org/toop/app/gui/LocalServerSelector.form deleted file mode 100644 index 68623a2..0000000 --- a/app/src/main/java/org/toop/app/gui/LocalServerSelector.form +++ /dev/null @@ -1,39 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/app/src/main/java/org/toop/app/gui/LocalServerSelector.java b/app/src/main/java/org/toop/app/gui/LocalServerSelector.java deleted file mode 100644 index 4ba4c86..0000000 --- a/app/src/main/java/org/toop/app/gui/LocalServerSelector.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.toop.app.gui; - -import org.toop.events.WindowEvents; -import org.toop.framework.eventbus.EventFlow; -import org.toop.local.AppContext; - -import javax.swing.*; -import java.util.Locale; -import java.util.ResourceBundle; - -public class LocalServerSelector { - private JPanel panel1; - private JButton serverButton; - private JButton localButton; - private final JFrame frame; - Locale locale = AppContext.getLocale(); - ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", locale); - - public LocalServerSelector() { - frame = new JFrame(resourceBundle.getString("windowTitleServerSelector")); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.setContentPane(panel1); - frame.setSize(1920, 1080); - frame.setLocationRelativeTo(null); // Sets to center - frame.setVisible(true); - - serverButton.addActionListener(e -> onServerClicked()); - serverButton.setText(resourceBundle.getString("buttonSelectServer")); - localButton.addActionListener(e -> onLocalClicked()); - localButton.setText(resourceBundle.getString("buttonSelectLocal")); - new EventFlow().listen(WindowEvents.LanguageChanged.class, this::changeLanguage); - } - private void changeLanguage(WindowEvents.LanguageChanged event) { - locale = AppContext.getLocale(); - resourceBundle = ResourceBundle.getBundle("Localization", locale); - } - private void onServerClicked() { - frame.dispose(); - new RemoteGameSelector(); - } - - private void onLocalClicked() { - frame.dispose(); - new LocalGameSelector(); - } -} diff --git a/app/src/main/java/org/toop/app/gui/RemoteGameSelector.form b/app/src/main/java/org/toop/app/gui/RemoteGameSelector.form deleted file mode 100644 index df43ead..0000000 --- a/app/src/main/java/org/toop/app/gui/RemoteGameSelector.form +++ /dev/null @@ -1,147 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/app/src/main/java/org/toop/app/gui/RemoteGameSelector.java b/app/src/main/java/org/toop/app/gui/RemoteGameSelector.java deleted file mode 100644 index 0d13a98..0000000 --- a/app/src/main/java/org/toop/app/gui/RemoteGameSelector.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.toop.app.gui; - -import java.awt.event.ActionEvent; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; -import javax.swing.*; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.toop.framework.eventbus.EventFlow; -import org.toop.framework.networking.events.NetworkEvents; -import org.toop.tictactoe.LocalTicTacToe; -import org.toop.framework.networking.NetworkingGameClientHandler; -import org.toop.tictactoe.gui.UIGameBoard; - -public class RemoteGameSelector { - private static final Logger logger = LogManager.getLogger(RemoteGameSelector.class); - - private JPanel mainMenu; - private JTextField nameTextField; - private JTextField name2TextField; - private JTextField ipTextField; - private JTextField portTextField; - private JButton connectButton; - private JComboBox gameSelectorBox; - private JPanel cards; - private JPanel gameSelector; - private JFrame frame; - private JLabel fillAllFields; - - private LocalTicTacToe localTicTacToe; - - public RemoteGameSelector() { - gameSelectorBox.addItem("Tic Tac Toe"); - gameSelectorBox.addItem("Reversi"); - // todo get supported games from server and add to gameSelectorBox - frame = new JFrame("Game Selector"); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.setSize(1920, 1080); - frame.setResizable(true); - - init(); - frame.add(mainMenu); - frame.setVisible(true); - // GlobalEventBus.subscribeAndRegister() Todo add game panel to frame when connection - // succeeds - - } - - private void init() { - connectButton.addActionListener( - (ActionEvent e) -> { - if (!nameTextField.getText().isEmpty() - && !name2TextField.getText().isEmpty() - && !ipTextField.getText().isEmpty() - && !portTextField.getText().isEmpty()) { - - AtomicReference clientId = new AtomicReference<>(); - new EventFlow().addPostEvent( - NetworkEvents.StartClient.class, - (Supplier) NetworkingGameClientHandler::new, - "127.0.0.1", - 5001 - ).onResponse( - NetworkEvents.StartClientSuccess.class, - (response) -> { - clientId.set(response.clientId()); - } - ).asyncPostEvent(); - -// GlobalEventBus.subscribeAndRegister( -// NetworkEvents.ReceivedMessage.class, -// event -> { -// if (event.message().equalsIgnoreCase("ok")) { -// logger.info("received ok from server."); -// } else if (event.message().toLowerCase().startsWith("gameid")) { -// String gameId = -// event.message() -// .toLowerCase() -// .replace("gameid ", ""); -// GlobalEventBus.post( -// new NetworkEvents.SendCommand( -// "start_game " + gameId)); -// } else { -// logger.info("{}", event.message()); -// } -// }); - frame.remove(mainMenu); - UIGameBoard ttt = new UIGameBoard(localTicTacToe, this); - localTicTacToe.startThreads(); - frame.add(ttt.getTTTPanel()); // TODO: Fix later - frame.revalidate(); - frame.repaint(); - } else { - fillAllFields.setVisible(true); - } - }); - } - - public void showMainMenu() { - frame.removeAll(); - frame.add(mainMenu); - frame.revalidate(); - frame.repaint(); - } -} diff --git a/app/src/main/java/org/toop/app/menu/CreditsMenu.java b/app/src/main/java/org/toop/app/menu/CreditsMenu.java new file mode 100644 index 0000000..96c77cd --- /dev/null +++ b/app/src/main/java/org/toop/app/menu/CreditsMenu.java @@ -0,0 +1,6 @@ +package org.toop.app.menu; + +public final class CreditsMenu extends Menu { + public CreditsMenu() { + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/menu/MainMenu.java b/app/src/main/java/org/toop/app/menu/MainMenu.java new file mode 100644 index 0000000..d4e1b70 --- /dev/null +++ b/app/src/main/java/org/toop/app/menu/MainMenu.java @@ -0,0 +1,31 @@ +package org.toop.app.menu; + +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; + +public final class MainMenu extends Menu { + public MainMenu() { + final ImageView background = new ImageView(); + + final Button tictactoe = createButton("Tic Tac Toe", () -> {}); + final Button reversi = createButton("Reversi", () -> {}); + final Button sudoku = createButton("Sudoku", () -> {}); + final Button battleship = createButton("Battleship", () -> {}); + final Button other = createButton("Other", () -> {}); + + final VBox gamesBox = new VBox(tictactoe, reversi, sudoku, background, other); + gamesBox.setAlignment(Pos.TOP_CENTER); + + final Button credits = createButton("Credits", () -> {}); + final Button options = createButton("Options", () -> {}); + final Button quit = createButton("Quit", () -> {}); + + final VBox creditsBox = new VBox(10, credits, options, quit); + creditsBox.setAlignment(Pos.BOTTOM_CENTER); + + pane = new StackPane(background, grid); + pane.getStylesheets().add(getClass().getResource("/style/main.css").toExternalForm()); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/menu/Menu.java b/app/src/main/java/org/toop/app/menu/Menu.java new file mode 100644 index 0000000..fc0627f --- /dev/null +++ b/app/src/main/java/org/toop/app/menu/Menu.java @@ -0,0 +1,27 @@ +package org.toop.app.menu; + +import org.toop.app.App; + +import javafx.animation.FadeTransition; +import javafx.scene.control.Button; +import javafx.scene.layout.Pane; +import javafx.util.Duration; + +public abstract class Menu { + protected Pane pane; + public Pane getPane() { return pane; } + + public void fadeBackgroundImage(String imagePath, float from, float to, float milliseconds) { + final FadeTransition fade = new FadeTransition(Duration.millis(milliseconds), App.getRoot()); + fade.setFromValue(from); + fade.setToValue(to); + fade.play(); + } + + public Button createButton(String text, Runnable runnable) { + final Button button = new Button(text); + button.setOnAction(_ -> runnable.run()); + button.getStyleClass().add("button"); + return button; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/menu/OptionsMenu.java b/app/src/main/java/org/toop/app/menu/OptionsMenu.java new file mode 100644 index 0000000..541bb73 --- /dev/null +++ b/app/src/main/java/org/toop/app/menu/OptionsMenu.java @@ -0,0 +1,6 @@ +package org.toop.app.menu; + +public final class OptionsMenu extends Menu { + public OptionsMenu() { + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/menu/QuitMenu.java b/app/src/main/java/org/toop/app/menu/QuitMenu.java new file mode 100644 index 0000000..fe359da --- /dev/null +++ b/app/src/main/java/org/toop/app/menu/QuitMenu.java @@ -0,0 +1,48 @@ +package org.toop.app.menu; + +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import org.toop.app.App; + +public final class QuitMenu extends Menu { + public QuitMenu() { + final Region background = new Region(); + background.getStyleClass().add("quit-background"); + background.setPrefSize(Double.MAX_VALUE, Double.MAX_VALUE); + + final Text sure = new Text("Are you sure?"); + sure.getStyleClass().add("quit-text"); + + final Button yes = new Button("Yes"); + yes.getStyleClass().add("quit-button"); + yes.setOnAction(_ -> { + App.quit(); + }); + + final Button no = new Button("No"); + no.getStyleClass().add("quit-button"); + no.setOnAction(_ -> { + App.pop(); + }); + + final HBox buttons = new HBox(10, yes, no); + buttons.setAlignment(Pos.CENTER); + + VBox box = new VBox(43, sure, buttons); + box.setAlignment(Pos.CENTER); + box.getStyleClass().add("quit-box"); + box.setMaxWidth(350); + box.setMaxHeight(200); + + StackPane modalContainer = new StackPane(background, box); + StackPane.setAlignment(box, Pos.CENTER); + + pane = modalContainer; + pane.getStylesheets().add(getClass().getResource("/style/quit.css").toExternalForm()); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/toop/events/WindowEvents.java b/app/src/main/java/org/toop/events/WindowEvents.java deleted file mode 100644 index 50d3f2d..0000000 --- a/app/src/main/java/org/toop/events/WindowEvents.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.toop.events; - -import org.toop.framework.eventbus.events.EventWithoutSnowflake; -import org.toop.framework.eventbus.events.EventsBase; - -public class WindowEvents extends EventsBase { - - /** Triggers when a cell is clicked in one of the game boards. */ - public record CellClicked(int cell) implements EventWithoutSnowflake {} - - /** Triggers when the window wants to quit. */ - public record OnQuitRequested() implements EventWithoutSnowflake {} - - /** Triggers when the window is resized. */ -// public record OnResize(Window.Size size) implements EventWithoutSnowflake {} - - /** Triggers when the mouse is moved within the window. */ - public record OnMouseMove(int x, int y) implements EventWithoutSnowflake {} - - /** Triggers when the mouse is clicked within the window. */ - public record OnMouseClick(int button) implements EventWithoutSnowflake {} - - /** Triggers when the mouse is released within the window. */ - public record OnMouseRelease(int button) implements EventWithoutSnowflake {} - - /** Triggers when the language is changed. */ - public record LanguageChanged() implements EventWithoutSnowflake {} -} \ No newline at end of file diff --git a/app/src/main/java/org/toop/tictactoe/LocalTicTacToe.java b/app/src/main/java/org/toop/tictactoe/LocalTicTacToe.java deleted file mode 100644 index f0903b2..0000000 --- a/app/src/main/java/org/toop/tictactoe/LocalTicTacToe.java +++ /dev/null @@ -1,257 +0,0 @@ -package org.toop.tictactoe; - -import java.util.concurrent.*; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.toop.framework.eventbus.EventFlow; -import org.toop.framework.networking.events.NetworkEvents; -import org.toop.game.GameBase; -import org.toop.tictactoe.gui.UIGameBoard; -import org.toop.framework.networking.NetworkingGameClientHandler; - -import java.util.function.Supplier; - -import static java.lang.Thread.sleep; - -/** - * A representation of a local tic-tac-toe game. Calls are made to a server for information about - * current game state. MOST OF THIS CODE IS TRASH, THROW IT OUT OF THE WINDOW AFTER DEMO. - */ -// Todo: refactor -public class LocalTicTacToe { // TODO: Implement runnable - private static final Logger logger = LogManager.getLogger(LocalTicTacToe.class); - - private final ExecutorService executor = Executors.newFixedThreadPool(3); - private final BlockingQueue receivedQueue = new LinkedBlockingQueue<>(); - private final BlockingQueue moveQueuePlayerA = new LinkedBlockingQueue<>(); - private final BlockingQueue moveQueuePlayerB = new LinkedBlockingQueue<>(); - - private Object receivedMessageListener = null; - - private boolean isLocal; - private String gameId; - private String connectionId = null; - private String serverId = null; - - private boolean isAiPlayer[] = new boolean[2]; - private TicTacToeAI[] aiPlayers = new TicTacToeAI[2]; - private TicTacToe ticTacToe; - private UIGameBoard ui; - - /** Is either 0 or 1. */ - private int playersTurn = 0; - - /** - * @return The current players turn. - */ - public int getCurrentPlayersTurn() { - return this.playersTurn; - } - - // LocalTicTacToe(String gameId, String connectionId, String serverId) { - // this.gameId = gameId; - // this.connectionId = connectionId; - // this.serverId = serverId; - // this.receivedMessageListener = - // GlobalEventBus.subscribe(Events.ServerEvents.ReceivedMessage.class, - // this::receiveMessageAction); - // GlobalEventBus.register(this.receivedMessageListener); - // - // - // this.executor.submit(this::gameThread); - // } TODO: If remote server - - /** - * Starts a connection with a remote server. - * - * @param ip The IP of the server to connect to. - * @param port The port of the server to connect to. - */ - private LocalTicTacToe(String ip, int port) { -// this.receivedMessageListener = -// GlobalEventBus.subscribe(this::receiveMessageAction); -// GlobalEventBus.subscribe(this.receivedMessageListener); - this.connectionId = this.createConnection(ip, port); - this.createGame("X", "O"); - this.isLocal = false; - //this.executor.submit(this::remoteGameThread); - } - - private LocalTicTacToe(boolean[] aiFlags) { - this.isAiPlayer = aiFlags; // store who is AI - - for (int i = 0; i < aiFlags.length && i < this.aiPlayers.length; i++) { - if (aiFlags[i]) { - this.aiPlayers[i] = new TicTacToeAI(); // create AI for that player - } else { - this.aiPlayers[i] = null; // not an AI player - } - } - - this.isLocal = true; - //this.executor.submit(this::localGameThread); - } - public void startThreads(){ - if (isLocal) { - this.executor.submit(this::localGameThread); - }else { - this.executor.submit(this::remoteGameThread); - } - } - - public static LocalTicTacToe createLocal(boolean[] aiPlayers) { - return new LocalTicTacToe(aiPlayers); - } - - public static LocalTicTacToe createRemote(String ip, int port) { - return new LocalTicTacToe(ip, port); - } - - private String createConnection(String ip, int port) { - CompletableFuture connectionIdFuture = new CompletableFuture<>(); - new EventFlow().addPostEvent(NetworkEvents.StartClientRequest.class, - (Supplier) NetworkingGameClientHandler::new, - ip, port, connectionIdFuture).asyncPostEvent(); // TODO: what if server couldn't be started with port. - try { - return connectionIdFuture.get(); - } catch (InterruptedException | ExecutionException e) { - logger.error("Error getting connection ID", e); - } - return null; - } - - private void createGame(String nameA, String nameB) { - nameA = nameA.trim().replace(" ", "-"); - nameB = nameB.trim().replace(" ", "-"); - this.sendCommand("create_game", nameA, nameB); - } - - private void startGame() { - if (this.gameId == null) { - return; - } - this.sendCommand("start_game", this.gameId); - } - - private void localGameThread() { - boolean running = true; - this.ticTacToe = new TicTacToe("X", "O"); - while (running) { - try { - GameBase.State state; - if (!isAiPlayer[0]) { - state = this.ticTacToe.play(this.moveQueuePlayerA.take()); - } else { - int bestMove = aiPlayers[0].findBestMove(this.ticTacToe); - state = this.ticTacToe.play(bestMove); - if (state != GameBase.State.INVALID) { - ui.setCell(bestMove, "X"); - } - } - if (state == GameBase.State.WIN || state == GameBase.State.DRAW) { - ui.setState(state, "X"); - running = false; - } - this.setNextPlayersTurn(); - if (!isAiPlayer[1]) { - state = this.ticTacToe.play(this.moveQueuePlayerB.take()); - } else { - int bestMove = aiPlayers[1].findBestMove(this.ticTacToe); - state = this.ticTacToe.play(bestMove); - if (state != GameBase.State.INVALID) { - ui.setCell(bestMove, "O"); - } - } - if (state == GameBase.State.WIN || state == GameBase.State.DRAW) { - ui.setState(state, "O"); - running = false; - } - this.setNextPlayersTurn(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - } - - private void remoteGameThread() { - // TODO: If server start this. - } - - public void setNextPlayersTurn() { - if (this.playersTurn == 0) { - this.playersTurn += 1; - } else { - this.playersTurn -= 1; - } - } - - public char[] getCurrentBoard() { - return ticTacToe.getGrid(); - } - - /** End the current game. */ - public void endGame() { - sendCommand("gameid", "end_game"); // TODO: Command is a bit wrong. - } - - /** - * @param moveIndex The index of the move to make. - */ - public void move(int moveIndex) { - this.executor.submit( - () -> { - try { - if (this.playersTurn == 0 && !isAiPlayer[0]) { - this.moveQueuePlayerA.put(moveIndex); - logger.info( - "Adding player's {}, move: {} to queue A", - this.playersTurn, - moveIndex); - } else if (this.playersTurn == 1 && !isAiPlayer[1]) { - this.moveQueuePlayerB.put(moveIndex); - logger.info( - "Adding player's {}, move: {} to queue B", - this.playersTurn, - moveIndex); - } - } catch (InterruptedException e) { - logger.error( - "Could not add player: {}'s, move {}", - this.playersTurn, - moveIndex); // TODO: Error handling instead of crash. - } - }); - } - - private void endTheGame() { - this.sendCommand("end_game", this.gameId); -// this.endListeners(); - } - - private void receiveMessageAction(NetworkEvents.ReceivedMessage receivedMessage) { - if (!receivedMessage.ConnectionUuid().equals(this.connectionId)) { - return; - } - - try { - logger.info( - "Received message from {}: {}", this.connectionId, receivedMessage.message()); - this.receivedQueue.put(receivedMessage.message()); - } catch (InterruptedException e) { - logger.error("Error waiting for received Message", e); - } - } - - private void sendCommand(String... args) { - new EventFlow().addPostEvent(NetworkEvents.SendCommand.class, this.connectionId, args).asyncPostEvent(); - } - -// private void endListeners() { -// GlobalEventBus.unregister(this.receivedMessageListener); -// } TODO - - public void setUIReference(UIGameBoard uiGameBoard) { - this.ui = uiGameBoard; - } -} diff --git a/app/src/main/java/org/toop/tictactoe/gui/UIGameBoard.form b/app/src/main/java/org/toop/tictactoe/gui/UIGameBoard.form deleted file mode 100644 index b69fad4..0000000 --- a/app/src/main/java/org/toop/tictactoe/gui/UIGameBoard.form +++ /dev/null @@ -1,23 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - -
diff --git a/app/src/main/java/org/toop/tictactoe/gui/UIGameBoard.java b/app/src/main/java/org/toop/tictactoe/gui/UIGameBoard.java deleted file mode 100644 index 68e0b9f..0000000 --- a/app/src/main/java/org/toop/tictactoe/gui/UIGameBoard.java +++ /dev/null @@ -1,153 +0,0 @@ -package org.toop.tictactoe.gui; - -import java.awt.*; -import java.awt.event.ActionEvent; -import java.util.Locale; -import java.util.ResourceBundle; -import javax.swing.*; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.toop.app.gui.LocalGameSelector; -import org.toop.app.gui.RemoteGameSelector; -import org.toop.events.WindowEvents; -import org.toop.framework.eventbus.EventFlow; -import org.toop.local.AppContext; -import org.toop.tictactoe.LocalTicTacToe; -import org.toop.game.GameBase; - -public class UIGameBoard { - private static final int TICTACTOE_SIZE = 3; - - private static final Logger logger = LogManager.getLogger(LocalGameSelector.class); - - private JPanel tttPanel; // Root panel for this game - private JButton backToMainMenuButton; - private JButton[] cells; - private String currentPlayer = "X"; - private int currentPlayerIndex = 0; - - private Object parentSelector; - private boolean parentLocal; - private LocalTicTacToe localTicTacToe; - - private boolean gameOver = false; - Locale locale = AppContext.getLocale(); - ResourceBundle resourceBundle = ResourceBundle.getBundle("Localization", locale); - - public UIGameBoard(LocalTicTacToe lttt, Object parent) { - if (!(parent == null)) { - if (parent instanceof LocalGameSelector) { - parentLocal = true; - } else if (parent instanceof RemoteGameSelector) { - parentLocal = false; - } - } - this.parentSelector = parent; - this.localTicTacToe = lttt; - lttt.setUIReference(this); - - // Root panel - tttPanel = new JPanel(new BorderLayout()); - - // Back button - backToMainMenuButton = new JButton(resourceBundle.getString("buttonBackToMainMenu")); - tttPanel.add(backToMainMenuButton, BorderLayout.SOUTH); - backToMainMenuButton.addActionListener( - _ -> { - // TODO reset game and connections - // Game now gets reset in local - if (parentLocal) { - ((LocalGameSelector) parent).showMainMenu(); - } else { - ((RemoteGameSelector) parent).showMainMenu(); - } - }); - - // Game grid - JPanel gameGrid = createGridPanel(TICTACTOE_SIZE, TICTACTOE_SIZE); - tttPanel.add(gameGrid, BorderLayout.CENTER); - - // localTicTacToe.setMoveListener((playerIndex, moveIndex, symbol) -> { - // SwingUtilities.invokeLater(() -> { - // cells[moveIndex].setText(String.valueOf(symbol)); - // }); - // }); - new EventFlow().listen(WindowEvents.LanguageChanged.class, this::changeLanguage); - } - private void changeLanguage(WindowEvents.LanguageChanged event) { - locale = AppContext.getLocale(); - resourceBundle = ResourceBundle.getBundle("Localization", locale); - } - - private JPanel createGridPanel(int sizeX, int sizeY) { - JPanel panel = new JPanel(new GridLayout(sizeX, sizeY)); - cells = new JButton[sizeX * sizeY]; - - for (int i = 0; i < sizeX * sizeY; i++) { - cells[i] = new JButton(" "); - cells[i].setFont(new Font("Arial", Font.BOLD, 400 / sizeX)); - panel.add(cells[i]); - cells[i].setFocusable(false); - - final int index = i; - cells[i].addActionListener( - (ActionEvent _) -> { - if (!gameOver) { - if (cells[index].getText().equals(" ")) { - int cp = this.localTicTacToe.getCurrentPlayersTurn(); - if (cp == 0) { - this.currentPlayer = "X"; - currentPlayerIndex = 0; - } else if (cp == 1) { - this.currentPlayer = "O"; - currentPlayerIndex = 1; - } - this.localTicTacToe.move(index); - cells[index].setText(currentPlayer); - } else { - logger.info( - "Player " - + currentPlayerIndex - + " attempted invalid move at: " - + cells[index].getText()); - } - } else { - logger.info( - "Player " - + currentPlayerIndex - + " attempted to move after the game has ended."); - } - }); - } - - return panel; - } - - public void setCell(int index, String move) { - System.out.println(cells[index].getText()); - cells[index].setText(move); - } - - public void setState(GameBase.State state, String playerMove) { - Color color; - if (state == GameBase.State.WIN && playerMove.equals(currentPlayer)) { - color = new Color(160, 220, 160); - } else if (state == GameBase.State.WIN) { - color = new Color(220, 160, 160); - } else if (state == GameBase.State.DRAW) { - color = new Color(220, 220, 160); - } else { - color = new Color(220, 220, 220); - } - for (JButton cell : cells) { - cell.setBackground(color); - } - if (state == GameBase.State.DRAW || state == GameBase.State.WIN) { - gameOver = true; - } - } - - public JPanel getTTTPanel() { - return tttPanel; - } -} diff --git a/app/src/main/resources/image/game/battleship.png b/app/src/main/resources/image/game/battleship.png new file mode 100644 index 0000000..813893f Binary files /dev/null and b/app/src/main/resources/image/game/battleship.png differ diff --git a/app/src/main/resources/image/game/other.png b/app/src/main/resources/image/game/other.png new file mode 100644 index 0000000..6bd4167 Binary files /dev/null and b/app/src/main/resources/image/game/other.png differ diff --git a/app/src/main/resources/image/game/reversi.png b/app/src/main/resources/image/game/reversi.png new file mode 100644 index 0000000..bd9252f Binary files /dev/null and b/app/src/main/resources/image/game/reversi.png differ diff --git a/app/src/main/resources/image/game/sudoku.png b/app/src/main/resources/image/game/sudoku.png new file mode 100644 index 0000000..ec88234 Binary files /dev/null and b/app/src/main/resources/image/game/sudoku.png differ diff --git a/app/src/main/resources/image/game/tictactoe.png b/app/src/main/resources/image/game/tictactoe.png new file mode 100644 index 0000000..2a81e05 Binary files /dev/null and b/app/src/main/resources/image/game/tictactoe.png differ diff --git a/app/src/main/resources/style/main.css b/app/src/main/resources/style/main.css new file mode 100644 index 0000000..99e8087 --- /dev/null +++ b/app/src/main/resources/style/main.css @@ -0,0 +1,33 @@ +.main-button { + -fx-background-color: transparent; + -fx-background-image: url("card-default.jpg"); /* fallback image */ + -fx-background-size: cover; + -fx-background-position: center; + -fx-pref-width: 250px; + -fx-pref-height: 180px; + -fx-border-radius: 15; + -fx-background-radius: 15; + -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 15, 0.4, 0, 4); + -fx-cursor: hand; + -fx-padding: 0; +} + +.card-label { + -fx-background-color: rgba(0, 0, 0, 0.5); + -fx-font-size: 20px; + -fx-font-weight: bold; + -fx-text-fill: white; + -fx-padding: 10px; + -fx-alignment: top-center; + -fx-background-radius: 15 15 0 0; + -fx-opacity: 0; + -fx-transition: all 0.3s ease; +} + +.main-button:hover { + -fx-effect: dropshadow(gaussian, #00ffff, 15, 0.5, 0, 0); +} + +.main-button:hover .card-label { + -fx-opacity: 1; +} \ No newline at end of file diff --git a/app/src/main/resources/style/quit.css b/app/src/main/resources/style/quit.css new file mode 100644 index 0000000..59c72a3 --- /dev/null +++ b/app/src/main/resources/style/quit.css @@ -0,0 +1,33 @@ +.quit-background { + -fx-background-color: rgba(0, 0, 0, 0.6); +} + +.quit-box { + -fx-background-color: rgba(30, 30, 30, 0.95); + -fx-background-radius: 15; + -fx-padding: 30; + -fx-effect: dropshadow(gaussian, black, 20, 0.6, 0, 4); +} + +.quit-text { + -fx-fill: white; + -fx-font-size: 28px; + -fx-font-weight: 600; + -fx-font-family: "Segoe UI", sans-serif; +} + +.quit-button { + -fx-font-size: 16px; + -fx-text-fill: white; + -fx-background-color: transparent; + -fx-border-color: white; + -fx-border-radius: 5; + -fx-padding: 8 20; + -fx-cursor: hand; +} + +.quit-button:hover { + -fx-text-fill: #00ffff; + -fx-border-color: #00ffff; + -fx-effect: dropshadow(gaussian, #00ffff, 8, 0.5, 0, 0); +} \ No newline at end of file diff --git a/app/src/main/resources/style/style.css b/app/src/main/resources/style/style.css new file mode 100644 index 0000000..c09d516 --- /dev/null +++ b/app/src/main/resources/style/style.css @@ -0,0 +1,20 @@ +.root { + -fx-background-color: #2d2d2d; + + -fx-font-size: 28px; + -fx-font-weight: 600; + -fx-font-family: "Segoe UI", sans-serif; +} + +.button { + -fx-background-color: transparent; + -fx-text-fill: white; + -fx-border-color: transparent; + -fx-padding: 10 20; + -fx-cursor: hand; + -fx-effect: null; +} + +.button:hover { + -fx-effect: dropshadow(gaussian, #00ffff, 10, 0.3, 0, 0); +} \ No newline at end of file diff --git a/game/pom.xml b/game/pom.xml index 3297948..c82a815 100644 --- a/game/pom.xml +++ b/game/pom.xml @@ -13,6 +13,50 @@ + + org.junit + junit-bom + 5.13.4 + pom + import + + + org.junit.jupiter + junit-jupiter-api + 5.13.4 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.13.4 + test + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.4 + + + org.mockito + mockito-core + 5.19.0 + test + + + org.mockito + mockito-junit-jupiter + 5.19.0 + test + + org.apache.logging.log4j log4j-api diff --git a/game/src/main/java/org/toop/game/AI.java b/game/src/main/java/org/toop/game/AI.java new file mode 100644 index 0000000..0506b10 --- /dev/null +++ b/game/src/main/java/org/toop/game/AI.java @@ -0,0 +1,5 @@ +package org.toop.game; + +public abstract class AI { + public abstract Game.Move findBestMove(T game, int depth); +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/Game.java b/game/src/main/java/org/toop/game/Game.java new file mode 100644 index 0000000..b37bd73 --- /dev/null +++ b/game/src/main/java/org/toop/game/Game.java @@ -0,0 +1,57 @@ +package org.toop.game; + +import java.util.Arrays; + +public abstract class Game { + public enum State { + NORMAL, LOSE, DRAW, WIN, + } + + public record Move(int position, char value) {} + + public static final char EMPTY = (char)0; + + protected final int rowSize; + protected final int columnSize; + protected final char[] board; + + protected final Player[] players; + protected int currentPlayer; + + protected Game(int rowSize, int columnSize, Player... players) { + assert rowSize > 0 && columnSize > 0; + assert players.length >= 1; + + this.rowSize = rowSize; + this.columnSize = columnSize; + + board = new char[rowSize * columnSize]; + Arrays.fill(board, EMPTY); + + this.players = players; + currentPlayer = 0; + } + + protected Game(Game other) { + rowSize = other.rowSize; + columnSize = other.columnSize; + board = Arrays.copyOf(other.board, other.board.length); + + players = Arrays.copyOf(other.players, other.players.length); + currentPlayer = other.currentPlayer; + } + + public int getRowSize() { return rowSize; } + public int getColumnSize() { return columnSize; } + public char[] getBoard() { return board; } + + public Player[] getPlayers() { return players; } + public Player getCurrentPlayer() { return players[currentPlayer]; } + + protected void nextPlayer() { + currentPlayer = (currentPlayer + 1) % players.length; + } + + public abstract Move[] getLegalMoves(); + public abstract State play(Move move); +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/GameBase.java b/game/src/main/java/org/toop/game/GameBase.java deleted file mode 100644 index c6de8c3..0000000 --- a/game/src/main/java/org/toop/game/GameBase.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.toop.game; - -// Todo: refactor -public abstract class GameBase { - public enum State { - INVALID, - - NORMAL, - DRAW, - WIN, - } - - public static char EMPTY = '-'; - - protected int size; - public char[] grid; - - protected Player[] players; - public int currentPlayer; - - public GameBase(int size, Player player1, Player player2) { - this.size = size; - grid = new char[size * size]; - - for (int i = 0; i < grid.length; i++) { - grid[i] = EMPTY; - } - - players = new Player[2]; - players[0] = player1; - players[1] = player2; - - currentPlayer = 0; - } - - public boolean isInside(int index) { - return index >= 0 && index < size * size; - } - - public int getSize() { - return size; - } - - public char[] getGrid() { - return grid; - } - - public Player[] getPlayers() { - return players; - } - - public Player getCurrentPlayer() { - return players[currentPlayer]; - } - - public abstract State play(int index); -} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/Player.java b/game/src/main/java/org/toop/game/Player.java index 8f71c40..2dc4a2f 100644 --- a/game/src/main/java/org/toop/game/Player.java +++ b/game/src/main/java/org/toop/game/Player.java @@ -1,20 +1,3 @@ package org.toop.game; -// Todo: refactor -public class Player { - String name; - char symbol; - - public Player(String name, char symbol) { - this.name = name; - this.symbol = symbol; - } - - public String getName() { - return this.name; - } - - public char getSymbol() { - return this.symbol; - } -} \ No newline at end of file +public record Player(String name, char... values) {} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/tictactoe/TicTacToe.java b/game/src/main/java/org/toop/game/tictactoe/TicTacToe.java new file mode 100644 index 0000000..4b39df2 --- /dev/null +++ b/game/src/main/java/org/toop/game/tictactoe/TicTacToe.java @@ -0,0 +1,84 @@ +package org.toop.game.tictactoe; + +import org.toop.game.Game; +import org.toop.game.Player; + +import java.util.ArrayList; + +public final class TicTacToe extends Game { + private int movesLeft; + + public TicTacToe(String player1, String player2) { + super(3, 3, new Player(player1, 'X'), new Player(player2, 'O')); + movesLeft = board.length; + } + + public TicTacToe(TicTacToe other) { + super(other); + movesLeft = other.movesLeft; + } + + @Override + public Move[] getLegalMoves() { + final ArrayList legalMoves = new ArrayList<>(); + + for (int i = 0; i < board.length; i++) { + if (board[i] == EMPTY) { + legalMoves.add(new Move(i, getCurrentPlayer().values()[0])); + } + } + + return legalMoves.toArray(new Move[0]); + } + + @Override + public State play(Move move) { + assert move != null; + assert move.position() >= 0 && move.position() < board.length; + assert move.value() == getCurrentPlayer().values()[0]; + + board[move.position()] = move.value(); + movesLeft--; + + if (checkForWin()) { + return State.WIN; + } + + if (movesLeft <= 0) { + return State.DRAW; + } + + nextPlayer(); + return State.NORMAL; + } + + private boolean checkForWin() { + // Horizontal + for (int i = 0; i < 3; i++) { + final int index = i * 3; + + if (board[index] != EMPTY + && board[index] == board[index + 1] + && board[index] == board[index + 2]) { + return true; + } + } + + // Vertical + for (int i = 0; i < 3; i++) { + if (board[i] != EMPTY + && board[i] == board[i + 3] + && board[i] == board[i + 6]) { + return true; + } + } + + // B-Slash + if (board[0] != EMPTY && board[0] == board[4] && board[0] == board[8]) { + return true; + } + + // F-Slash + return board[2] != EMPTY && board[2] == board[4] && board[2] == board[6]; + } +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java b/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java new file mode 100644 index 0000000..afc61b8 --- /dev/null +++ b/game/src/main/java/org/toop/game/tictactoe/TicTacToeAI.java @@ -0,0 +1,68 @@ +package org.toop.game.tictactoe; + +import org.toop.game.AI; +import org.toop.game.Game; + +public final class TicTacToeAI extends AI { + @Override + public Game.Move findBestMove(TicTacToe game, int depth) { + assert game != null; + assert depth >= 0; + + final Game.Move[] legalMoves = game.getLegalMoves(); + + if (legalMoves.length <= 0) { + return null; + } + + if (legalMoves.length == 9) { + return switch ((int)(Math.random() * 4)) { + case 1 -> legalMoves[2]; + case 2 -> legalMoves[6]; + case 3 -> legalMoves[8]; + default -> legalMoves[0]; + }; + } + + int bestScore = -depth; + Game.Move bestMove = null; + + for (final Game.Move move : legalMoves) { + final int score = getMoveScore(game, depth, move, true); + + if (score > bestScore) { + bestMove = move; + bestScore = score; + } + } + + return bestMove != null? bestMove : legalMoves[(int)(Math.random() * legalMoves.length)]; + } + + private int getMoveScore(TicTacToe game, int depth, Game.Move move, boolean maximizing) { + final TicTacToe copy = new TicTacToe(game); + final Game.State state = copy.play(move); + + switch (state) { + case Game.State.DRAW: return 0; + case Game.State.WIN: return maximizing? depth + 1 : -depth - 1; + } + + if (depth <= 0) { + return 0; + } + + final Game.Move[] legalMoves = copy.getLegalMoves(); + int score = maximizing? depth + 1 : -depth - 1; + + for (final Game.Move next : legalMoves) { + if (maximizing) { + score = Math.min(score, getMoveScore(copy, depth - 1, next, false)); + } else { + score = Math.max(score, getMoveScore(copy, depth - 1, next, true)); + } + } + + return score; + } +} \ No newline at end of file diff --git a/game/src/main/java/org/toop/tictactoe/TicTacToe.java b/game/src/main/java/org/toop/tictactoe/TicTacToe.java deleted file mode 100644 index 70d1598..0000000 --- a/game/src/main/java/org/toop/tictactoe/TicTacToe.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.toop.tictactoe; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.toop.game.GameBase; -import org.toop.game.Player; - -// Todo: refactor -public class TicTacToe extends GameBase { - - protected static final Logger logger = LogManager.getLogger(TicTacToe.class); - - public Thread gameThread; - public String gameId; - - public int movesLeft; - - public TicTacToe(String player1, String player2) { - super(3, new Player(player1, 'X'), new Player(player2, 'O')); - movesLeft = size * size; - } - - /** - * Used for the server. - * - * @param player1 - * @param player2 - * @param gameId - */ - public TicTacToe(String player1, String player2, String gameId) { - super(3, new Player(player1, 'X'), new Player(player2, 'O')); - this.gameId = gameId; - movesLeft = size * size; - } - - @Override - public State play(int index) { - if (!validateMove(index)) { - return State.INVALID; - } - - grid[index] = getCurrentPlayer().getSymbol(); - movesLeft--; - - if (checkWin()) { - return State.WIN; - } - - if (movesLeft <= 0) { - return State.DRAW; - } - - currentPlayer = (currentPlayer + 1) % players.length; - return State.NORMAL; - } - - public boolean validateMove(int index) { - return movesLeft > 0 && isInside(index) && grid[index] == EMPTY; - } - - public boolean checkWin() { - // Horizontal - for (int i = 0; i < 3; i++) { - final int index = i * 3; - - if (grid[index] != EMPTY - && grid[index] == grid[index + 1] - && grid[index] == grid[index + 2]) { - return true; - } - } - - // Vertical - for (int i = 0; i < 3; i++) { - int index = i; - - if (grid[index] != EMPTY - && grid[index] == grid[index + 3] - && grid[index] == grid[index + 6]) { - return true; - } - } - - // B-Slash - if (grid[0] != EMPTY && grid[0] == grid[4] && grid[0] == grid[8]) { - return true; - } - - // F-Slash - if (grid[2] != EMPTY && grid[2] == grid[4] && grid[2] == grid[6]) { - return true; - } - - return false; - } - - /** For AI use only. */ - public void decrementMovesLeft() { - movesLeft--; - } - - /** This method copies the board, mainly for AI use. */ - public TicTacToe copyBoard() { - TicTacToe clone = new TicTacToe(players[0].getName(), players[1].getName()); - System.arraycopy(this.grid, 0, clone.grid, 0, this.grid.length); - clone.movesLeft = this.movesLeft; - clone.currentPlayer = this.currentPlayer; - return clone; - } -} diff --git a/game/src/main/java/org/toop/tictactoe/TicTacToeAI.java b/game/src/main/java/org/toop/tictactoe/TicTacToeAI.java deleted file mode 100644 index 3eb475d..0000000 --- a/game/src/main/java/org/toop/tictactoe/TicTacToeAI.java +++ /dev/null @@ -1,139 +0,0 @@ -package org.toop.tictactoe; - -import org.toop.game.GameBase; - -// Todo: refactor -public class TicTacToeAI { - /** - * This method tries to find the best move by seeing if it can set a winning move, if not, it - * will do a minimax. - */ - public int findBestMove(TicTacToe game) { - int bestVal = -100; // set bestVal to something impossible - int bestMove = 10; // set bestMove to something impossible - - int winningMove = -5; - - boolean empty = true; - for (char cell : game.grid) { - if (!(cell == GameBase.EMPTY)) { - empty = false; - break; - } - } - - if (empty) { // start in a random corner - return switch ((int) (Math.random() * 4)) { - case 1 -> 2; - case 2 -> 6; - case 3 -> 8; - default -> 0; - }; - } - - // simulate all possible moves on the field - for (int i = 0; i < game.grid.length; i++) { - - if (game.validateMove(i)) { // check if the move is legal here - TicTacToe copyGame = game.copyBoard(); // make a copy of the game - GameBase.State result = copyGame.play(i); // play a move on the copy board - - int thisMoveValue; - - if (result == GameBase.State.WIN) { - return i; // just return right away if you can win on the next move - } - - for (int index = 0; index < game.grid.length; index++) { - if (game.validateMove(index)) { - TicTacToe opponentCopy = copyGame.copyBoard(); - GameBase.State opponentResult = opponentCopy.play(index); - if (opponentResult == GameBase.State.WIN) { - winningMove = index; - } - } - } - - thisMoveValue = - doMinimax(copyGame, game.movesLeft, false); // else look at other moves - if (thisMoveValue - > bestVal) { // if better move than the current best, change the move - bestVal = thisMoveValue; - bestMove = i; - } - } - } - if (winningMove > -5) { - return winningMove; - } - return bestMove; // return the best move when we've done everything - } - - /** - * This method simulates all the possible future moves in the game through a copy in search of - * the best move. - */ - public int doMinimax(TicTacToe game, int depth, boolean maximizing) { - boolean state = game.checkWin(); // check for a win (base case stuff) - - if (state) { - if (maximizing) { - // it's the maximizing players turn and someone has won. this is not good, so return - // a negative value - return -10 + depth; - } else { - // it is the turn of the AI and it has won! this is good for us, so return a - // positive value above 0 - return 10 - depth; - } - } else { - boolean empty = false; - for (char cell : - game.grid) { // else, look at draw conditions. we check per cell if it's empty - // or not - if (cell == GameBase.EMPTY) { - empty = true; // if a thing is empty, set to true - break; // break the loop - } - } - if (!empty - || depth == 0) { // if the grid is full or the depth is 0 (both meaning game is - // over) return 0 for draw - return 0; - } - } - - int bestVal; // set the value to the highest possible - if (maximizing) { // it's the maximizing players turn, the AI - bestVal = -100; - for (int i = 0; i < game.grid.length; i++) { // loop through the grid - if (game.validateMove(i)) { - TicTacToe copyGame = game.copyBoard(); - copyGame.play(i); // play the move on a copy board - int value = - doMinimax(copyGame, depth - 1, false); // keep going with the minimax - bestVal = - Math.max( - bestVal, - value); // select the best value for the maximizing player (the - // AI) - } - } - } else { // it's the minimizing players turn, the player - bestVal = 100; - for (int i = 0; i < game.grid.length; i++) { // loop through the grid - if (game.validateMove(i)) { - TicTacToe copyGame = game.copyBoard(); - copyGame.play(i); // play the move on a copy board - int value = doMinimax(copyGame, depth - 1, true); // keep miniMaxing - bestVal = - Math.min( - bestVal, - value); // select the lowest score for the minimizing player, - // they want to make it hard for us - } - } - } - return bestVal; - } -} \ No newline at end of file diff --git a/game/src/test/java/org/toop/game/PlayerTest.java b/game/src/test/java/org/toop/game/PlayerTest.java new file mode 100644 index 0000000..3a3f14b --- /dev/null +++ b/game/src/test/java/org/toop/game/PlayerTest.java @@ -0,0 +1,48 @@ +package org.toop.game; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class PlayerTest { + private Player playerA; + private Player playerB; + private Player playerC; + + @BeforeEach + void setup() { + playerA = new Player("test A", 'x', 'Z', 'i'); + playerB = new Player("test B", 'O', (char)12, (char)-34, 's'); + playerC = new Player("test C", (char)9, '9', (char)-9, '0', 'X', 'O'); + } + + @Test + void testNameGetter_returnsTrueForValidName() { + assertEquals("test A", playerA.name()); + assertEquals("test B", playerB.name()); + assertEquals("test C", playerC.name()); + } + + @Test + void testValuesGetter_returnsTrueForValidValues() { + final char[] valuesA = playerA.values(); + assertEquals('x', valuesA[0]); + assertEquals('Z', valuesA[1]); + assertEquals('i', valuesA[2]); + + final char[] valuesB = playerB.values(); + assertEquals('O', valuesB[0]); + assertEquals(12, valuesB[1]); + assertEquals((char)-34, valuesB[2]); + assertEquals('s', valuesB[3]); + + final char[] valuesC = playerC.values(); + assertEquals((char)9, valuesC[0]); + assertEquals('9', valuesC[1]); + assertEquals((char)-9, valuesC[2]); + assertEquals('0', valuesC[3]); + assertEquals('X', valuesC[4]); + assertEquals('O', valuesC[5]); + } +} \ No newline at end of file diff --git a/game/src/test/java/org/toop/game/tictactoe/TicTacToeAITest.java b/game/src/test/java/org/toop/game/tictactoe/TicTacToeAITest.java new file mode 100644 index 0000000..a320631 --- /dev/null +++ b/game/src/test/java/org/toop/game/tictactoe/TicTacToeAITest.java @@ -0,0 +1,83 @@ +package org.toop.game.tictactoe; + +import org.toop.game.Game; + +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TicTacToeAITest { + private TicTacToe game; + private TicTacToeAI ai; + + @BeforeEach + void setup() { + game = new TicTacToe("AI", "AI"); + ai = new TicTacToeAI(); + } + + @Test + void testBestMove_returnWinningMoveWithDepth1() { + // X X - + // O O - + // - - - + game.play(new Game.Move(0, 'X')); + game.play(new Game.Move(3, 'O')); + game.play(new Game.Move(1, 'X')); + game.play(new Game.Move(4, 'O')); + + final Game.Move move = ai.findBestMove(game, 1); + + assertNotNull(move); + assertEquals('X', move.value()); + assertEquals(2, move.position()); + } + + @Test + void testBestMove_blockOpponentWinDepth1() { + // - - - + // O - - + // X X - + game.play(new Game.Move(6, 'X')); + game.play(new Game.Move(3, 'O')); + game.play(new Game.Move(7, 'X')); + + final Game.Move move = ai.findBestMove(game, 1); + + assertNotNull(move); + assertEquals('O', move.value()); + assertEquals(8, move.position()); + } + + @Test + void testBestMove_preferCornerOnEmpty() { + final Game.Move move = ai.findBestMove(game, 0); + + assertNotNull(move); + assertEquals('X', move.value()); + assertTrue(Set.of(0, 2, 6, 8).contains(move.position())); + } + + @Test + void testBestMove_findBestMoveDraw() { + // O X - + // - O X + // X O X + game.play(new Game.Move(1, 'X')); + game.play(new Game.Move(0, 'O')); + game.play(new Game.Move(5, 'X')); + game.play(new Game.Move(4, 'O')); + game.play(new Game.Move(6, 'X')); + game.play(new Game.Move(7, 'O')); + game.play(new Game.Move(8, 'X')); + + final Game.Move move = ai.findBestMove(game, game.getLegalMoves().length); + + assertNotNull(move); + assertEquals('O', move.value()); + assertEquals(2, move.position()); + } +} \ No newline at end of file diff --git a/game/src/test/java/org/toop/tictactoe/GameBaseTest.java b/game/src/test/java/org/toop/tictactoe/GameBaseTest.java deleted file mode 100644 index ff025c4..0000000 --- a/game/src/test/java/org/toop/tictactoe/GameBaseTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package java.org.toop.tictactoe; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.toop.game.GameBase; -import org.toop.game.Player; - -class GameBaseTest { - - private static class TestGame extends GameBase { - public TestGame(int size, Player p1, Player p2) { - super(size, p1, p2); - } - - @Override - public State play(int index) { - if (!isInside(index)) return State.INVALID; - grid[index] = getCurrentPlayer().getSymbol(); - // Just alternate players for testing - currentPlayer = (currentPlayer + 1) % 2; - return State.NORMAL; - } - } - - private GameBase game; - private Player player1; - private Player player2; - - @BeforeEach - void setUp() { - player1 = new Player("A", 'X'); - player2 = new Player("B", 'O'); - game = new TestGame(3, player1, player2); - } - - @Test - void testConstructor_initializesGridAndPlayers() { - assertEquals(3, game.getSize()); - assertEquals(9, game.getGrid().length); - - for (char c : game.getGrid()) { - assertEquals(GameBase.EMPTY, c); - } - - assertEquals(player1, game.getPlayers()[0]); - assertEquals(player2, game.getPlayers()[1]); - assertEquals(player1, game.getCurrentPlayer()); - } - - @Test - void testIsInside_returnsTrueForValidIndices() { - for (int i = 0; i < 9; i++) { - assertTrue(game.isInside(i)); - } - } - - @Test - void testIsInside_returnsFalseForInvalidIndices() { - assertFalse(game.isInside(-1)); - assertFalse(game.isInside(9)); - assertFalse(game.isInside(100)); - } - - @Test - void testPlay_alternatesPlayersAndMarksGrid() { - // First move - assertEquals(GameBase.State.NORMAL, game.play(0)); - assertEquals('X', game.getGrid()[0]); - assertEquals(player2, game.getCurrentPlayer()); - - // Second move - assertEquals(GameBase.State.NORMAL, game.play(1)); - assertEquals('O', game.getGrid()[1]); - assertEquals(player1, game.getCurrentPlayer()); - } - - @Test - void testPlay_invalidIndexReturnsInvalid() { - assertEquals(GameBase.State.INVALID, game.play(-1)); - assertEquals(GameBase.State.INVALID, game.play(9)); - } -} diff --git a/game/src/test/java/org/toop/tictactoe/PlayerTest.java b/game/src/test/java/org/toop/tictactoe/PlayerTest.java deleted file mode 100644 index ca6151a..0000000 --- a/game/src/test/java/org/toop/tictactoe/PlayerTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.toop.game.tictactoe; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.toop.game.Player; - -class PlayerTest { - - private Player playerA; - private Player playerB; - - @BeforeEach - void setup() { - playerA = new Player("testA", 'X'); - playerB = new Player("testB", 'O'); - } - - @Test - void testNameGetter_returnsTrueForValidName() { - assertEquals("testA", playerA.getName()); - assertEquals("testB", playerB.getName()); - } - - @Test - void testSymbolGetter_returnsTrueForValidSymbol() { - assertEquals('X', playerA.getSymbol()); - assertEquals('O', playerB.getSymbol()); - } -} diff --git a/game/src/test/java/org/toop/tictactoe/ai/MinMaxTicTacToeTest.java b/game/src/test/java/org/toop/tictactoe/ai/MinMaxTicTacToeTest.java deleted file mode 100644 index 9cb083f..0000000 --- a/game/src/test/java/org/toop/tictactoe/ai/MinMaxTicTacToeTest.java +++ /dev/null @@ -1,142 +0,0 @@ -package org.toop.game.tictactoe.ai; - -import static org.junit.jupiter.api.Assertions.*; - -import java.util.Set; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.toop.game.GameBase; -import org.toop.tictactoe.TicTacToe; - -/** Unit tests for MinMaxTicTacToe AI. */ -public class MinMaxTicTacToeTest { - - private MinMaxTicTacToe ai; - private TicTacToe game; - - @BeforeEach // called before every test is done to make it work - void setUp() { - ai = new MinMaxTicTacToe(); - game = new TicTacToe("AI", "Human"); - } - - @Test - void testBestMoveWinningMoveAvailable() { - // Setup board where AI can win immediately - // X = AI, O = player - // X | X | . - // O | O | . - // . | . | . - game.grid = - new char[] { - 'X', - 'X', - GameBase.EMPTY, - 'O', - 'O', - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY - }; - game.movesLeft = 4; - - int bestMove = ai.findBestMove(game); - - // Ai is expected to place at index 2 to win - assertEquals(2, bestMove); - } - - @Test - void testBestMoveBlocksOpponentWin() { - // Setup board where player could win next turn - // O | O | . - // X | . | . - // . | . | . - game.grid = - new char[] { - 'O', - 'O', - GameBase.EMPTY, - 'X', - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY - }; - - int bestMove = ai.findBestMove(game); - - // AI block at index 2 to continue the game - assertEquals(2, bestMove); - } - - @Test - void testBestMoveCornerPreferredOnEmptyBoard() { - // On empty board, center (index 4) is strongest - int bestMove = ai.findBestMove(game); - - assertTrue(Set.of(0, 2, 6, 8).contains(bestMove)); - } - - @Test - void testDoMinimaxScoresWinPositive() { - // Simulate a game state where AI has already won - TicTacToe copy = game.copyBoard(); - copy.grid = - new char[] { - 'X', - 'X', - 'X', - 'O', - 'O', - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY - }; - - int score = ai.doMinimax(copy, 5, false); - - assertTrue(score > 0, "AI win should yield positive score"); - } - - @Test - void testDoMinimaxScoresLossNegative() { - // Simulate a game state where human has already won - TicTacToe copy = game.copyBoard(); - copy.grid = - new char[] { - 'O', - 'O', - 'O', - 'X', - 'X', - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY, - GameBase.EMPTY - }; - - int score = ai.doMinimax(copy, 5, true); - - assertTrue(score < 0, "Human win should yield negative score"); - } - - @Test - void testDoMinimaxDrawReturnsZero() { - // Simulate a draw position - TicTacToe copy = game.copyBoard(); - copy.grid = - new char[] { - 'X', 'O', 'X', - 'X', 'O', 'O', - 'O', 'X', 'X' - }; - - int score = ai.doMinimax(copy, 0, true); - - assertEquals(0, score, "Draw should return 0 score"); - } -}