7 Commits
v1.0 ... main

Author SHA1 Message Date
cd1a375ed9 update README
All checks were successful
Linux arm64 / Build (push) Successful in 49s
2025-05-26 21:50:51 +02:00
3956f55c59 add screenshots 2025-05-26 21:50:45 +02:00
24e0fbc84c gradle: add deps in jar
All checks were successful
Linux arm64 / Build (push) Successful in 42s
2025-05-26 21:42:52 +02:00
86ea62614b feat: add app chooser
All checks were successful
Linux arm64 / Build (push) Successful in 43s
2025-05-26 21:33:02 +02:00
9d65432eff build: auto-detect os
All checks were successful
Linux arm64 / Build (push) Successful in 1m6s
2025-05-26 20:11:12 +02:00
f0efdf4105 Update README.md
All checks were successful
Linux arm64 / Build (push) Successful in 48s
2025-05-19 09:08:50 +00:00
e9a9916d9d add README
All checks were successful
Linux arm64 / Build (push) Successful in 47s
2025-05-19 09:01:44 +00:00
16 changed files with 371 additions and 64 deletions

42
README.md Normal file
View File

@@ -0,0 +1,42 @@
# 3DChess ♟
Super chess engine. Outputs to console, Java Swing and 3D (Lwjgl) !
## Features 🌟
- Views:
- Console
- Java Swing
- OpengGL (3D)
- Audio
- Pgn support (import/export)
- Undo moves support
- AIs:
- DumbAI: just makes some random moves
- HungryAI: tries to eat the biggest piece (no depth, no thinking)
- AlphaBetaAI: plays the best move by searching through all possible moves
## Screenshots 🖼
[[screenshots/select_view.png]]
[[screenshots/2d_view.png]]
[[screenshots/3d_view.png]]
## Binaries 📦
Precompiled binaries can be found [here](https://git.ale-pri.com/Crabs/3DChess/releases)
## Run 🏃
```
./gradlew run
```
## Run tests 🗣️🔥
```
./gradlew test
```

View File

@@ -16,7 +16,7 @@ repositories {
mavenCentral() mavenCentral()
} }
def os = "linux"; def os = "linux"
def lwjgl_version = "3.3.6" def lwjgl_version = "3.3.6"
def lwjgl_natives = "natives-$os" def lwjgl_natives = "natives-$os"
def imgui_version = "1.87.0" def imgui_version = "1.87.0"
@@ -47,12 +47,20 @@ application {
applicationName = "3DChess" applicationName = "3DChess"
} }
// Add libraries into the final jar
jar { jar {
archiveBaseName = rootProject.getName() + "-" + os
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
manifest { manifest {
attributes 'Main-Class': application.mainClass attributes "Main-Class": application.mainClass
}
from {
configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) }
} }
} }
run { run {
standardInput = System.in standardInput = System.in
} }

View File

