This commit is contained in:
Felix Steghofer
2022-12-04 01:25:28 +01:00
commit 3db4f94e6a
31 changed files with 2762 additions and 0 deletions

121
Reversi/Board.java Normal file
View File

@@ -0,0 +1,121 @@
package reversi.model;
/**
* Interface for a Reversi game, also known as Othello.
*
* A human plays against the machine.
*/
public interface Board extends Cloneable {
/**
* The number of rows and columns of the game grid. Originally 8.
* Here, even and at least 4.
*/
int SIZE = 8;
/**
* Get the player who should start or already has started the game.
*
* @return The player who makes the initial move.
*/
Player getFirstPlayer();
/**
* Get the player who owns the next game turn.
*
* @return The player who is allowed to make the next turn.
*/
Player next();
/**
* Execute a human move.
*
* @param row The slot's row number where a tile of the human player should
* be placed on.
* @param col The slot's column number where a tile of the human player
* should be placed on.
* @return {@code true} if and only if move was successful, e.g., the
* defined slot was not occupied and at least one tile of the
* machine was reversed.
* @throws IllegalMoveException If the game is already over, or it is not
* the human's turn.
* @throws IllegalArgumentException If the provided parameters are
* invalid, e.g., the defined slot was is not on the grid.
*/
boolean move(int row, int col);
/**
* Execute a machine move.
*
* @throws IllegalMoveException If the game is already over, or it is not
* the machine's turn.
*/
void machineMove();
/**
* Set the skill level of the machine.
*
* @param level The skill as a number, must be at least 1.
*/
void setLevel(int level);
/**
* Check if the game is over. Either one player has won or there is a tie,
* i.e., no player can perform a move any more.
*
* @return {@code true} if and only if the game is over.
*/
boolean gameOver();
/**
* Check if the game state is won.
*
* @return The winner or nobody in case of a tie.
*/
Player getWinner();
/**
* Get the number of human tiles currently placed on the grid.
*
* @return The number of human tiles.
*/
int getNumberOfHumanTiles();
/**
* Get the number of machine tiles currently placed on the grid.
*
* @return The number of machine tiles.
*/
int getNumberOfMachineTiles();
/**
* Get the content of the slot at the specified coordinates. Either it
* contains a tile of one of the two players already or it is empty.
*
* @param row The row of the slot in the game grid.
* @param col The column of the slot in the game grid.
* @return The slot's content.
*/
Player getSlot(int row, int col);
/**
* Deep-copy the board.
*
* @return A clone.
*/
Board clone();
/**
* Get the string representation of this board as row x column matrix. Each
* slot is represented by one the three chars '.', 'X', or 'O'. '.' means
* that the slot currently contains no tile. 'X' means that it contains a
* tile of the human player. 'O' means that it contains a machine tile. In
* contrast to the rows, the columns are whitespace separated.
*
* @return The string representation of the current Reversi game.
*/
@Override
String toString();
}

42
Reversi/Field.java Normal file
View File

@@ -0,0 +1,42 @@
package reversi.model;
/**
* Class for a reversi field.
*/
public class Field {
/**
* <code>Row</code> of the field.
*/
private int row;
/**
* <code>Column</code> of the field.
*/
private int col;
/**
* Constructs a new field.
*
* @param row row of the field
* @param col column of the field
*/
public Field(final int row, final int col) {
this.row = row;
this.col = col;
}
/**
*
* @return The row of the field.
*/
public final int row() {
return row;
}
/**
*
* @return the column of the field.
*/
public final int col() {
return col;
}
}

164
Reversi/GameNode.java Normal file
View File

