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

View File

@@ -1,11 +1,38 @@
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 Pribylski Simon
* @see ConsoleMain
@@ -14,29 +41,204 @@ import chess.view.consolerender.Colors;
*/
public class App {
public static void main(String[] args) {
System.out.println(Colors.RED + "Credits: Grenier Lilas, Pribylski Simon." + Colors.RESET);
System.out.println("""
Pick the version to use:
1 - Console
2 - Window
3 - 3D.""");
Scanner scan = new Scanner(System.in);
String line = scan.nextLine();
scan.close();
switch (line) {
case "1", "Console", "console":
ConsoleMain.main(args);
break;
case "2", "Window", "window":
SwingMain.main(args);
break;
case "3", "3D", "3d":
OpenGLMain.main(args);
break;
default:
System.out.println("Invalid input");
break;
private static final String version = "v1.1";
private final JFrame frame;
private final JPanel content;
private ButtonGroup viewSelection;
private JRadioButton consoleButton;
private JRadioButton swingButton;
private JRadioButton openglButton;
private final JRadioButton[] manualButtons = new JRadioButton[2];
private final JRadioButton[] dumbButtons = new JRadioButton[2];
private final JRadioButton[] hungryButtons = new JRadioButton[2];
private final JRadioButton[] alphaButtons = new JRadioButton[2];
private JCheckBox audioCheck;
private JCheckBox pgnCheck;
private void drawViewSelection() {
JPanel viewPanel = new JPanel();
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();
CommandExecutor commandExecutor = new CommandExecutor(game);
Window window = new Window(commandExecutor, true);
Window window = new Window(commandExecutor);
commandExecutor.addListener(window);
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;
}
public Color getColor() {
return color;
}
}

View File

@@ -124,4 +124,8 @@ public class CommandExecutor {
public void 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();
}
@Override
public List<GameListener> getListeners() {
return this.listeners;
}
}

View File

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

View File

@@ -1,5 +1,7 @@
package chess.controller.event;
import java.util.List;
/**
* 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 List<GameListener> getListeners();
public abstract void close();
}

View File

@@ -22,11 +22,12 @@ import chess.model.Color;
import chess.model.Coordinate;
import chess.model.Move;
import chess.model.Piece;
import chess.view.GameView;
import chess.view.DDDrender.world.BoardEntity;
import chess.view.DDDrender.world.PieceEntity;
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 WHITE = new Vector3f(1.0f, 1.0f, 1.0f);
@@ -409,8 +410,10 @@ public class DDDView extends GameAdapter implements CommandSender {
/**
* Run the game.
*/
@Override
public void run() {
this.window.run();
this.commandExecutor.close();
// free OpenGL resources
try {
@@ -541,6 +544,8 @@ public class DDDView extends GameAdapter implements CommandSender {
@Override
public void onPromotePawn(Coordinate pieceCoords) {
if (hasAIAttached(getPieceAt(pieceCoords).getColor()))
return;
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.Move;
import chess.model.Piece;
import chess.view.GameView;
import java.util.List;
import java.util.Objects;
@@ -21,7 +22,7 @@ import java.util.concurrent.Executors;
* Console renderer.
*/
public class Console extends GameAdapter implements CommandSender {
public class Console extends GameView {
private final Scanner scanner = new Scanner(System.in);
private final CommandExecutor commandExecutor;
private final ConsolePieceName consolePieceName = new ConsolePieceName();
@@ -365,5 +366,9 @@ public class Console extends GameAdapter implements CommandSender {
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.model.Coordinate;
import chess.model.Move;
import chess.model.Piece;
import chess.view.GameView;
/**
* Window for the 2D chess game.
*/
public class Window extends JFrame implements GameListener, CommandSender {
public class Window extends GameView {
private final CommandExecutor commandExecutor;
private Coordinate lastClick = null;
private final JFrame frame;
private final JLabel cells[][];
private final JLabel displayText;
private final JButton castlingButton = new JButton("Roque");
private final JButton bigCastlingButton = new JButton("Grand Roque");
private final JButton undoButton = new JButton("Annuler le coup précédent");
private final boolean showPopups;
public Window(final CommandExecutor commandExecutor, boolean showPopups) {
public Window(final CommandExecutor commandExecutor) {
this.frame = new JFrame();
this.cells = new JLabel[8][8];
this.displayText = new JLabel();
this.commandExecutor = commandExecutor;
this.showPopups = showPopups;
setSize(800, 910);
setVisible(true);
setLocationRelativeTo(null);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
addWindowListener(new WindowAdapter() {
this.frame.setSize(800, 910);
this.frame.setVisible(true);
this.frame.setLocationRelativeTo(null);
this.frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
this.frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
commandExecutor.close();
@@ -99,7 +101,7 @@ public class Window extends JFrame implements GameListener, CommandSender {
content.add(grid);
content.add(bottom);
setContentPane(content);
this.frame.setContentPane(content);
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 8; x++) {
@@ -213,39 +215,37 @@ public class Window extends JFrame implements GameListener, CommandSender {
@Override
public void onWin(chess.model.Color color) {
JOptionPane.showMessageDialog(this, "Victory of " + color);
JOptionPane.showMessageDialog(this.frame, "Victory of " + color);
}
@Override
public void onKingInCheck() {
if (!showPopups)
return;
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this, "Check!");
JOptionPane.showMessageDialog(this.frame, "Check!");
});
}
@Override
public void onKingInMat() {
SwingUtilities.invokeLater(() -> {
JOptionPane.showMessageDialog(this, "Checkmate!");
JOptionPane.showMessageDialog(this.frame, "Checkmate!");
});
}
@Override
public void onPatSituation() {
JOptionPane.showMessageDialog(this, "Pat. It's a draw!");
JOptionPane.showMessageDialog(this.frame, "Pat. It's a draw!");
}
@Override
public void onSurrender(chess.model.Color color) {
JOptionPane.showMessageDialog(this, color + " has surrendered.");
JOptionPane.showMessageDialog(this.frame, color + " has surrendered.");
}
@Override
public void onGameEnd() {
JOptionPane.showMessageDialog(this, "End of the game");
this.dispose();
JOptionPane.showMessageDialog(this.frame, "End of the game");
this.frame.dispose();
this.commandExecutor.close();
}
@@ -259,8 +259,13 @@ public class Window extends JFrame implements GameListener, CommandSender {
*/
@Override
public void onPromotePawn(Coordinate pieceCoords) {
if (!showPopups)
Piece pawn = getPieceAt(pieceCoords);
chess.model.Color player = pawn.getColor();
if (hasAIAttached(player))
return;
SwingUtilities.invokeLater(() -> {
String result = null;
@@ -273,7 +278,7 @@ public class Window extends JFrame implements GameListener, CommandSender {
while (result == null || result.isEmpty()) {
result = (String) JOptionPane.showInputDialog(
this,
this.frame,
"Choose the type of piece to upgrade the pawn",
"Promote Dialog",
JOptionPane.PLAIN_MESSAGE,
@@ -301,9 +306,6 @@ public class Window extends JFrame implements GameListener, CommandSender {
updateBoard();
}
@Override
public void onMove(Move move, boolean captured) {}
@Override
public void onMoveNotAllowed(Move move) {
drawInvalid(move);
@@ -311,18 +313,16 @@ public class Window extends JFrame implements GameListener, CommandSender {
@Override
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
public CommandExecutor getCommandExecutor() {
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