@@ -1,11 +1,38 @@
package chess; package chess;
import java.util.Scanner; import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import chess.view.consolerender.Colors; import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import chess.ai.AI;
import chess.ai.ais.AlphaBetaAI;
import chess.ai.ais.DumbAI;
import chess.ai.ais.HungryAI;
import chess.ai.alphabeta.AlphaBetaConsolePrinter;
import chess.controller.CommandExecutor;
import chess.controller.commands.NewGameCommand;
import chess.controller.event.GameAdapter;
import chess.model.Color;
import chess.model.Game;
import chess.pgn.PgnExport;
import chess.view.GameView;
import chess.view.DDDrender.DDDView;
import chess.view.audio.GameAudio;
import chess.view.consolerender.Console;
import chess.view.simplerender.Window;
/** /**
* Main class of the chess game. Asks the user for the version to use and runs the according main. * Main class of the chess game. Asks the user for the version to use and runs
* the according main.
*
* @author Grenier Lilas * @author Grenier Lilas
* @author Pribylski Simon * @author Pribylski Simon
* @see ConsoleMain * @see ConsoleMain
@@ -14,29 +41,204 @@ import chess.view.consolerender.Colors;
*/ */
public class App { public class App {
public static void main(String[] args) {
System.out.println(Colors.RED + "Credits: Grenier Lilas, Pribylski Simon." + Colors.RESET); private static final String version = "v1.1";
System.out.println("""
Pick the version to use: private final JFrame frame;
1 - Console private final JPanel content;
2 - Window
3 - 3D."""); private ButtonGroup viewSelection;
Scanner scan = new Scanner(System.in); private JRadioButton consoleButton;
String line = scan.nextLine(); private JRadioButton swingButton;
scan.close(); private JRadioButton openglButton;
switch (line) {
case "1", "Console", "console": private final JRadioButton[] manualButtons = new JRadioButton[2];
ConsoleMain.main(args); private final JRadioButton[] dumbButtons = new JRadioButton[2];
break; private final JRadioButton[] hungryButtons = new JRadioButton[2];
case "2", "Window", "window": private final JRadioButton[] alphaButtons = new JRadioButton[2];
SwingMain.main(args);
break; private JCheckBox audioCheck;
case "3", "3D", "3d": private JCheckBox pgnCheck;
OpenGLMain.main(args);
break; private void drawViewSelection() {
default: JPanel viewPanel = new JPanel();
System.out.println("Invalid input");
break; JLabel text = new JLabel("Display mode: ");
viewPanel.add(text);
viewSelection = new ButtonGroup();
consoleButton = new JRadioButton("Console");
viewPanel.add(consoleButton);
viewSelection.add(consoleButton);
swingButton = new JRadioButton("2D Window");
viewPanel.add(swingButton);
viewSelection.add(swingButton);
openglButton = new JRadioButton("3D Window");
viewPanel.add(openglButton);
viewSelection.add(openglButton);
openglButton.setSelected(true);
content.add(viewPanel);
} }
private void drawPlayer(Color color) {
JPanel playerContent = new JPanel();
JLabel text = new JLabel(color + ": ");
playerContent.add(text);
ButtonGroup playerSelection = new ButtonGroup();
int playerIndex = color.ordinal();
JRadioButton manualButton = new JRadioButton("Manual");
playerContent.add(manualButton);
playerSelection.add(manualButton);
manualButton.setSelected(true);
JRadioButton dumbButton = new JRadioButton("DumbAI");
playerContent.add(dumbButton);
playerSelection.add(dumbButton);
JRadioButton hungryButton = new JRadioButton("HungryAI");
playerContent.add(hungryButton);
playerSelection.add(hungryButton);
JRadioButton alphaButton = new JRadioButton("AlphaBetaAI");
playerContent.add(alphaButton);
playerSelection.add(alphaButton);
content.add(playerContent);
manualButtons[playerIndex] = manualButton;
dumbButtons[playerIndex] = dumbButton;
hungryButtons[playerIndex] = hungryButton;
alphaButtons[playerIndex] = alphaButton;
}
private void drawFooter() {
JPanel footerArea = new JPanel();
this.audioCheck = new JCheckBox("Audio");
footerArea.add(this.audioCheck);
this.pgnCheck = new JCheckBox("PGN export");
footerArea.add(this.pgnCheck);
JButton startButton = new JButton("Start");
startButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
startGame();
}
});
footerArea.add(startButton);
content.add(footerArea);
}
public App() {
frame = new JFrame();
frame.setTitle("3DChess " + version);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
content = new JPanel();
content.setLayout(new BoxLayout(content, BoxLayout.PAGE_AXIS));
frame.setContentPane(content);
frame.setLocationRelativeTo(null);
frame.setSize(500, 500);
drawViewSelection();
drawPlayer(Color.White);
drawPlayer(Color.Black);
drawFooter();
frame.setVisible(true);
}
private GameView getViewSelection(CommandExecutor commandExecutor) {
if (consoleButton.isSelected())
return new Console(commandExecutor);
if (swingButton.isSelected())
return new Window(commandExecutor);
return new DDDView(commandExecutor);
}
private AI getAISelection(CommandExecutor commandExecutor, Color color) {
int playerIndex = color.ordinal();
if (dumbButtons[playerIndex].isSelected())
return new DumbAI(commandExecutor, color);
if (hungryButtons[playerIndex].isSelected())
return new HungryAI(commandExecutor, color);
if (alphaButtons[playerIndex].isSelected()) {
AlphaBetaAI ai = new AlphaBetaAI(commandExecutor, color, 5);
AlphaBetaConsolePrinter printer = new AlphaBetaConsolePrinter(ai);
printer.connect();
return ai;
}
return null;
}
private void addAI(CommandExecutor commandExecutor, Color color) {
AI ai = getAISelection(commandExecutor, color);
if (ai == null)
return;
commandExecutor.addListener(ai);
}
private void addOptionalAudio(CommandExecutor commandExecutor) {
if (this.audioCheck.isSelected())
commandExecutor.addListener(new GameAudio());
}
private void addOptionalPgnExport(CommandExecutor commandExecutor, Game game) {
if (!this.pgnCheck.isSelected())
return;
commandExecutor.addListener(new GameAdapter() {
@Override
public void onGameEnd() {
System.out.println(PgnExport.exportGame(game));
}
});
}
private void addAIs(CommandExecutor commandExecutor) {
addAI(commandExecutor, Color.White);
addAI(commandExecutor, Color.Black);
}
private GameView addView(CommandExecutor commandExecutor) {
GameView view = getViewSelection(commandExecutor);
commandExecutor.addListener(view);
return view;
}
private void startGame() {
Game game = new Game();
CommandExecutor commandExecutor = new CommandExecutor(game);
GameView view = addView(commandExecutor);
addAIs(commandExecutor);
addOptionalAudio(commandExecutor);
addOptionalPgnExport(commandExecutor, game);
commandExecutor.executeCommand(new NewGameCommand());
frame.dispose();
view.run();
}
public static void main(String[] args) {
new App();
} }
} }