@@ -0,0 +1,164 @@
package reversi.model;
import java.util.LinkedList;
/**
* Class for a gamenode. Is needed to build a gametree for e.g. minmax
* algorithm. A node represents a virtual move.
*
*/
public class GameNode {
/**
* Reversi-board of the node.
*/
private Reversi state;
/**
* Scoring of the node.
*/
private double score;
/**
* The Move.
*/
private Field move;
/**
* Children of the node.
*/
private LinkedList<GameNode> children;
/**
* Constructor for the root node.
*
* @param move Root move
* @param state Board of the root node
*/
GameNode(final Field move, final Reversi state) {
this.move = move;
this.state = state;
this.children = new LinkedList<GameNode>();
}
/**
* Constructor for node.
*
* @param move Move of node
* @param state Board of node
* @param parent Parent of node
*/
GameNode(final Field move, final Reversi state, final GameNode parent) {
this(move, state);
parent.children.add(this);
}
/**
*
* @param score Score to set
*/
public final void setScore(final double score) {
this.score = score;
}
/**
*
* @return Score
*/
public final double getScore() {
return score;
}
/**
*
* @return Board
*/
public final Reversi getState() {
return state;
}
/**
*
* @return Move
*/
public final Field getMove() {
return move;
}
/**
*
* @return Children
*/
public final LinkedList<GameNode> getChildren() {
return children;
}
/**
* Is needed to calculate the combined score of the parent node.
*
* @return GameNode with highest score
*/
public final GameNode getChildrensHighestScore() {
GameNode highest = children.getFirst();
for (GameNode node : children) {
if (node.getScore() > highest.getScore()) {
highest = node;
}
}
return highest;
}
/**
* Is needed to calculate the combined score of the parent node.
*
* @return GameNode with lowest score
*/
public final GameNode getChildrensLowestScore() {
GameNode lowest = children.getFirst();
for (GameNode node : children) {
if (node.getScore() < lowest.getScore()) {
lowest = node;
}
}
return lowest;
}
/**
*
* @return If node has at least one child
*/
public final boolean hasChild() {
return (children.size() > 0);
}
/**
* Set children of a node.
*/
public final void fillNode() {
for (Field move : generateChildren()) {
Reversi tmpBoard = this.getState().clone();
tmpBoard.performMove(move.row(), move.col());
new GameNode(move, tmpBoard, this);
}
}
/**
*
* @return <code>Linkedlist</code> with possible moves in the
* current reversi-board.
*/
private LinkedList<Field> generateChildren() {
LinkedList<Field> movesList = new LinkedList<Field>();
for (int row = 0; row < Board.SIZE; row++) {
for (int col = 0; col < Board.SIZE; col++) {
if (getState().isValidMove(row, col)) {
movesList.add(new Field(row, col));
}
}
}
return movesList;
}
}

75
Reversi/GameTree.java Normal file
View File

@@ -0,0 +1,75 @@
package reversi.model;
/**
* Class for a GameTree.
*/
public class GameTree {
/**
* Root gamenode of the gametree.
*/
private GameNode root;
/**
* Constructs a new gametree.
*
* @param board of the root node
* @param level depth of the tree
*/
GameTree(final Reversi board, final int level) {
this.root = new GameNode(new Field(0, 0), board.clone());
fillTree(root, level);
generateScores(root);
}
/**
*
* @return Root of the gametree.
*/
public final GameNode getRoot() {
return root;
}
/**
* Fill the Tree with nodes.
*
* @param node Node to fill (in most cases root gamenode)
* @param depth depth of the tree (level)
*/
private void fillTree(final GameNode node, final int depth) {
if (depth > 0) {
node.fillNode();
for (GameNode n : node.getChildren()) {
fillTree(n, (depth - 1));
}
}
}
/**
* Generates and sets the score of a Gamenode.
*
* @param node Gamenode to set
*/
public final void generateScores(final GameNode node) {
Reversi currentBoard = node.getState();
if (!node.hasChild()) {
node.setScore(currentBoard.score());
} else {
double score;
score = currentBoard.score();
for (GameNode child : node.getChildren()) {
generateScores(child);
}
if (!currentBoard.next().getType()) {
score += node.getChildrensHighestScore().getScore();
} else {
score += node.getChildrensLowestScore().getScore();
}
node.setScore(score);
}
}
}

View File

@@ -0,0 +1,31 @@
package reversi.model;
import java.awt.Toolkit;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
/**
* Class for a Illegal move in reversi game.
*/
public class IllegalMoveException extends RuntimeException {
private static final long serialVersionUID = 240330120795325167L;
/**
* Prints an error or plays a "beep" if a illegal move is made.
* A beep is more common but not so easy to implement for various
* OS, so the sound is only played on Windows XP or 7.
*
* @param s Error to print
*/
public IllegalMoveException(final String s) {
if (System.getProperty("os.name").equals("Windows 7")
|| System.getProperty("os.name").equals("Windows XP")) {
Toolkit.getDefaultToolkit().beep();
} else {
JFrame screen = new JFrame();
JOptionPane.showMessageDialog(screen, s);
}
}
}

View File

@@ -0,0 +1,42 @@
package reversi.controller;
import reversi.model.Player;
import reversi.model.Reversi;
import reversi.view.ReversiView;
/**
* Creates a new thread for the machine move.
*/
public class MachineMoveThread extends Thread {
private Reversi model;
private ReversiView view;
private Player bot;
MachineMoveThread(final Reversi model, final ReversiView view) {
this.model = model;
this.view = view;
this.bot = model.next();
}
@Override
public final void run() {
this.model.machineMove();
javax.swing.SwingUtilities.invokeLater(new Runnable() {
@Override
public final void run() {
view.updatePlayField(model);
view.setMovePossible(true);
}
});
if (model.next() == bot && !model.gameOver()) {
this.run();
javax.swing.SwingUtilities.invokeLater(new Runnable() {
@Override
public final void run() {
view.viewMessage("Human has to miss a turn");
}
});
}
}
}

27
Reversi/Player.java Normal file
View File

@@ -0,0 +1,27 @@
package reversi.model;
/**
* Class for a player of reversi.
*/
public class Player {
/**
* Indicates if a player is human.
*/
private Boolean isHuman;
/**
* Constructs a new player.
*
* @param isHuman is the player human?
*/
public Player(final boolean isHuman) {
this.isHuman = isHuman;
}
/**
*
* @return Is player human?
*/
public final boolean getType() {
return isHuman;
}
}

527
Reversi/Reversi.java Normal file
View File

@@ -0,0 +1,527 @@
package reversi.model;
/**
* Class for reversi game. A human player tries to beat a bot with modifiable
* difficulty level.
*/
public class Reversi implements Board {
private Player bot;
private Player human;
/**
* Player with the next move.
*/
private Player currentPlayer;
private Player firstPlayer;
/**
* Playboard saves the tiles.
*/
private Player[][] playBoard;
/**
* Gametree for the bot to calculate future moves.
*/
private GameTree gameTree;
/**
* Vectors to traverse the 8 directions around a tile.
*/
private Field[] vectors;
/**
* Saves if game is over.
*/
private boolean gameOver;
/**
* Standard weighting of the playboard. Used by the bot to calculate
* gametree.
*/
private double[][] weighting = new double[][]
{{9999, 5, 500, 200, 200, 500, 5, 9999},
{5, 1, 50, 150, 150, 50, 1, 5},
{500, 50, 250, 100, 100, 250, 50, 500},
{200, 150, 100, 50, 50, 100, 150, 200},
{200, 150, 100, 50, 50, 100, 150, 200},
{500, 50, 250, 100, 100, 250, 50, 500},
{5, 1, 50, 150, 150, 50, 1, 5},
{9999, 5, 500, 200, 200, 500, 5, 9999}};
/**
* Difficulty level for the human. Represents the depth of the gametree.
*/
private int level = 3;
/**
* Constructs a new reversi game.
*
* @param human Human player
* @param bot Machine player
* @param firstPlayer Player that starts
*/
public Reversi(Player human, Player bot, Player firstPlayer) {
this.bot = bot;
this.human = human;
this.firstPlayer = firstPlayer;
this.playBoard = setInitialPositions();
currentPlayer = firstPlayer;
this.vectors = generateVectors();
}
/**
* {@inheritDoc}
*/
public final Player getFirstPlayer() {
return firstPlayer;
}
/**
* {@inheritDoc}
*/
public final Player next() {
return currentPlayer;
}
/**
* {@inheritDoc}
*/
public final boolean move(final int row, final int col) {
if (currentPlayer != human) {
return false;
}
if (!isValidMove(row, col)) {
new IllegalMoveException("Error! invalid move at ("
+ (row + 1) + ", " + (col + 1) + ").");
return false;
}
performMove(row, col);
return true;
}
/**
* {@inheritDoc}
*/
public final void machineMove() {
if (currentPlayer != bot) {
return;
}
gameTree = new GameTree(this, level);
Field bestMove = gameTree.getRoot()
.getChildrensHighestScore().getMove();
performMove(bestMove.row(), bestMove.col());
}
/**
* {@inheritDoc}
*/
public final void setLevel(final int level) {
this.level = level;
}
/**
* {@inheritDoc}
*/
public final boolean gameOver() {
return gameOver;
}
/**
* {@inheritDoc}
*/
public final Player getWinner() {
if (getNumberOfHumanTiles() > getNumberOfMachineTiles()) {
return human;
} else if (getNumberOfHumanTiles() == getNumberOfMachineTiles()) {
return null;
} else {
return bot;
}
}
/**
* {@inheritDoc}
*/
public final int getNumberOfHumanTiles() {
return numberOfTiles(human);
}
/**
* {@inheritDoc}
*/
public final int getNumberOfMachineTiles() {
return numberOfTiles(bot);
}
/**
* {@inheritDoc}
*/
public final int getNumberOfTilesSet() {
int n = (getNumberOfHumanTiles() + getNumberOfMachineTiles());
return n;
}
/**
* {@inheritDoc}
*/
public final Player getSlot(final int row, final int col) {
return playBoard[row][col];
}
@Override
public final Reversi clone() {
Reversi boardCopy = new Reversi(human, bot, firstPlayer);
boardCopy.currentPlayer = currentPlayer;
Player[][] playBoardCopy = new Player[SIZE][SIZE];
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
playBoardCopy[row][col] = playBoard[row][col];
}
}
boardCopy.playBoard = playBoardCopy;
return boardCopy;
}
/**
* Generate the vectors for the 8 directions. array[0] = 0°,
* array[1] = 45°...
*
* @return <code>boolean-array</code> with all vector fields
*/
private Field[] generateVectors() {
Field[] vector = {
new Field(-1, 0),
new Field(-1, +1),
new Field(0, +1),
new Field(+1, +1),
new Field(+1, 0),
new Field(+1, -1),
new Field(0, -1),
new Field(-1, -1)};
return vector;
}
/**
* Sets two tiles/player at the beginning of the match in the middle of
* of the playboard.
*
* @return <code>Player-array</code>
*/
private Player[][] setInitialPositions() {
Player[][] playBoard = new Player[SIZE][SIZE];
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
if (((SIZE / 2) - 1 == row || SIZE / 2 == row)
&& ((SIZE - col - 1) == row)) {
playBoard[row][col] = firstPlayer;
} else if ((SIZE / 2 - 1 == row || SIZE / 2 == row)
&& (col == row)) {
playBoard[row][col] = getEnemy(firstPlayer);
} else {
playBoard[row][col] = null;
}
}
}
return playBoard;
}
/**
*
* @param self Player
* @return Enemy of the Player
*/
private Player getEnemy(final Player self) {
if (self == human) {
return bot;
} else {
return human;
}
}
/**
* Switches the currentplayer and checks if the next player can do a valid
* move. If both players cannot move the game is over.
* game is over
*/
private void switchPlayer() {
currentPlayer = getEnemy(currentPlayer);
if (!movePossible()) {
currentPlayer = getEnemy(currentPlayer);
if (!movePossible()) {
gameOver = true;
}
}
}
/**
* Switch a tile to the current player.
*
* @param row Row of field
* @param col Column of field
*/
private void switchTile(final int row, final int col) {
playBoard[row][col] = currentPlayer;
}
/**
* Switch all corresponding enemy tiles around a <code>field</code>.
*
* @param row Row of field
* @param col Column of field
*/
private void switchTiles(final int row, final int col) {
boolean[] surrounders = validMoves(row, col);
switchTile(row, col);
for (int i = 0; i < SIZE; i++) {
if (surrounders[i]) {
int x = row + vectors[i].row();
int y = col + vectors[i].col();
while (playBoard[x][y] != currentPlayer) {
switchTile(x, y);
x = x + vectors[i].row();
y = y + vectors[i].col();
}
}
}
}
private int numberOfTiles(Player player) {
int mount = 0;
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
if (playBoard[row][col] == player) {
mount++;
}
}
}
return mount;
}
/**
*
* @param row Row of the field
* @param col Column of the field
* @return <code>true</code> if field with <code>row</code> and
* <code>col</code> is not set by one of the
* player <br /> else if field is set
*/
private boolean isFree(final int row, final int col) {
if (playBoard[row][col] == null) {
return true;
}
return false;
}
/**
* Checks the hole playBoard if the current player can do a valid move.
*
* @return <code>true</code> if another move is possible.
*/
private boolean movePossible() {
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
if (isValidMove(row, col)) {
return true;
}
}
}
return false;
}
/**
* Walk the board in a direction.
*
* @param row Row of field
* @param col Column of field
* @param x row direction to walk
* @param y column direction to walk
* @return if the direction has a valid move
*/
private boolean boardWalker(int row, int col, final int x, final int y) {
row = row + x;
col = col + y;
int counter = 0;
while (row < SIZE && row >= 0 && col < SIZE && col >= 0) {
if (playBoard[row][col] == null) {
return false;
} else if (playBoard[row][col] == currentPlayer) {
return (counter > 0);
}
row = row + x;
col = col + y;
counter++;
}
return false;
}
/**
* Checks a boolean array if it contains a value that is true.
*
* @param array Array
* @return <code>true</code> if any value is true
*/
private boolean checkBoolArray(final boolean[] array) {
for (int i = 0; i < array.length; i++) {
if (array[i]) {
return true;
}
}
return false;
}
/**
*
* @param row Row of field
* @param col Column of field
* @return if the currentPlayer can do a move on field(row, col)
*/
final boolean isValidMove(final int row, final int col) {
return isFree(row, col)
&& checkBoolArray(validMoves(row, col));
}
/**
* Searches a field for valid moves.
*
* @param row Row of field
* @param col Column of field
* @return <code>Array</code> of enemy tiles: <br />
* array[0] is at 0°, array[1] is diagonal 45°,<br />
* array[2] right 90° and so on
*/
private boolean[] validMoves(final int row, final int col) {
boolean[] neighbourhood = new boolean[8];
for (int i = 0; i < neighbourhood.length; i++) {
neighbourhood[i] = boardWalker(row, col,
vectors[i].row(), vectors[i].col());
}
return neighbourhood;
}
/**
* Switches all tiles and changes switches player if possible.
*
* @param row Row of field
* @param col Column of field
*/
void performMove(final int row, final int col) {
switchTiles(row, col);
switchPlayer();
}
/**
* Calculates the score of a reversi.
*
* @return <code>score</code>
*/
double score() {
double score = (scoreT() + scoreM() + scoreP());
return score;
}
/**
* Calculates the weighting score.
*
* @return <code>scoreT</code>
*/
private double scoreT() {
double scoreSelf = 0;
double scoreEnemy = 0;
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
if (playBoard[row][col] == bot) {
scoreSelf += weighting[row][col];
} else if (playBoard[row][col] == human) {
scoreEnemy += weighting[row][col];
}
}
}
return (scoreSelf - (1.5 * scoreEnemy));
}
/**
* Calculates the mobility score.
*
* @return <code>scoreM</code>
*/
private double scoreM() {
double scoreSelf = 0;
double scoreEnemy = 0;
Player tmp = currentPlayer;
int n = getNumberOfTilesSet();
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
currentPlayer = bot;
if (isValidMove(row, col)) {
scoreSelf++;
}
currentPlayer = human;
if (isValidMove(row, col)) {
scoreEnemy++;
}
currentPlayer = bot;
}
}
currentPlayer = tmp;
return (64.0 / n) * (3.0 * scoreSelf - 4.0 * scoreEnemy);
}
/**
* Calculates the future-potential score.
*
* @return <code>scoreP</code>
*/
private double scoreP() {
double scoreSelf = 0;
double scoreEnemy = 0;
int n = getNumberOfTilesSet();
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
if (playBoard[row][col] == human) {
for (int i = 0; i < 8; i++) {
int x = row + vectors[i].row();
int y = col + vectors[i].col();
if ((x >= 0 && x < SIZE && y >= 0 && y < SIZE)
&& (playBoard[x][y] == null)) {
scoreSelf++;
}
}
} else if (playBoard[row][col] == bot) {
for (int i = 0; i < 8; i++) {
int x = row + vectors[i].row();
int y = col + vectors[i].col();
if ((x >= 0 && x < SIZE && y >= 0 && y < SIZE)
&& (playBoard[x][y] == null)) {
scoreEnemy++;
}
}
}
}
}
return (64.0 / (2 * n)) * (2.5 * scoreSelf - 3.0 * scoreEnemy);
}
}