View File

@@ -19,7 +19,7 @@ public class SwingMain {
Game game = new Game(); Game game = new Game();
CommandExecutor commandExecutor = new CommandExecutor(game); CommandExecutor commandExecutor = new CommandExecutor(game);
Window window = new Window(commandExecutor, true); Window window = new Window(commandExecutor);
commandExecutor.addListener(window); commandExecutor.addListener(window);
AlphaBetaAI ai = new AlphaBetaAI(commandExecutor, Color.Black, 5); AlphaBetaAI ai = new AlphaBetaAI(commandExecutor, Color.Black, 5);

View File

@@ -38,4 +38,8 @@ public abstract class AI extends GameAdapter implements AIActions {
return this.commandExecutor; return this.commandExecutor;
} }
public Color getColor() {
return color;
}
} }

View File

@@ -124,4 +124,8 @@ public class CommandExecutor {
public void close() { public void close() {
this.dispatcher.close(); this.dispatcher.close();
} }
public List<GameListener> getListeners() {
return this.dispatcher.getListeners();
}
} }

View File

@@ -113,4 +113,9 @@ public class AsyncGameDispatcher extends GameDispatcher {
this.executor.shutdown(); this.executor.shutdown();
} }
@Override
public List<GameListener> getListeners() {
return this.listeners;
}
} }

View File

@@ -1,5 +1,7 @@
package chess.controller.event; package chess.controller.event;
import java.util.List;
import chess.controller.commands.PromoteCommand.PromoteType; import chess.controller.commands.PromoteCommand.PromoteType;
import chess.model.Color; import chess.model.Color;
import chess.model.Coordinate; import chess.model.Coordinate;
@@ -78,4 +80,9 @@ public class EmptyGameDispatcher extends GameDispatcher {
public void close() { public void close() {
} }
@Override
public List<GameListener> getListeners() {
return List.of();
}
} }

View File

@@ -1,5 +1,7 @@
package chess.controller.event; package chess.controller.event;
import java.util.List;
/** /**
* Abstract class, provides a dispatcher for game events. * Abstract class, provides a dispatcher for game events.
*/ */
@@ -7,6 +9,8 @@ public abstract class GameDispatcher extends GameAdapter {
public abstract void addListener(GameListener listener); public abstract void addListener(GameListener listener);
public abstract List<GameListener> getListeners();
public abstract void close(); public abstract void close();
} }

View File