View File

@@ -0,0 +1,237 @@
package reversi.controller;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Stack;
import javax.swing.JComboBox;
import reversi.model.Board;
import reversi.model.Field;
import reversi.model.IllegalMoveException;
import reversi.model.Player;
import reversi.model.Reversi;
import reversi.view.ReversiField;
import reversi.view.ReversiView;
/**
* Controller has an instance of the board and handles the
* mouse and keyboard actions. Updates the gui if playBoard changes.
*/
public class ReversiController {
private Player human = new Player(true);
private Player bot = new Player(false);
private Reversi model;
private ReversiView view;
private MachineMoveThread machineMoveThread;
/**
* Stack for UndoMove method.
*/
private Stack<Reversi> pastMoves;
/**
* Create the model and the view.
*/
public ReversiController() {
this.model = new Reversi(human, bot, human);
this.view = new ReversiView();
addListener();
this.view.updatePlayField(this.model);
createStack();
}
/**
* View the gui.
*/
public final void showView() {
this.view.setVisible(true);
}
/**
* Opens a new reversi game.
*
* @param firstPlayer Player that starts
* @return new reversi board
*/
private Reversi newGame(Player firstPlayer) {
stopMachineMoveThread();
createStack();
Reversi model = new Reversi(this.human, this.bot, firstPlayer);
model.setLevel(this.view.getLevel());
if (firstPlayer == bot) {
performMachineMove(model);
} else {
this.view.updatePlayField(model);
this.view.setMovePossible(true);
}
return model;
}
/**
* Add all Listener in the gui.
*/
private void addListener() {
setPlayFieldListeners();
this.view.setNewGameListener(new NewGameListener());
this.view.setSwitchLevelListener(new SwitchLevelListener());
this.view.setSwitchGameListener(new SwitchGameListener());
this.view.setUndoMoveListener(new UndoMoveListener());
this.view.setQuitGameListener(new QuitGameListener());
}
/**
* Set the MouseListener for each PlayField.
*/
private void setPlayFieldListeners() {
for (int row = 0; row < Board.SIZE; row++) {
for (int col = 0; col < Board.SIZE; col++) {
this.view.setPlayFieldListener(
new FieldListener(), new Field(row, col));
}
}
}
/**
* Init Stack and disable Undo Button.
*/
private void createStack() {
this.pastMoves = new Stack<Reversi>();
view.setUndoButtonVisible(false);
}
/**
* Return to the state of the board before the last human and bot move.
*/
private void undoMove() {
stopMachineMoveThread();
this.model = pastMoves.pop();
this.view.updatePlayField(this.model);
if (pastMoves.isEmpty()) {
this.view.setUndoButtonVisible(false);
} else {
this.view.setMovePossible(true);
}
}
private void performMachineMove(Reversi model) {
this.view.setMovePossible(false);
machineMoveThread = new MachineMoveThread(model, this.view);
machineMoveThread.start();
}
private void performHumanMove(Field field) {
pastMoves.push(model.clone());
view.setUndoButtonVisible(true);
if (model.move(field.row(), field.col())) {
if (model.next() == human && !model.gameOver()) {
javax.swing.SwingUtilities.invokeLater(new Runnable() {
@Override
public final void run() {
view.viewMessage("The bot has to miss a turn");
}
});
}
} else {
undoMove();
}
view.updatePlayField(model);
if (model.next() == bot) {
performMachineMove(model);
}
}
@SuppressWarnings("deprecation")
private void stopMachineMoveThread() {
if (machineMoveThread != null) {
machineMoveThread.stop();
}
}
/**
* Inner class FieldListener performs a move on click and starts the
* machine move. After every move, game over check is done.
*/
private class FieldListener extends MouseAdapter {
@Override
public void mousePressed(final MouseEvent event) {
if (!view.getMovePossible()) {
new IllegalMoveException("Machine Move is active");
return;
}
Field field = ((ReversiField) event.getComponent()).getField();
performHumanMove(field);
}
}
/**
* Create Listener for the level switch combobox.
*/
private class SwitchLevelListener implements ActionListener {
@Override
public void actionPerformed(final ActionEvent e) {
@SuppressWarnings("unchecked")
JComboBox<Integer> level = (JComboBox<Integer>) e.getSource();
model.setLevel((Integer) level.getSelectedItem());
}
}
/**
* Create Listener for the new game button.
*/
private class NewGameListener implements ActionListener {
@Override
public void actionPerformed(final ActionEvent e) {
model = newGame(model.getFirstPlayer());
}
}
/**
* Create Listener for the switch game button.
*/
private class SwitchGameListener implements ActionListener {
@Override
public void actionPerformed(final ActionEvent e) {
if (model.getFirstPlayer() == human) {
model = newGame(bot);
} else {
model = newGame(human);
}
}
}
/**
* Create Listener for the undo move button.
*/
private class UndoMoveListener implements ActionListener {
@Override
public void actionPerformed(final ActionEvent e) {
undoMove();
}
}
/**
* Create Listener for the quit game button.
*/
private class QuitGameListener implements ActionListener {
@Override
public void actionPerformed(final ActionEvent e) {
for (Frame frame : java.awt.Frame.getFrames()) {
frame.dispose();
}
stopMachineMoveThread();
}
}
}

83
Reversi/ReversiField.java Normal file
View File

@@ -0,0 +1,83 @@
package reversi.view;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import javax.swing.JPanel;
import reversi.model.Field;
import reversi.model.Player;
/**
* Class for a single play field panel. Paints the circles with the color of
* the Player.
*/
public class ReversiField extends JPanel {
private static final long serialVersionUID = -5762403586769495603L;
private int height;
private int width;
private int y;
private int x;
private final Field field;
private Player player;
/**
*
* @param field <code>Field</code> with coordinates of the play field.
*/
public ReversiField(final Field field) {
this.field = field;
setPlayer(player);
}
/**
* {@inheritDoc}
*/
@Override
public final void paintComponent(final Graphics g) {
super.paintComponent(g);
height = (int) (super.getHeight() * 0.8);
width = (int) (super.getWidth() * 0.8);
y = (int) (width * 0.1 + 2);
x = (int) (height * 0.1 + 2);
if (player != null && player.getType()) {
g.setColor(Color.BLUE);
g.fillOval(y, x, width, height);
} else if (player != null && !player.getType()) {
g.setColor(Color.RED);
g.fillOval(y, x, width, height);
}
}
/**
*
* @return <code>Field</code> with coordinates of the play field.
*/
public final Field getField() {
return this.field;
}
/**
* Set the listener created in the controller.
*
* @param ma Mouseadapter
*/
public final void setListener(final MouseAdapter ma) {
this.addMouseListener(ma);
}
/**
* Set a player on a field (draw the circle in right color).
*
* @param player Player that is set.
*/
public void setPlayer(Player player) {
this.player = player;
repaint();
}
}

24
Reversi/ReversiGame.java Normal file
View File

@@ -0,0 +1,24 @@
package reversi.main;
import reversi.controller.ReversiController;
/**
* Starts a Reversi Game with the gui.
*/
public final class ReversiGame {
private ReversiController controller;
private ReversiGame() {
this.controller = new ReversiController();
}
/**
*
* @param args unused...
*/
public static void main(final String...args) {
ReversiGame reversiGame = new ReversiGame();
reversiGame.controller.showView();
}
}

View File

@@ -0,0 +1,70 @@
package reversi.view;
import java.awt.Color;
import java.awt.GridLayout;
import java.awt.event.MouseAdapter;
import javax.swing.JPanel;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder;
import reversi.model.Board;
import reversi.model.Field;
import reversi.model.Reversi;
/**
* Create a reversi playboard for the reversi game gui.
*/
public class ReversiPlayBoard extends JPanel {
private static final long serialVersionUID = 2725183063551812386L;
private ReversiField[][] playField = new ReversiField
[Board.SIZE][Board.SIZE];
/**
* Init array with all reversi fields. Set background and tooltip of
* fields.
*/
public ReversiPlayBoard() {
this.setLayout(new GridLayout(Board.SIZE, Board.SIZE));
this.setBorder(new CompoundBorder(
null, new EmptyBorder(0, 0, 10, 10)));
for (int row = 0; row < Board.SIZE; row++) {
for (int col = 0; col < Board.SIZE; col++) {
playField[row][col] = new ReversiField(new Field(row, col));
playField[row][col].setBorder(
new MatteBorder(1, 1, 0, 0, Color.BLACK));
playField[row][col].setBackground(new Color(0, 170, 0));
playField[row][col].setToolTipText("Click to make a move!");
this.add(playField[row][col]);
}
}
this.setVisible(true);
}
/**
* Update hole playboard players.
*
* @param board Board to update.
*/
public void updatePlayBoard(Reversi board) {
for (int row = 0; row < Board.SIZE; row++) {
for (int col = 0; col < Board.SIZE; col++) {
this.playField[row][col].setPlayer(board.getSlot(row, col));
}
}
}
/**
* Set mouselistener of <code>field</code>
*
* @param ma Mouseadapter
* @param field Field
*/
public final void setFieldListener(
final MouseAdapter ma, final Field field) {
this.playField[field.row()][field.col()].setListener(ma);
}
}

292
Reversi/ReversiView.java Normal file
View File

@@ -0,0 +1,292 @@
package reversi.view;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.Toolkit;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import reversi.model.Board;
import reversi.model.Field;
import reversi.model.Reversi;
/**
* Create and update the gui.
*/
public class ReversiView extends JFrame {
private static final long serialVersionUID = 5382834862595792805L;
private boolean movePossible;
private ReversiPlayBoard playBoard;
private JPanel horizTableLabel;
private JPanel verticTableLabel;
private JLabel humanTiles;
private JLabel botTiles;
private JComboBox<Integer> switchLevel =
new JComboBox<Integer>(new Integer[] {1, 2, 3, 4, 5});
private JButton newGame = new JButton("New");
private JButton switchGame = new JButton("Switch");
private JButton undoMove = new JButton("Undo");
private JButton quitGame = new JButton("Quit");
/**
* Create the gui for the reversi game.
*/
public ReversiView() {
super("Reversi");
initForms();
movePossible = true;
}
/**
* Create all buttons and the playfield.
*/
private void initForms() {
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(410, 450);
this.setLayout(new BorderLayout());
playBoard = new ReversiPlayBoard();
verticTableLabel = verticTableLabel();
horizTableLabel = horizTableLabel();
humanTiles = tilesSet();
humanTiles.setForeground(Color.BLUE);
botTiles = tilesSet();
botTiles.setForeground(Color.RED);
Container menuBar = new Container();
menuBar.setLayout(new FlowLayout());
menuBar.add(humanTiles);
menuBar.add(switchLevel);
menuBar.add(newGame);
menuBar.add(switchGame);
switchLevel.setSelectedIndex(2);
menuBar.add(undoMove);
menuBar.add(quitGame);
menuBar.add(botTiles);
this.getContentPane().add(playBoard, BorderLayout.CENTER);
this.getContentPane().add(menuBar, BorderLayout.SOUTH);
this.getContentPane().add(verticTableLabel, BorderLayout.WEST);
this.getContentPane().add(horizTableLabel, BorderLayout.NORTH);
}
/**
* Create the vertical label for the playfield.
*
* @return JPanel The label.
*/
private JPanel verticTableLabel() {
JPanel panel = new JPanel();
panel.setLayout(new GridLayout(Board.SIZE, 1));
panel.setBorder(new CompoundBorder(
null, new EmptyBorder(0, 5, 10, 5)));
for (int i = 1; i <= Board.SIZE; i++) {
JLabel tmp = new JLabel(String.valueOf(i));
panel.add(tmp);
}
return panel;
}
/**
* Create the horizontal label for the playfield.
*
* @return JPanel The label.
*/
private JPanel horizTableLabel() {
JPanel panel = new JPanel();
panel.setLayout(new GridLayout(1, Board.SIZE));
panel.setBorder(new CompoundBorder(
null, new EmptyBorder(0, 20, 0, 12)));
for (int i = 1; i <= Board.SIZE; i++) {
JLabel tmp = new JLabel(String.valueOf(i));
tmp.setHorizontalAlignment(SwingConstants.CENTER);
panel.add(tmp);
}
return panel;
}
/**
* Creates both labels for the number of tiles that are set.
*
* @return JLabel The label.
*/
private JLabel tilesSet() {
JLabel tiles = new JLabel();
tiles.setBorder(new CompoundBorder(null, new EmptyBorder(0, 3, 0, 3)));
tiles.setFont(new Font(Font.DIALOG, Font.BOLD, 20));
return tiles;
}
/**
*
* @return Level
*/
public final int getLevel() {
return (Integer) this.switchLevel.getSelectedItem();
}
/**
*
* @return <code>true</code> if human is allowed to perform a move.<br />
* <code>false</code> e.g. if a machine move is active.
*/
public boolean getMovePossible() {
return movePossible;
}
/**
*
* @param movePossible Set if human move is possible.
*/
public final void setMovePossible(final boolean movePossible) {
this.movePossible = movePossible;
}
/**
*
* @param bool Set the undo button vissibility.
*/
public final void setUndoButtonVisible(final boolean bool) {
this.undoMove.setEnabled(bool);
}
/**
* Update the Tiles display in the gui.
*
* @param human Human amount of tiles.
* @param bot Amount of machine tiles.
*/
private void setTilesSet(int human, int bot) {
this.humanTiles.setText(String.valueOf(human));
this.botTiles.setText(String.valueOf(bot));
}
/**
* Updates the players on the playfield.
*
* @param model Board to update.
*/
public final void updatePlayField(final Reversi model) {
javax.swing.SwingUtilities.invokeLater(new Runnable() {
@Override
public final void run() {
playBoard.updatePlayBoard(model);
setTilesSet(model.getNumberOfHumanTiles(),
model.getNumberOfMachineTiles());
/* check movePossible just because of thread interference
when game is over
*/
if (model.gameOver() && movePossible) {
viewMessage(winnerMessage(model));
movePossible = false;
}
}
});
}
/**
* Show a message dialog and play a system sound.
*
* @param msg Message to show.
*/
public final void viewMessage(final String msg) {
Toolkit.getDefaultToolkit().beep();
JOptionPane.showMessageDialog(this, msg);
}
/**
* Checks which player has won.
*
* @return Message with the winner.
*/
private String winnerMessage(Reversi model) {
if (model.getWinner() == null) {
return "Tie game!";
} else if (model.getWinner().getType()) {
return "You have won!";
} else {
return "The Computer has won!";
}
}
/**
* Pass the field listener of the controller to ReversiPlayBoard class.
*
* @param ma Mouseadapter
* @param field Field
*/
public final void setPlayFieldListener(
final MouseAdapter ma, final Field field) {
this.playBoard.setFieldListener(ma, field);
}
/**
*
* @param l Actionlistener
*/
public final void setSwitchLevelListener(final ActionListener l) {
this.switchLevel.addActionListener(l);
this.switchLevel.setToolTipText("Choose a difficulty level");
}
/**
*
* @param l Actionlistener
*/
public final void setNewGameListener(final ActionListener l) {
this.newGame.addActionListener(l);
this.newGame.setMnemonic(KeyEvent.VK_N);
this.newGame.setToolTipText("Start new game");
}
/**
*
* @param l Actionlistener
*/
public final void setSwitchGameListener(final ActionListener l) {
this.switchGame.addActionListener(l);
this.switchGame.setMnemonic(KeyEvent.VK_S);
this.switchGame.setToolTipText(
"Switch first player and start new game");
}
/**
*
* @param l Actionlistener
*/
public final void setUndoMoveListener(final ActionListener l) {
this.undoMove.addActionListener(l);
this.undoMove.setMnemonic(KeyEvent.VK_U);
this.undoMove.setToolTipText("Undo last move");
}
/**
*
* @param l Actionlistener
*/
public final void setQuitGameListener(final ActionListener l) {
this.quitGame.addActionListener(l);
this.quitGame.setMnemonic(KeyEvent.VK_Q);
this.quitGame.setToolTipText("Quit the Game");
}
}