@@ -22,11 +22,12 @@ import chess.model.Color;
import chess.model.Coordinate; import chess.model.Coordinate;
import chess.model.Move; import chess.model.Move;
import chess.model.Piece; import chess.model.Piece;
import chess.view.GameView;
import chess.view.DDDrender.world.BoardEntity; import chess.view.DDDrender.world.BoardEntity;
import chess.view.DDDrender.world.PieceEntity; import chess.view.DDDrender.world.PieceEntity;
import chess.view.DDDrender.world.World; import chess.view.DDDrender.world.World;
public class DDDView extends GameAdapter implements CommandSender { public class DDDView extends GameView {
private static final Vector3f BLACK = new Vector3f(0.3f, 0.3f, 0.3f); private static final Vector3f BLACK = new Vector3f(0.3f, 0.3f, 0.3f);
private static final Vector3f WHITE = new Vector3f(1.0f, 1.0f, 1.0f); private static final Vector3f WHITE = new Vector3f(1.0f, 1.0f, 1.0f);
@@ -409,8 +410,10 @@ public class DDDView extends GameAdapter implements CommandSender {
/** /**
* Run the game. * Run the game.
*/ */
@Override
public void run() { public void run() {
this.window.run(); this.window.run();
this.commandExecutor.close();
// free OpenGL resources // free OpenGL resources
try { try {
@@ -541,6 +544,8 @@ public class DDDView extends GameAdapter implements CommandSender {
@Override @Override
public void onPromotePawn(Coordinate pieceCoords) { public void onPromotePawn(Coordinate pieceCoords) {
if (hasAIAttached(getPieceAt(pieceCoords).getColor()))
return;
openPopup("Promotion"); openPopup("Promotion");
} }

View File

@@ -0,0 +1,21 @@
package chess.view;
import chess.ai.AI;
import chess.controller.CommandSender;
import chess.controller.event.GameAdapter;
import chess.controller.event.GameListener;
import chess.model.Color;
public abstract class GameView extends GameAdapter implements CommandSender{
public abstract void run();
public boolean hasAIAttached(Color color) {
for (GameListener listener : getCommandExecutor().getListeners()) {
if (listener instanceof AI ai) {
if (ai.getColor() == color)
return true;
}
}
return false;
}
}

View File

@@ -10,6 +10,7 @@ import chess.model.Color;
import chess.model.Coordinate; import chess.model.Coordinate;
import chess.model.Move; import chess.model.Move;
import chess.model.Piece; import chess.model.Piece;
import chess.view.GameView;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -21,7 +22,7 @@ import java.util.concurrent.Executors;
* Console renderer. * Console renderer.
*/ */
public class Console extends GameAdapter implements CommandSender { public class Console extends GameView {
private final Scanner scanner = new Scanner(System.in); private final Scanner scanner = new Scanner(System.in);
private final CommandExecutor commandExecutor; private final CommandExecutor commandExecutor;
private final ConsolePieceName consolePieceName = new ConsolePieceName(); private final ConsolePieceName consolePieceName = new ConsolePieceName();
@@ -365,5 +366,9 @@ public class Console extends GameAdapter implements CommandSender {
return this.commandExecutor; return this.commandExecutor;
} }
@Override
public void run() {
}
} }

View File

@@ -23,34 +23,36 @@ import chess.controller.commands.PromoteCommand.PromoteType;
import chess.controller.event.GameListener; import chess.controller.event.GameListener;
import chess.model.Coordinate; import chess.model.Coordinate;
import chess.model.Move; import chess.model.Move;
import chess.model.Piece;
import chess.view.GameView;
/** /**
* Window for the 2D chess game. * Window for the 2D chess game.
*/ */
public class Window extends JFrame implements GameListener, CommandSender { public class Window extends GameView {
private final CommandExecutor commandExecutor; private final CommandExecutor commandExecutor;
private Coordinate lastClick = null; private Coordinate lastClick = null;
private final JFrame frame;
private final JLabel cells[][]; private final JLabel cells[][];
private final JLabel displayText; private final JLabel displayText;
private final JButton castlingButton = new JButton("Roque"); private final JButton castlingButton = new JButton("Roque");
private final JButton bigCastlingButton = new JButton("Grand Roque"); private final JButton bigCastlingButton = new JButton("Grand Roque");
private final JButton undoButton = new JButton("Annuler le coup précédent"); private final JButton undoButton = new JButton("Annuler le coup précédent");
private final boolean showPopups; public Window(final CommandExecutor commandExecutor) {
this.frame = new JFrame();
public Window(final CommandExecutor commandExecutor, boolean showPopups) {
this.cells = new JLabel[8][8]; this.cells = new JLabel[8][8];
this.displayText = new JLabel(); this.displayText = new JLabel();
this.commandExecutor = commandExecutor; this.commandExecutor = commandExecutor;
this.showPopups = showPopups; this.frame.setSize(800, 910);
setSize(800, 910); this.frame.setVisible(true);
setVisible(true); this.frame.setLocationRelativeTo(null);
setLocationRelativeTo(null); this.frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
setDefaultCloseOperation(DISPOSE_ON_CLOSE); this.frame.addWindowListener(new WindowAdapter() {
addWindowListener(new WindowAdapter() {
@Override @Override
public void windowClosed(WindowEvent e) { public void windowClosed(WindowEvent e) {
commandExecutor.close(); commandExecutor.close();
@@ -99,7 +101,7 @@ public class Window extends JFrame implements GameListener, CommandSender {
content.add(grid); content.add(grid);
content.add(bottom); content.add(bottom);
setContentPane(content); this.frame.setContentPane(content);
for (int y = 0; y < 8; y++) { for (int y = 0; y < 8; y++) {
for (int x = 0; x < 8; x++) { for (int x = 0; x < 8; x++) {
@@ -213,39 +215,37 @@ public class Window extends JFrame implements GameListener, CommandSender {
@Override @Override
public void onWin(chess.model.Color color) { public void onWin(chess.model.Color color) {
JOptionPane.showMessageDialog(this, "Victory of " + color); JOptionPane.showMessageDialog(this.frame, "Victory of " + color);
} }
@Override @Override
public void onKingInCheck() { public void onKingInCheck() {
if (!showPopups)
return;
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this, "Check!"); JOptionPane.showMessageDialog(this.frame, "Check!");
}); });
} }
@Override @Override
public void onKingInMat() { public void onKingInMat() {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this, "Checkmate!"); JOptionPane.showMessageDialog(this.frame, "Checkmate!");
}); });
} }
@Override @Override
public void onPatSituation() { public void onPatSituation() {
JOptionPane.showMessageDialog(this, "Pat. It's a draw!"); JOptionPane.showMessageDialog(this.frame, "Pat. It's a draw!");
} }
@Override @Override
public void onSurrender(chess.model.Color color) { public void onSurrender(chess.model.Color color) {
JOptionPane.showMessageDialog(this, color + " has surrendered."); JOptionPane.showMessageDialog(this.frame, color + " has surrendered.");
} }
@Override @Override
public void onGameEnd() { public void onGameEnd() {
JOptionPane.showMessageDialog(this, "End of the game"); JOptionPane.showMessageDialog(this.frame, "End of the game");
this.dispose(); this.frame.dispose();
this.commandExecutor.close(); this.commandExecutor.close();
} }
@@ -259,8 +259,13 @@ public class Window extends JFrame implements GameListener, CommandSender {
*/ */
@Override @Override
public void onPromotePawn(Coordinate pieceCoords) { public void onPromotePawn(Coordinate pieceCoords) {
if (!showPopups)
Piece pawn = getPieceAt(pieceCoords);
chess.model.Color player = pawn.getColor();
if (hasAIAttached(player))
return; return;
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
String result = null; String result = null;
@@ -273,7 +278,7 @@ public class Window extends JFrame implements GameListener, CommandSender {
while (result == null || result.isEmpty()) { while (result == null || result.isEmpty()) {
result = (String) JOptionPane.showInputDialog( result = (String) JOptionPane.showInputDialog(
this, this.frame,
"Choose the type of piece to upgrade the pawn", "Choose the type of piece to upgrade the pawn",
"Promote Dialog", "Promote Dialog",
JOptionPane.PLAIN_MESSAGE, JOptionPane.PLAIN_MESSAGE,
@@ -301,9 +306,6 @@ public class Window extends JFrame implements GameListener, CommandSender {
updateBoard(); updateBoard();
} }
@Override
public void onMove(Move move, boolean captured) {}
@Override @Override
public void onMoveNotAllowed(Move move) { public void onMoveNotAllowed(Move move) {
drawInvalid(move); drawInvalid(move);
@@ -311,18 +313,16 @@ public class Window extends JFrame implements GameListener, CommandSender {
@Override @Override
public void onDraw() { public void onDraw() {
JOptionPane.showMessageDialog(this, "Same position was repeated three times. It's a draw!"); JOptionPane.showMessageDialog(this.frame, "Same position was repeated three times. It's a draw!");
} }
@Override
public void onCastling(boolean bigCastling, Move kingMove, Move rookMove) {}
@Override
public void onPawnPromoted(PromoteType promotion, Coordinate coordinate) {}
@Override @Override
public CommandExecutor getCommandExecutor() { public CommandExecutor getCommandExecutor() {
return this.commandExecutor; return this.commandExecutor;
} }
@Override
public void run() {
}
} }

BIN
screenshots/2d_view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
screenshots/3d_view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

BIN
screenshots/select_view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB