Update project

master
Minh Bui 5 years ago
commit fc3ec3660c

Binary file not shown.

@ -0,0 +1,469 @@
/**
* This class contains the main method for running the game.
*
*/
//import view.Connect4TextView;
import view.Connect4View;
import view.DialogBox;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Observer;
import controller.Connect4Controller;
import model.Connect4Model;
import model.Connect4MoveMessage;
import model.IllegalArgumentException;
import model.Minimax_AlphaBeta;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.BorderPane;
import javafx.stage.Modality;
import javafx.scene.control.Alert;
public class Connect4 extends Application implements Runnable {
private Observer currentView;
private Observer guiView;
private BorderPane window;
private Connect4Model gameModel;
private static final int width = 344;
private static final int height = 326;
private static final String SERIALIZED_MODEL_NAME = "save_game.dat";
private DialogBox dialogBox;
private ServerSocket server;
private Socket socket;
Connect4MoveMessage mvMsg = null;
public static void main(String[] args) {
launch(args);
}
/**
* This method is to generate a GUI that can interact with the user and contains
* a menu item to start a new game, and close with a saved game
*
* @param primaryStage the window of the GUI
*/
@Override
public void start(Stage primaryStage) throws Exception {
boolean foundSerializedFile = this.deserializeGameModel();
if (!foundSerializedFile) {
initGame();
} else {
}
primaryStage.setTitle("Connect 4");
window = new BorderPane();
Scene scene = new Scene(window, width, height);
// Add MenuBar
MenuBar menuBar = new MenuBar();
// Do this for more responsive UI when resizing the window.
menuBar.prefWidthProperty().bind(primaryStage.widthProperty());
MenuItem newGameButton = new MenuItem();
Menu FileMenu = new Menu();
FileMenu.setText("File");
MenuItem networkedButton = new MenuItem("Networked Game");
newGameButton.setText("New Game");
// construct a new game model
newGameButton.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent ae) {
// gameModel.newGame();
gameModel = new Connect4Model();
modelSetting(foundSerializedFile, true, false, true);
}
});
networkedButton.setOnAction(event -> {
dialogBox = new DialogBox();
dialogBox.setTitle("Network Setup");
dialogBox.initModality(Modality.APPLICATION_MODAL);
dialogBox.getOk().setOnAction(e -> {
gameModel = new Connect4Model();
if (dialogBox.serverIsCheck() || dialogBox.clientIsCheck()) {
if (dialogBox.humanIsCheck() || dialogBox.computerIsCheck()) {
if (dialogBox.serverIsCheck()) {
modelSetting(false, dialogBox.humanIsCheck(), true, true);
createServer(dialogBox.getPort());
} else {
modelSetting(false, dialogBox.humanIsCheck(), true, false);
createClient(dialogBox.getServerName(), dialogBox.getPort());
}
} else {
new Alert(Alert.AlertType.ERROR, "Please select Human or Computer").showAndWait();
return;
}
} else {
new Alert(Alert.AlertType.ERROR, "Please select Server or Client").showAndWait();
return;
}
Thread networkThread = new Thread(this);
networkThread.start();
dialogBox.close();
});
dialogBox.show();
});
FileMenu.getItems().add(newGameButton);
FileMenu.getItems().add(networkedButton);
menuBar.getMenus().add(FileMenu);
window.setTop((Node) menuBar);
modelSetting(foundSerializedFile, true, false, true);
// if the game is not over, create a file storing the game(model object)
primaryStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
public void handle(WindowEvent we) {
if (gameModel.isOver())
((Connect4View) guiView).deleteSerializedFile();
else
serializeGameModel();
}
});
primaryStage.setScene(scene);
primaryStage.setOnCloseRequest((event) -> {
closeNetwork();
});
primaryStage.show();
}
/**
* This method is to set up the model by linking it to the GUIView(Observer) and
* controller
*
* @param foundSerializedFile true means that there is a serializedFile under
* the current directory otherwise not
*/
private void modelSetting(boolean foundSerializedFile, boolean isFirstHuman, boolean isNetworkGame,
boolean moveFirst) {
Connect4Controller gameController = new Connect4Controller(gameModel);
guiView = new Connect4View(gameModel, gameController, foundSerializedFile, isFirstHuman, isNetworkGame,
moveFirst);
setViewTo(guiView);
gameModel.addObserver(guiView);
gameModel.addObserver(currentView);
}
private void createServer(int port) {
try {
server = new ServerSocket(port);
socket = server.accept();
System.out.println("Client connected: " + socket.getInetAddress());
} catch (IOException e) {
e.printStackTrace();
}
}
private void createClient(String serverName, int port) {
try {
socket = new Socket(serverName, port);
System.out.println("Connected to server at: " + socket.getInetAddress() + " with port: " + port);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* This method is to create a new instance for gameModel
*/
private void initGame() {
gameModel = new Connect4Model();
}
/**
* This method is to set the GUI to the center of the pane
*
* @param newView
*/
private void setViewTo(Observer newView) {
window.setCenter(null);
currentView = newView;
window.setCenter((Node) currentView);
}
/**
* This method is to serialize the game model object.
*/
public void serializeGameModel() {
try {
FileOutputStream outputFile = new FileOutputStream("./" + SERIALIZED_MODEL_NAME);
ObjectOutputStream oos = new ObjectOutputStream(outputFile);
oos.writeObject(gameModel);
oos.close();
outputFile.close();
} catch (IOException ioe) {
System.err.println(ioe.getMessage());
}
}
/**
* This method will return true if found a serialized file to deserialize from
* and false otherwise.
*
* @return True if found a serialized file to deserialize from.
*/
public boolean deserializeGameModel() {
try {
FileInputStream inputFile = new FileInputStream("./" + SERIALIZED_MODEL_NAME);
ObjectInputStream ois = new ObjectInputStream(inputFile);
this.gameModel = (Connect4Model) ois.readObject();
this.gameModel.setComputerPlayer(new Minimax_AlphaBeta());
System.out.println("Found a serialized file.");
ois.close();
inputFile.close();
return true;
} catch (IOException ioe) {
System.out.println(ioe.getMessage());
return false;
} catch (ClassNotFoundException ex) {
System.err.println(ex.getStackTrace());
return false;
}
}
@Override
public void run() {
Connect4MoveMessage oldMvMsg = null;
ObjectOutputStream output = null;
ObjectInputStream input = null;
// This instance of program is a server.
try {
output = new ObjectOutputStream(socket.getOutputStream());
input = new ObjectInputStream(socket.getInputStream());
} catch (IOException e1) {
e1.printStackTrace();
}
while (!gameModel.isOver() && socket.isConnected()) {
System.out.println("Networking thread is still running.");
if (server != null) {
try {
if (gameModel.isHumanMove()) {
if (!dialogBox.humanIsCheck()) {
gameModel.computerMove(gameModel.getHumanChar(), true);
mvMsg = gameModel.getMoveMsg();
output.writeObject(mvMsg);
output.flush();
oldMvMsg = mvMsg;
gameModel.switchTurn();
mvMsg = null;
continue;
}
mvMsg = gameModel.getMoveMsg();
if (mvMsg == null)
continue;
if (oldMvMsg != null) {
if ((mvMsg.getRow() == oldMvMsg.getRow() && mvMsg.getColumn() == oldMvMsg.getColumn()
&& mvMsg.getColor() == oldMvMsg.getColor()))
continue;
}
System.out.println(mvMsg.getRow() + " " + mvMsg.getColumn() + " " + mvMsg.getColor());
if (!mvMsg.equals(oldMvMsg)) {
if (oldMvMsg == null) {
output.writeObject(mvMsg);
output.flush();
oldMvMsg = mvMsg;
gameModel.switchTurn();
mvMsg = null;
} else {
if ((mvMsg.getRow() != oldMvMsg.getRow()) || mvMsg.getColumn() != oldMvMsg.getColumn()
|| mvMsg.getColor() != oldMvMsg.getColor()) {
output.writeObject(mvMsg);
output.flush();
oldMvMsg = mvMsg;
gameModel.switchTurn();
mvMsg = null;
}
}
}
} else {
mvMsg = (Connect4MoveMessage) input.readObject();
if (mvMsg == null)
continue;
if (oldMvMsg != null) {
if ((mvMsg.getRow() == oldMvMsg.getRow() && mvMsg.getColumn() == oldMvMsg.getColumn()
&& mvMsg.getColor() == oldMvMsg.getColor()))
continue;
}
if (!mvMsg.equals(oldMvMsg)) {
if (oldMvMsg == null) {
System.out.println(mvMsg.getRow() + " " + mvMsg.getColumn() + " " + mvMsg.getColor());
((Connect4View) currentView).getController().placeToken(mvMsg.getColumn());
gameModel.printRawBoard();
gameModel.switchTurn();
} else {
if ((mvMsg.getRow() != oldMvMsg.getRow()) || mvMsg.getColumn() != oldMvMsg.getColumn()
|| mvMsg.getColor() != oldMvMsg.getColor()) {
System.out
.println(mvMsg.getRow() + " " + mvMsg.getColumn() + " " + mvMsg.getColor());
((Connect4View) currentView).getController().placeToken(mvMsg.getColumn());
gameModel.printRawBoard();
gameModel.switchTurn();
}
}
oldMvMsg = mvMsg;
mvMsg = null;
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// This instance of program is a client.
} else {
try {
if (!gameModel.isHumanMove()) {
if (!dialogBox.humanIsCheck()) {
gameModel.computerMove(gameModel.getComputerChar(), false);
mvMsg = gameModel.getMoveMsg();
output.writeObject(mvMsg);
output.flush();
oldMvMsg = mvMsg;
gameModel.switchTurn();
mvMsg = null;
continue;
}
mvMsg = gameModel.getMoveMsg();
if (mvMsg == null)
continue;
if (oldMvMsg != null) {
if ((mvMsg.getRow() == oldMvMsg.getRow() && mvMsg.getColumn() == oldMvMsg.getColumn()
&& mvMsg.getColor() == oldMvMsg.getColor()))
continue;
}
System.out.println(mvMsg.getRow() + " " + mvMsg.getColumn() + " " + mvMsg.getColor());
if (!mvMsg.equals(oldMvMsg)) {
if (oldMvMsg == null) {
output.writeObject(mvMsg);
output.flush();
oldMvMsg = mvMsg;
gameModel.switchTurn();
mvMsg = null;
} else {
if ((mvMsg.getRow() != oldMvMsg.getRow()) || mvMsg.getColumn() != oldMvMsg.getColumn()
|| mvMsg.getColor() != oldMvMsg.getColor()) {
output.writeObject(mvMsg);
output.flush();
oldMvMsg = mvMsg;
gameModel.switchTurn();
mvMsg = null;
}
}
}
} else {
mvMsg = (Connect4MoveMessage) input.readObject();
if (mvMsg == null)
continue;
if (oldMvMsg != null) {
if ((mvMsg.getRow() == oldMvMsg.getRow() && mvMsg.getColumn() == oldMvMsg.getColumn()
&& mvMsg.getColor() == oldMvMsg.getColor()))
continue;
}
if (!mvMsg.equals(oldMvMsg)) {
if (oldMvMsg == null) {
System.out.println(mvMsg.getRow() + " " + mvMsg.getColumn() + " " + mvMsg.getColor());
((Connect4View) currentView).getController().placeToken(mvMsg.getColumn());
gameModel.printRawBoard();
gameModel.switchTurn();
} else {
if ((mvMsg.getRow() != oldMvMsg.getRow()) || mvMsg.getColumn() != oldMvMsg.getColumn()
|| mvMsg.getColor() != oldMvMsg.getColor()) {
System.out
.println(mvMsg.getRow() + " " + mvMsg.getColumn() + " " + mvMsg.getColor());
((Connect4View) currentView).getController().placeToken(mvMsg.getColumn());
gameModel.printRawBoard();
gameModel.switchTurn();
}
}
oldMvMsg = mvMsg;
mvMsg = null;
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public void closeNetwork() {
try {
if (server != null)
server.close();
if (socket != null)
socket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

@ -0,0 +1,333 @@
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import model.Connect4Model;
import controller.Connect4Controller;
/**
* This class contains test methods for our Connect4 game.
*
* @author Minh Bui
*
*/
public class Connect4Test {
/**
* Test method for Connect4Model.wonByDiagonal().
*/
@Test
void testWonByDiagonal() {
Connect4Model gm = new Connect4Model();
Connect4Controller controller = new Connect4Controller(gm);
try {
gm.setObjectAt(3, 0, gm.getHumanChar());
gm.setObjectAt(2, 1, gm.getHumanChar());
gm.setObjectAt(1, 2, gm.getHumanChar());
gm.setObjectAt(0, 3, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (gm.wonByDiagonal(gm.getHumanChar()));
gm = new Connect4Model();
controller = new Connect4Controller(gm);
gm.setObjectAt(4, 0, gm.getHumanChar());
gm.setObjectAt(3, 1, gm.getHumanChar());
gm.setObjectAt(2, 2, gm.getHumanChar());
gm.setObjectAt(1, 3, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (gm.wonByDiagonal(gm.getHumanChar()));
gm = new Connect4Model();
controller = new Connect4Controller(gm);
gm.setObjectAt(3, 1, gm.getHumanChar());
gm.setObjectAt(2, 2, gm.getHumanChar());
gm.setObjectAt(1, 3, gm.getHumanChar());
gm.setObjectAt(0, 4, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (gm.wonByDiagonal(gm.getHumanChar()));
gm = new Connect4Model();
controller = new Connect4Controller(gm);
gm.setObjectAt(4, 1, gm.getHumanChar());
gm.setObjectAt(3, 2, gm.getHumanChar());
gm.setObjectAt(2, 3, gm.getHumanChar());
gm.setObjectAt(1, 4, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (gm.wonByDiagonal(gm.getHumanChar()));
gm = new Connect4Model();
controller = new Connect4Controller(gm);
gm.setObjectAt(6, 0, gm.getHumanChar());
gm.setObjectAt(5, 1, gm.getHumanChar());
gm.setObjectAt(4, 2, gm.getHumanChar());
gm.setObjectAt(3, 3, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (gm.wonByDiagonal(gm.getHumanChar()));
gm = new Connect4Model();
controller = new Connect4Controller(gm);
gm.setObjectAt(6, 2, gm.getHumanChar());
gm.setObjectAt(5, 3, gm.getHumanChar());
gm.setObjectAt(4, 4, gm.getHumanChar());
gm.setObjectAt(3, 5, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (gm.wonByDiagonal(gm.getHumanChar()));
gm = new Connect4Model();
controller = new Connect4Controller(gm);
gm.setObjectAt(3, 0, gm.getHumanChar());
gm.setObjectAt(4, 1, gm.getHumanChar());
gm.setObjectAt(5, 2, gm.getHumanChar());
gm.setObjectAt(6, 3, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (gm.wonByDiagonal(gm.getHumanChar()));
gm = new Connect4Model();
controller = new Connect4Controller(gm);
gm.setObjectAt(0, 2, gm.getHumanChar());
gm.setObjectAt(1, 3, gm.getHumanChar());
gm.setObjectAt(2, 4, gm.getHumanChar());
gm.setObjectAt(3, 5, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (gm.wonByDiagonal(gm.getHumanChar()));
gm = new Connect4Model();
controller = new Connect4Controller(gm);
gm.setObjectAt(1, 1, gm.getHumanChar());
gm.setObjectAt(2, 2, gm.getHumanChar());
gm.setObjectAt(3, 3, gm.getHumanChar());
gm.setObjectAt(4, 4, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (gm.wonByDiagonal(gm.getHumanChar()));
gm = new Connect4Model();
controller = new Connect4Controller(gm);
gm.setObjectAt(0, 2, gm.getHumanChar());
gm.setObjectAt(1, 3, gm.getHumanChar());
gm.setObjectAt(2, 4, gm.getHumanChar());
gm.setObjectAt(3, 5, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (gm.wonByDiagonal(gm.getHumanChar()));
gm = new Connect4Model();
controller = new Connect4Controller(gm);
gm.setObjectAt(0, 0, gm.getHumanChar());
gm.setObjectAt(1, 1, gm.getHumanChar());
gm.setObjectAt(3, 3, gm.getHumanChar());
gm.setObjectAt(4, 4, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (!gm.wonByDiagonal(gm.getHumanChar()));
gm = new Connect4Model();
controller = new Connect4Controller(gm);
gm.setObjectAt(2, 2, gm.getHumanChar());
gm.setObjectAt(1, 1, gm.getHumanChar());
gm.setObjectAt(3, 3, gm.getHumanChar());
gm.setObjectAt(4, 4, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (gm.wonByDiagonal(gm.getHumanChar()));
} catch (model.IllegalArgumentException e) {
// Nothing will happen. Test cases are all valid input.
}
}
/**
* Test method for model.Connect4Model.wonByRow().
*/
@Test
void testWonByRow() {
Connect4Model gm = new Connect4Model();
Connect4Controller controller = new Connect4Controller(gm);
try {
gm.setObjectAt(0, 0, gm.getHumanChar());
gm.setObjectAt(1, 0, gm.getHumanChar());
gm.setObjectAt(2, 0, gm.getHumanChar());
gm.setObjectAt(3, 0, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (gm.wonByRow(gm.getHumanChar()));
gm = new Connect4Model();
controller = new Connect4Controller(gm);
gm.setObjectAt(0, 0, gm.getHumanChar());
gm.setObjectAt(1, 0, gm.getHumanChar());
gm.setObjectAt(4, 0, gm.getHumanChar());
gm.setObjectAt(5, 0, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (!gm.wonByRow(gm.getHumanChar()));
} catch (model.IllegalArgumentException e) {
// Test cases are all valid.
}
}
/**
* Test method for Connect4Model.wonByCol().
*/
@Test
void testWonByCol() {
Connect4Model gm = new Connect4Model();
Connect4Controller controller = new Connect4Controller(gm);
try {
gm.setObjectAt(0, 1, gm.getHumanChar());
gm.setObjectAt(0, 2, gm.getHumanChar());
gm.setObjectAt(0, 3, gm.getHumanChar());
gm.setObjectAt(0, 4, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (gm.wonByCol(gm.getHumanChar()));
gm = new Connect4Model();
controller = new Connect4Controller(gm);
gm.setObjectAt(5, 1, gm.getHumanChar());
gm.setObjectAt(5, 2, gm.getHumanChar());
gm.setObjectAt(5, 4, gm.getHumanChar());
gm.setObjectAt(5, 5, gm.getHumanChar());
// System.out.println(controller.gameBoardToString());
assert (!gm.wonByCol(gm.getHumanChar()));
} catch (model.IllegalArgumentException e) {
// Nothing to do.
}
}
/**
* Test misc methods.
*/
@Test
void testMisc() {
Connect4Model gm = new Connect4Model();
Connect4Controller controller = new Connect4Controller(gm);
// Testing board toString() method.
String empty_board = "_ _ _ _ _ _ _ " + "\n" + "_ _ _ _ _ _ _ " + "\n" + "_ _ _ _ _ _ _ " + "\n"
+ "_ _ _ _ _ _ _ " + "\n" + "_ _ _ _ _ _ _ " + "\n" + "_ _ _ _ _ _ _ " + "\n" + "0 1 2 3 4 5 6\n";
assertEquals(controller.gameBoardToString(), empty_board);
// Testing other misc methods.
assert (gm.isHumanMove());
try {
assertEquals(gm.getObjectAt(0, 0), gm.getBlankChar());
} catch (model.IllegalArgumentException e) {
// Guaranteed succeed. Nothing to do.
}
try {
controller.placeToken(0);
} catch (model.IllegalArgumentException e) {
// Nothing to handle.
}
gm.switchTurn();
assert (!gm.isHumanMove());
gm.computerMove(gm.getComputerChar(), false);
// Play a random game where computer vs computer.
gm = new Connect4Model();
controller = new Connect4Controller(gm);
while (!controller.gameIsOver()) {
gm.computerMove(gm.getHumanChar(), false);
gm.switchTurn();
gm.computerMove(gm.getComputerChar(), false);
gm.switchTurn();
}
// Test tie game.
gm = new Connect4Model();
controller = new Connect4Controller(gm);
for (int i = 0; i < gm.getMaxRow() * gm.getMaxCol(); i++)
gm.switchTurn();
try {
gm.setObjectAt(0, 0, gm.getComputerChar());
gm.setObjectAt(0, 1, gm.getHumanChar());
gm.setObjectAt(0, 2, gm.getComputerChar());
gm.setObjectAt(0, 3, gm.getHumanChar());
gm.setObjectAt(0, 4, gm.getComputerChar());
gm.setObjectAt(0, 5, gm.getHumanChar());
gm.setObjectAt(1, 0, gm.getHumanChar());
gm.setObjectAt(1, 1, gm.getComputerChar());
gm.setObjectAt(1, 2, gm.getHumanChar());
gm.setObjectAt(1, 3, gm.getComputerChar());
gm.setObjectAt(1, 4, gm.getHumanChar());
gm.setObjectAt(1, 5, gm.getComputerChar());
gm.setObjectAt(2, 0, gm.getComputerChar());
gm.setObjectAt(2, 1, gm.getHumanChar());
gm.setObjectAt(2, 2, gm.getComputerChar());
gm.setObjectAt(2, 3, gm.getHumanChar());
gm.setObjectAt(2, 4, gm.getComputerChar());
gm.setObjectAt(2, 5, gm.getHumanChar());
gm.setObjectAt(3, 0, gm.getComputerChar());
gm.setObjectAt(3, 1, gm.getHumanChar());
gm.setObjectAt(3, 2, gm.getComputerChar());
gm.setObjectAt(3, 3, gm.getHumanChar());
gm.setObjectAt(3, 4, gm.getComputerChar());
gm.setObjectAt(3, 5, gm.getHumanChar());
gm.setObjectAt(4, 0, gm.getComputerChar());
gm.setObjectAt(4, 1, gm.getHumanChar());
gm.setObjectAt(4, 2, gm.getComputerChar());
gm.setObjectAt(4, 3, gm.getHumanChar());
gm.setObjectAt(4, 4, gm.getComputerChar());
gm.setObjectAt(4, 5, gm.getHumanChar());
gm.setObjectAt(5, 0, gm.getHumanChar());
gm.setObjectAt(5, 1, gm.getComputerChar());
gm.setObjectAt(5, 2, gm.getHumanChar());
gm.setObjectAt(5, 3, gm.getComputerChar());
gm.setObjectAt(5, 4, gm.getHumanChar());
gm.setObjectAt(5, 5, gm.getComputerChar());
gm.setObjectAt(6, 0, gm.getHumanChar());
gm.setObjectAt(6, 1, gm.getComputerChar());
gm.setObjectAt(6, 2, gm.getHumanChar());
gm.setObjectAt(6, 3, gm.getComputerChar());
gm.setObjectAt(6, 4, gm.getHumanChar());
gm.setObjectAt(6, 5, gm.getComputerChar());
// Try to insert a token into a full grid.
assert(!controller.placeToken(0));
} catch (model.IllegalArgumentException e) {
// No exception will be caught.
}
assert (!gm.isBlank(6, 5));
// System.out.println(controller.gameBoardToString());
// System.out.println(gm.didWin(gm.getComputerChar()));
// System.out.println(gm.didWin(gm.getHumanChar()));
// System.out.println (gm.isTied());
// System.out.println(gm.moveMaked);
assert (gm.isTied());
// Test exceptions and faulty input.
final Connect4Model gm1 = new Connect4Model();
final Connect4Controller controller1 = new Connect4Controller(gm1);
assertThrows(model.IllegalArgumentException.class, () -> gm1.getObjectAt(-1, 0));
assertThrows(model.IllegalArgumentException.class, () -> gm1.getObjectAt(7, 0));
assertThrows(model.IllegalArgumentException.class, () -> gm1.getObjectAt(2, -1));
assertThrows(model.IllegalArgumentException.class, () -> gm1.getObjectAt(2, 6));
assertThrows(model.IllegalArgumentException.class, () -> gm1.setObjectAt(-1, 0, gm1.getHumanChar()));
assertThrows(model.IllegalArgumentException.class, () -> gm1.setObjectAt(7, 0, gm1.getHumanChar()));
assertThrows(model.IllegalArgumentException.class, () -> gm1.setObjectAt(2, -1, gm1.getHumanChar()));
assertThrows(model.IllegalArgumentException.class, () -> gm1.setObjectAt(2, 6, gm1.getHumanChar()));
assertThrows(model.IllegalArgumentException.class, () -> controller1.placeToken(-1));
assertThrows(model.IllegalArgumentException.class, () -> controller1.placeToken(7));
}
}

@ -0,0 +1,66 @@
# Connect 4
## Description
Connect 4 is a two-player zero-sume connection game. For more details on rules, please refer to: https://en.wikipedia.org/wiki/Connect_Four
![Connect4](https://en.wikipedia.org/wiki/Minimax)
## Features
· Allows either the AI or the user to play.
· It is possible for the AI to play against itself.
· Auto-saved game on close using serialization.
## Implementation
## Intelligent Computer Player
Computer player's using Minimax alogrithms with alpha-beta pruning. More details here: https://en.wikipedia.org/wiki/Minimax
## Network Protocol
### Establishing Client and Server Roles
The Server will always take the first turn. If it is a human player, the player will click and send the event to the client. Otherwise the AI will generate its turn and send it to the client. The client will go second. This will repeat until the game is over.
The moves made over network is implemented using serialization. Here's the details:
public class Connect4MoveMessage implements Serializable {
public static int YELLOW = 1;
public static int RED = 2;
private static final long serialVersionUID = 1L;
private int row;
private int col;
private int color;
public Connect4MoveMessage(int row, int col, int color) { … }
public int getRow() { … }
public int getColumn() { … }
public int getColor() { … }
}

@ -0,0 +1 @@
/Connect4Controller.class

@ -0,0 +1,86 @@
/**
* This class represents the Connect4 game controller, handling interaction with
* the game model.
*
*/
package controller;
import model.Connect4Model;
public class Connect4Controller {
private Connect4Model gameModel;
public Connect4Controller(Connect4Model model) {
gameModel = model;
}
/**
* Constructs a string that represents the current state of the board.
*
* @return A string representing the board.
*/
public String gameBoardToString() {
String boardString = "";
int max_row = gameModel.getMaxRow();
int max_col = gameModel.getMaxCol();
for (int j = max_col-1; j >= 0; j--) {
for (int i = 0; i < max_row; i++) {
try {
boardString += gameModel.getObjectAt(i, j) + " ";
} catch (model.IllegalArgumentException e) {
// It's guaranteed that the try block always succeed.
}
}
boardString += "\n";
}
boardString += "0 1 2 3 4 5 6\n";
return boardString;
}
/**
* Returns a string indicating whose turn is it.
*
* @return A string indicating a player's turn.
*/
public String playerTurnToString() {
return gameModel.isHumanMove() ? gameModel.getHumanChar() + "" : gameModel.getComputerChar() + "";
}
/**
* Returns true if successfully place a token of the current player into the atCol-th
* column and false otherwise.
*
* @param atCol The column in which the token will be attempted to place.
* @return True if success and false otherwise.
* @throws model.IllegalArgumentException is thrown if atCol is not between 0 and max row.
*/
public boolean placeToken(int atCol) throws model.IllegalArgumentException {
if (atCol < 0 || atCol >= gameModel.getMaxRow()) {
throw new model.IllegalArgumentException("Invalid arguments.");
}
for (int i = 0; i < gameModel.getMaxCol(); i++) {
if (gameModel.isBlank(atCol, i)) {
gameModel.setObjectAt(atCol, i, this.playerTurnToString().charAt(0));
return true;
}
}
return false;
}
/**
* Returns true if the game is over, meaning either 1 of the player wins or it's a tie.
*
* @return true if the game is over.
*/
public boolean gameIsOver() {
return gameModel.isTied() ||
gameModel.didWin(gameModel.getComputerChar()) ||
gameModel.didWin(gameModel.getHumanChar());
}
}

8
model/.gitignore vendored

@ -0,0 +1,8 @@
/ComputerPlayer.class
/Connect4Model.class
/Connect4MoveMessage.class
/IllegalArgumentException.class
/Minimax_AlphaBeta$GameState.class
/Minimax_AlphaBeta.class
/RandomAI.class
/PatternMatchingAI.class

@ -0,0 +1,11 @@
package model;
/**
* An interface for generating strategies.
*
*
*/
public interface ComputerPlayer {
public int getMove(Connect4Model gameModel, boolean max);
}

@ -0,0 +1,452 @@
package model;
/**
* This class represents the Connect4 game model.
*
*/
public class Connect4Model extends java.util.Observable implements java.io.Serializable {
/**
* Java generated serialization ID.
*/
private static final long serialVersionUID = 1L;
/**
* Representation of the board.
*/
private char[][] board;
/**
* The maximum size for each rows.
*/
private final int ROWS_NUM = 7;
/**
* The maximum size of each column.
*/
private final int COLS_NUM = 6;
/**
* The number of consecutive pieces either horizontally, vertically, or
* diagonally to win the game.
*/
private final int CONNECT_SIZE = 4;
/**
* Indicates if it's currently the human turn.
*/
private boolean isHumanMove;
/**
* The default character representation of the player.
*/
private final char humanChar = 'X';
/**
* The default character representation of the computer player.
*/
private final char computerChar = 'O';
/**
* The default character representation of an empty cell.
*/
private final char blankChar = '_';
/**
* The object which represents the computer's strategy for playing the game.
*/
private transient ComputerPlayer computerPlayer;
/**
* Counts the total number of moves that have been made.
*/
private int moveMaked;
/**
* Encapsulate move information to send over to the views.
*/
private transient Connect4MoveMessage moveMsg;
public Connect4Model() {
board = new char[ROWS_NUM][COLS_NUM];
for (int i = 0; i < ROWS_NUM; i++) {
for (int j = 0; j < COLS_NUM; j++) {
board[i][j] = blankChar;
}
}
// Human moves first by default.
isHumanMove = true;
// computerPlayer = new RandomAI();
computerPlayer = new Minimax_AlphaBeta();
moveMaked = 0;
moveMsg = null;
setChanged();
notifyObservers(moveMsg);
}
/*
public void newGame() {
board = new char[ROWS_NUM][COLS_NUM];
for (int i = 0; i < ROWS_NUM; i++) {
for (int j = 0; j < COLS_NUM; j++) {
board[i][j] = blankChar;
}
}
// Human moves first by default.
isHumanMove = true;
// computerPlayer = new RandomAI();
computerPlayer = new Minimax_AlphaBeta();
moveMaked = 0;
moveMsg = new Connect4MoveMessage();
setChanged();
notifyObservers(moveMsg);
}*/
public void setComputerPlayer(ComputerPlayer another) {
this.computerPlayer = another;
}
/**
* Return a character on the board at row x and column y.
*
* @param x The row number.
* @param y The column number.
* @return A character on the board at row x and column y.
* @throws IllegalArgumentException when x is not between 0 and ROWS_NUM or y is
* not between 0 and COLS_NUM.
*/
public char getObjectAt(int x, int y) throws model.IllegalArgumentException {
if ((x >= this.ROWS_NUM || x < 0) || (y >= this.COLS_NUM || y < 0)) {
throw new model.IllegalArgumentException("Invalid arguments.");
}
return this.board[x][y];
}
/**
* Set an object at cell (x, y) to a character.
*
* @param x stands for row
* @param y stands for column
* @param obj The character representation of any player (can be blank).
* @throws IllegalArgumentException when x is not between 0 and ROWS_NUM or y is
* not between 0 and COLS_NUM.
*/
public void setObjectAt(int x, int y, char obj) throws model.IllegalArgumentException {
if ((x >= this.ROWS_NUM || x < 0) || (y >= this.COLS_NUM || y < 0)) {
throw new model.IllegalArgumentException("Invalid arguments.");
}
this.board[x][y] = obj;
if (obj == this.getComputerChar()) {
moveMsg = new Connect4MoveMessage(this.board[0].length - y - 1, x , 2);
} else
moveMsg = new Connect4MoveMessage(this.board[0].length - y - 1, x , 1);
setChanged();
notifyObservers(moveMsg);
}
/**
* Check if a cell is empty or not.
*
* @param x stands for row
* @param y stand for column
* @return true if the cell (x,y) is empty.
*/
public boolean isBlank(int x, int y) {
return this.board[x][y] == blankChar;
}
/**
* Return the maximum row of the board.
*
* @return The maximum row of the board.
*/
public int getMaxRow() {
return this.ROWS_NUM;
}
/**
* Return the maximum column of the board.
*
* @return The maximum column of the board.
*/
public int getMaxCol() {
return this.COLS_NUM;
}
/**
* Accessor to the model's character representation of the player.
*
* @return A character representing the player.
*/
public char getHumanChar() {
return humanChar;
}
/**
* Accessor to the model's character representation of the computer player.
*
* @return A character representing the computer.
*/
public char getComputerChar() {
return computerChar;
}
/**
* Accessor to the model's character representation of an empty slot.
*
* @return A blank character.
*/
public char getBlankChar() {
return blankChar;
}
/**
* An accessor to isHumanMove.
*
* @return Return true if it's currently the human's move.
*/
public boolean isHumanMove() {
return this.isHumanMove;
}
public void setHumanTurn(boolean isHumanTurn) {
isHumanMove = isHumanTurn;
}
public void switchTurn() {
this.moveMaked++;
isHumanMove = !isHumanMove;
// this.notifyObservers();
}
/**
* Check the winning condition by column.
*
* @param playerChar A character that represents the player.
* @return True if playerChar wins the game by column and false otherwise.
*/
public boolean wonByCol(char playerChar) {
for (int i = 0; i < ROWS_NUM; i++) {
int colSum = 0;
for (int j = 0; j < COLS_NUM; j++) {
if (board[i][j] == playerChar) {
colSum++;
if (colSum == CONNECT_SIZE)
return true;
} else
colSum = 0;
}
}
return false;
}
/**
* Check the winning condition by row.
*
* @param playerChar A character that represents the player.
* @return True if playerChar wins the game by row and false otherwise.
*/
public boolean wonByRow(char playerChar) {
for (int i = 0; i < COLS_NUM; i++) {
int rowSum = 0;
for (int j = 0; j < ROWS_NUM; j++) {
if (board[j][i] == playerChar) {
rowSum++;
if (rowSum == CONNECT_SIZE)
return true;
} else
rowSum = 0;
}
}
return false;
}
/**
* Check the winning condition diagonally, both left and right.
*
* @param playerChar A character that represents the player.
* @return True if playerChar wins the game diagonally and false otherwise.
*/
public boolean wonByDiagonal(char playerChar) {
int sum = 0;
// Covering from left to right.
for (int i = 3; i < ROWS_NUM; i++) {
sum = 0;
for (int j = 0; j < COLS_NUM && j <= i; j++) {
if (board[i - j][j] == playerChar) {
sum++;
if (sum == CONNECT_SIZE)
return true;
} else {
sum = 0;
}
}
}
for (int j = 1; j < 3; j++) {
sum = 0;
for (int k = 0; j + k < COLS_NUM; k++) {
// System.out.println(ROWS_NUM - 1 - k + " " + (j + k));
if (board[ROWS_NUM - 1 - k][j + k] == playerChar) {
sum++;
if (sum == CONNECT_SIZE)
return true;
} else
sum = 0;
}
}
// Covering from right to left.
for (int i = 3; i >= 0; i--) {
sum = 0;
for (int j = 0; j < COLS_NUM && (i + j) < ROWS_NUM; j++) {
if (board[i + j][j] == playerChar) {
// System.out.println(i + j + " " + j);
sum++;
if (sum == CONNECT_SIZE)
return true;
} else {
sum = 0;
}
}
}
for (int j = 1; j < 3; j++) {
sum = 0;
for (int k = 0; j + k < COLS_NUM; k++) {
if (board[0 + k][j + k] == playerChar) {
sum++;
if (sum == CONNECT_SIZE)
return true;
} else
sum = 0;
}
}
return false;
}
/**
* Check if a player win the game either by row, column, or diagonal.
*
* @param playerChar A character representing the player who won.
* @return True if playerChar win the game in some ways.
*/
public boolean didWin(char playerChar) {
return wonByRow(playerChar) || wonByCol(playerChar) || wonByDiagonal(playerChar);
}
/**
* Check the tie condition of the game. Tie happens when nobody wins and there's
* no move left.
*
* @return True if it's a tie and false otherwise.
*/
public boolean isTied() {
// Tie condition: Out of moves AND nobody wins.
return (moveMaked == ROWS_NUM * COLS_NUM && !(didWin(computerChar) || didWin(humanChar)));
// Board is filled.
/*
* boolean isFilled = true; for (int i = 0; i < board.length; i++) { for (int j
* = 0; j < board[0].length; j++) { if (board[i][j] == '_') { isFilled = false;
* break; } } } return (isFilled && !(didWin(computerChar) ||
* didWin(humanChar)));
*/
}
/**
* Prompts the computer to make a move.
*
* @param c The character token that will belong to the move.
*/
public void computerMove(char c, boolean max) {
boolean foundAMove = false;
while (!foundAMove) {
int colMove = computerPlayer.getMove(this, max);
if (colMove == -1)
return;
for (int i = 0; i < COLS_NUM; i++) {
if (board[colMove][i] == blankChar) {
board[colMove][i] = c;
if (c == this.computerChar)
moveMsg = new Connect4MoveMessage(board[0].length - i - 1, colMove , 2);
else
moveMsg = new Connect4MoveMessage(board[0].length - i - 1, colMove, 1);
foundAMove = true;
break;
}
}
}
setChanged();
notifyObservers(moveMsg);
}
/**
*
* @return A 2d characters array that represents the board.
*/
public char[][] getBoard() {
return this.board;
}
/**
*
* @return True if the game is already over.
*/
public boolean isOver() {
return this.didWin(this.getComputerChar()) || this.didWin(this.getHumanChar()) || this.isTied();
}
/**
* @return A move message that is used to update the observers.
*/
public Connect4MoveMessage getMoveMsg() {
return this.moveMsg;
}
/**
*
*/
public void printRawBoard() {
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[i].length; j++) {
System.out.print(board[i][j] + " ");
}
System.out.println();
}
}
/**
* Set the reference of the character array board to this.board. Its only
* purpose is to self assign a given board to model's subclass, in which
* this.board is not visible.
*
* @param board A character array board.
*/
public void setBoard(char[][] board) {
this.board = board;
}
/**
* Set the move made for this object. Similar to setBoard(), only to do a deep
* copy of model's subclass object. It shouldn't be called in other cases.
*
* @param m The number of moves that should be assigned to this instance's move.
*/
public void setMoveMaked(int m) {
this.moveMaked = m;
}
/**
* @return The number of moves that have been made in this model.
*/
public int getMoveMaked() {
return moveMaked;
}
}

@ -0,0 +1,51 @@
package model;
import java.io.Serializable;
/**
* This class encapsulates the rows, columns, and color information of the move.
* An instance of this class is created and sent by model to update the view.
*
*
*/
public class Connect4MoveMessage implements Serializable {
public static int YELLOW = 1;
public static int RED = 2;
private static final long serialVersionUID = 1L;
private int row;
private int col;
private int color;
/**
* Constructs a connect4 message object with the given parameters.
*
* @param row An integer for the row.
* @param col An integer for the column.
* @param color an integer 1 or 2 representing colors.
*/
public Connect4MoveMessage(int row, int col, int color) {
this.row = row;
this.col = col;
this.color = color;
}
/**
*
* @return The row number of this move message.
*/
public int getRow() { return row; }
/**
* @return The column number of this move message.
*/
public int getColumn() { return col; }
/**
* @return The color number either 1 or 2 for this move message.
*/
public int getColor() { return color; }
}

@ -0,0 +1,7 @@
package model;
public class IllegalArgumentException extends Exception {
public IllegalArgumentException(String msg) {
super(msg);
}
}

@ -0,0 +1,396 @@
package model;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* This class implements the minimax alpha-beta pruning algorithm.
*
* @author Minh Bui
*
*/
public class Minimax_AlphaBeta implements ComputerPlayer {
/**
*
*/
private static final long serialVersionUID = -8226404171733596155L;
/**
* Specify the maximum depth for the search tree.
*/
private final int init_depth = 5;
/**
* Comment: Since minimax looks at all of the nodes in the search tree, we need
* a class that is similar to the game model.
*
* It's quite inefficient since we have to create many instances of this class
* for each tree node.
*
* @author Minh Bui
*/
private class GameState extends Connect4Model {
/**
* Java generated serialize UID. Since GameState extends Connect4Model, it's
* just here for the sake of surpressing warning.
*/
private static final long serialVersionUID = 2L;
/**
* Create a GameState object given a game model.
*
* @param gameModel
* The given game model.
*/
public GameState(Connect4Model gameModel) {
// Create a deep copy of the board array.
char[][] board = new char[gameModel.getMaxRow()][gameModel.getMaxCol()];
char[][] orig_board = gameModel.getBoard();
for (int i = 0; i < board.length; i++) {
board[i] = Arrays.copyOf(orig_board[i], orig_board[i].length);
}
// Use setObjectAt instead of setBoard because it's a bad design.
this.setBoard(board);
this.setHumanTurn(gameModel.isHumanMove());
this.setMoveMaked(gameModel.getMoveMaked());
}
/**
* Create a GameState object given the crucial information for a game state.
* Used to make a deep copy of another game state.
*
* @param board
* The board that models the game connect4.
* @param isHumanTurn
* True if it's currently the human's turn in the given state.
* @param m
* The number of moves made in the given state.
*/
public GameState(char[][] board, boolean isHumanTurn, int m) {
// Create a deep copy of the board array.
char[][] copy_board = new char[board.length][board[0].length];
for (int i = 0; i < board.length; i++)
copy_board[i] = Arrays.copyOf(board[i], board[0].length);
this.setBoard(copy_board);
this.setHumanTurn(isHumanTurn);
this.setMoveMaked(m);
}
/**
* @return Returns a list of possible moves for this game state.
*/
private List<Integer> getLegalMoves() {
List<Integer> availableMoves = new ArrayList<>();
if (this.isOver())
return availableMoves;
for (int i = 0; i < this.getBoard().length; i++) {
if (this.getBoard()[i][this.getBoard()[0].length - 1] == this.getBlankChar()) {
availableMoves.add(i);
}
}
return availableMoves;
}
}
/**
* Given a game model object, use alpha-beta pruning minimax algorithm to
* calculate and return the next optimal move.
*
* @param gameModel
* The given game model object.
* @param max
* True if the agent is playing to maximize its utility and false in
* the case of minimizing utility.
*/
@Override
public int getMove(Connect4Model gameModel, boolean max) {
GameState currentState = new GameState(gameModel);
List<Integer> legalMoves = currentState.getLegalMoves();
if (legalMoves.isEmpty())
return -1;
int bestMove = 0;
double bestUtil = 0;
if (max) {
// Need to get the first available's move utility to have an initial value to
// compare to.
bestMove = legalMoves.get(0);
GameState nextState1 = getNextState(currentState, bestMove);
bestUtil = minValue(init_depth, nextState1, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
// Get the move with the minimum utility value.
for (int i = 1; i < legalMoves.size(); i++) {
double thisMoveUtil = minValue(init_depth, getNextState(currentState, legalMoves.get(i)),
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
if (bestUtil < thisMoveUtil) {
bestUtil = thisMoveUtil;
bestMove = legalMoves.get(i);
}
}
} else {
// Need to get the first available's move utility to have an initial value to
// compare to.
bestMove = legalMoves.get(0);
GameState nextState1 = getNextState(currentState, bestMove);
bestUtil = maxValue(init_depth, nextState1, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
// Get the move with the maximum utility value.
for (int i = 1; i < legalMoves.size(); i++) {
double thisMoveUtil = maxValue(init_depth, getNextState(currentState, legalMoves.get(i)),
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
if (bestUtil > thisMoveUtil) {
bestUtil = thisMoveUtil;
bestMove = legalMoves.get(i);
}
}
}
System.out.println("Computer bestmove: " + bestMove);
System.out.println("Utility: " + bestUtil);
return bestMove;
}
/**
* Calculate the max value for the current state. (State or node for the human
* player)
*
* @param depth
* The current depth of the search tree.
* @param state
* The current game state that needs to a minimax value.
* @param alpha
* The value of the best choice we have found so far in the MAX path.
* @param beta
* The value of the best choice we have found so far in the MIN path.
* @return The max value for the current state.
*/
private double maxValue(int depth, GameState state, double alpha, double beta) {
if (depth == 0 || state.isOver())
return utilityOf(state);
double v = Double.NEGATIVE_INFINITY;
for (int move : state.getLegalMoves()) {
v = Math.max(v, minValue(depth - 1, getNextState(state, move), alpha, beta));
if (v >= beta)
return v;
alpha = Math.max(alpha, v);
}
/*
* state.printRawBoard(); System.out.println(v);
*/
return v;
}
/**
* Calculate the min value for the current state. (State or node for the
* computer player)
*
* @param depth
* The current depth of the search tree.
* @param state
* The current game state that needs to a minimax value.
* @param alpha
* The value of the best choice we have found so far in the MAX path.
* @param beta
* The value of the best choice we have found so far in the MIN path.
* @return The min value for the current state.
*/
private double minValue(int depth, GameState state, double alpha, double beta) {
if (depth == 0 || state.isOver())
return utilityOf(state);
double v = Double.POSITIVE_INFINITY;
for (int move : state.getLegalMoves()) {
v = Math.min(v, maxValue(depth - 1, getNextState(state, move), alpha, beta));
if (v <= alpha)
return v;
beta = Math.min(beta, v);
}
/*
* System.out.println("In min value"); state.printRawBoard();
* System.out.println(v);
*/
return v;
}
/**
* Given a move and the current game state, generate the next game state.
*
* @param curState
* the current game state
* @param move
* The move that will be made in the next state.
* @return The next state with the move.
*/
private GameState getNextState(GameState curState, int move) {
GameState nextState = new GameState(curState.getBoard(), curState.isHumanMove(), curState.getMoveMaked());
// curState.printRawBoard();
try {
char c = '\0';
if (curState.isHumanMove())
c = curState.getHumanChar();
else
c = curState.getComputerChar();
for (int i = 0; i < nextState.getMaxCol(); i++) {
if (curState.isBlank(move, i)) {
nextState.setObjectAt(move, i, c);
nextState.switchTurn();
break;
}
}
} catch (Exception e) {
}
/*
* System.out.println(""); nextState.printRawBoard(); System.out.println("");
*/
return nextState;
}
/**
* Utility function assigning the utility values to terminal states of the game.
* Need a better evaluation function for non-terminal state. Right now we are
* relying on randomness.
*
* @param curState
* @return A number indicating the utility (score) for the given state.
*/
private double utilityOf(GameState curState) {
if (curState.isTied())
return 0;
else if (curState.didWin(curState.getHumanChar()))
return 10;
else if (curState.didWin(curState.getComputerChar()))
return -10;
else {
// return Math.random() * -10 + Math.random() * 10;
double score = countConsecutives(curState, curState.getHumanChar(), 2)
+ countConsecutives(curState, curState.getHumanChar(), 3) * 2
- countConsecutives(curState, curState.getComputerChar(), 2)
- countConsecutives(curState, curState.getComputerChar(), 3) * 2;
if (score == 0)
return Math.random() * -10 + Math.random() * 10;
else
return score;
}
}
/**
* Scan the board and count for n consecutive character c that is right before a
* a blank character. Only does so partially for the sake of improving the evaluation
* function.
*
* @param s the current game state
* @param c the character that represent the player
* @param n the number of consecutive characters.
* @return
*/
private int countConsecutives(GameState s, char c, int n) {
int count = 0;
// Count n characters in columns.
try {
for (int i = 0; i < s.getMaxRow(); i++) {
int colSum = 0;
for (int j = 0; j < s.getMaxCol(); j++) {
if (colSum == n) {
if (s.getObjectAt(i, j) == s.getBlankChar())
count++;
}
if (s.getObjectAt(i, j) == c) {
colSum++;
} else
colSum = 0;
}
}
// Count n characters in rows.
for (int i = 0; i < s.getMaxCol(); i++) {
int rowSum = 0;
for (int j = 0; j < s.getMaxRow(); j++) {
if (rowSum == n) {
if (s.getObjectAt(j, i) == s.getBlankChar()) {
count++;
}
if (s.getObjectAt(j, i) == c) {
rowSum++;
} else
rowSum = 0;
}
}
}
int sum = 0;
// Count n characters diagonally.// Covering from left to right.
for (int i = 3; i < s.getMaxRow(); i++) {
sum = 0;
for (int j = 0; j < s.getMaxCol() && j <= i; j++) {
if (sum == n) {
if (s.getObjectAt(i - j, j) == s.getBlankChar())
count++;
}
if (s.getObjectAt(i - j, j) == c) {
sum++;
} else {
sum = 0;
}
}
}
for (int j = 1; j < 3; j++) {
sum = 0;
for (int k = 0; j + k < s.getMaxCol(); k++) {
if (sum == n) {
if (s.getObjectAt(s.getMaxRow() - 1 - k, j + k) == s.getBlankChar())
count++;
}
if (s.getObjectAt(s.getMaxRow() - 1 - k, j + k) == c) {
sum++;
} else
sum = 0;
}
}
// Covering from right to left.
for (int i = 3; i >= 0; i--) {
sum = 0;
for (int j = 0; j < s.getMaxCol() && (i + j) < s.getMaxRow(); j++) {
if (sum == n) {
if (s.getObjectAt(i + j, j) == s.getBlankChar())
count++;
}
if (s.getObjectAt(i + j, j) == c) {
sum++;
} else {
sum = 0;
}
}
}
for (int j = 1; j < 3; j++) {
sum = 0;
for (int k = 0; j + k < s.getMaxCol(); k++) {
if (sum == n) {
if (s.getObjectAt(0 + k, j + k) == s.getBlankChar())
count++;
}
if (s.getObjectAt(0 + k, j + k) == c) {
sum++;
} else
sum = 0;
}
}
} catch (model.IllegalArgumentException e) {
}
return count;
}
}

@ -0,0 +1,15 @@
package model;
/**
* The class implements a random strategy for connect4 game.
*/
import model.Connect4Model;
import java.util.Random;
public class RandomAI implements ComputerPlayer {
public int getMove(Connect4Model gameModel, boolean max) {
Random randomGenerator = new Random();
return randomGenerator.nextInt(gameModel.getMaxRow());
}
}

7
view/.gitignore vendored

@ -0,0 +1,7 @@
/Connect4TextView.class
/Connect4View.class
/DialogBox.class
/Connect4View$1.class
/Connect4View$2.class
/Connect4View$3.class
/Connect4View$4.class

@ -0,0 +1,73 @@
/**
* A text view for the Connect4 game.
*/
package view;
import controller.Connect4Controller;
import model.Connect4Model;
import java.util.Scanner;
public class Connect4TextView {
/**
* Controller object for view-model interaction.
*/
private Connect4Controller gameController;
/**
* Scanner object to get user input.
*/
private Scanner in;
public Connect4TextView(Connect4Controller gameController) {
this.gameController = gameController;
in = new Scanner(System.in);
System.out.println("Welcome to Connect 4\n");
}
/**
* Prints out the current state of the game.
*
*/
public void printState() {
System.out.println(this.gameController.gameBoardToString());
System.out.print("You are ");
System.out.println(this.gameController.playerTurnToString());
System.out.println("");
}
/**
* Get a move from user.
*/
public void getMove() {
System.out.println("What column would you like to place your token in?");
while (true) {
int inputMove = in.nextInt();
try {
while (!gameController.placeToken(inputMove)) {
System.out.println("That's not a legit move. Please try again: ");
inputMove = in.nextInt();
}
break;
} catch (model.IllegalArgumentException e) {
System.err.println("That's not a legit move. Please try again: ");
}
}
}
/**
* Print the result of the game. Only 1 of these cases can happen: computer win,
* human win, and a tie.
*
* @param gameModel the game model to gather the state of the game.
*/
public void printResult(Connect4Model gameModel) {
if (gameModel.isTied()) {
System.out.println("Draw.");
} else {
System.out.println(gameModel.didWin(gameModel.getHumanChar()) ? gameModel.getHumanChar() + " wins"
: gameModel.getComputerChar() + " wins");
}
}
}

@ -0,0 +1,343 @@
/**
* GUI view for Connect4.
*
* @author Minh Bui
*/
package view;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Observable;
import controller.Connect4Controller;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.TilePane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import model.Connect4Model;
import model.Connect4MoveMessage;
public class Connect4View extends TilePane implements java.util.Observer, Runnable {
private Connect4MoveMessage moveMessage;
private Connect4Model gameModel;
private Connect4Controller controller;
private final int CIRCLE_RADIUS = 20;
public int getCircleRadius() {
return CIRCLE_RADIUS;
}
public int getSpace() {
return SPACE;
}
private final int SPACE = 8;
private static final Color HUMAN_COLOR = Color.YELLOW;
private static final Color COMPU_COLOR = Color.RED;
private static final String SERIALIZED_MODEL_NAME = "save_game.dat";
public Connect4View(Connect4Model gameModel, Connect4Controller gameController, boolean foundSerializedFile,
boolean isHuman, boolean isNetworkGame, boolean moveFirst) {
this.gameModel = gameModel;
this.controller = gameController;
Insets inset = new Insets(SPACE, SPACE, SPACE, SPACE);
this.setPadding(inset);
this.setBackground(new Background(new BackgroundFill(Color.BLUE, null, null)));
this.setHgap(SPACE);
this.setVgap(SPACE);
boardInit();
if (!foundSerializedFile) {
} else {
updateTile();
}
if (isNetworkGame) {
if (isHuman) {
this.setOnMouseClicked((event) -> {
System.out.println("This is a network game and is played by human.");
double x = event.getSceneX();
int col = 0;
if (x >= 0 && x <= CIRCLE_RADIUS * 2 + SPACE + SPACE / 2)
col = 0;
else if (x >= (CIRCLE_RADIUS * 2 + SPACE + SPACE / 2 + (CIRCLE_RADIUS * 2 + SPACE) * 5))
col = 6;
else {
col = (int) x / (CIRCLE_RADIUS * 2 + SPACE);
}
try {
if (!controller.placeToken(col)) {
Platform.runLater(new Runnable() {
public void run() {
showAlert(true, "Column fill, pick somewhere else!");
}
});
} else {
//this.gameModel.switchTurn();
}
} catch (model.IllegalArgumentException e) {
}
});
} else {
//this.gameModel.computerMove(controller.playerTurnToString().charAt(0), moveFirst);
//this.gameModel.switchTurn();
}
} else {
this.setOnMouseClicked((event) -> {
System.out.println("Normal game.");
double x = event.getSceneX();
try {
if (!this.gameModel.isOver()) {
int col = 0;
if (x >= 0 && x <= CIRCLE_RADIUS * 2 + SPACE + SPACE / 2)
col = 0;
else if (x >= (CIRCLE_RADIUS * 2 + SPACE + SPACE / 2 + (CIRCLE_RADIUS * 2 + SPACE) * 5))
col = 6;
else {
col = (int) x / (CIRCLE_RADIUS * 2 + SPACE);
}
if (!controller.placeToken(col)) {
showAlert(true, "Column fill, pick somewhere else!");
} else {
this.gameModel.switchTurn();
if (this.gameModel.isOver()) {
// Delete the serialize file if it exists.
deleteSerializedFile();
return;
}
this.gameModel.computerMove(this.gameModel.getComputerChar(), false);
this.gameModel.switchTurn();
}
updateTile();
} else {
// Delete the serialized file if it exists.
deleteSerializedFile();
}
} catch (model.IllegalArgumentException e) {
}
});
}
}
/**
* Delete the serialized file if it exists.
*/
public void deleteSerializedFile() {
File savData = new File("./" + SERIALIZED_MODEL_NAME);
if (savData.exists()) {
System.out.println("save_data.dat exists. Deleting.");
savData.delete();
}
}
/**
* Initialize every cell in this board to white color.
*/
private void boardInit() {
for (int i = 0; i < gameModel.getMaxCol() * gameModel.getMaxRow(); i++)
this.getChildren()
.add(new Circle(CIRCLE_RADIUS + SPACE, CIRCLE_RADIUS + SPACE, CIRCLE_RADIUS, Color.WHITE));
}
/**
* This method is called whenever there's a change in the game's board.
*/
private void updateTile() {
char[][] board = gameModel.getBoard();
// Note:
// Changing from creating a new circle to only changing the color of the circle.
// Much more efficient.
// Credit: Weixiang.
for (int j = gameModel.getMaxCol() - 1; j >= 0; j--) {
for (int i = 0; i < gameModel.getMaxRow(); i++)
if (board[i][j] == gameModel.getBlankChar()) {
// this.getChildren().set(((gameModel.getMaxCol() - j - 1) * board.length + i),
// new Circle(CIRCLE_RADIUS + SPACE, CIRCLE_RADIUS + SPACE, CIRCLE_RADIUS,
// Color.WHITE));
((Circle) this.getChildren().get(((gameModel.getMaxCol() - j - 1) * board.length + i)))
.setFill(Color.WHITE);
} else if (board[i][j] == gameModel.getHumanChar()) {
// this.getChildren().set(((gameModel.getMaxCol() - 1 - j) * board.length + i),
// new Circle(CIRCLE_RADIUS + SPACE, CIRCLE_RADIUS + SPACE, CIRCLE_RADIUS,
// HUMAN_COLOR));
((Circle) this.getChildren().get(((gameModel.getMaxCol() - j - 1) * board.length + i)))
.setFill(HUMAN_COLOR);
} else if (board[i][j] == gameModel.getComputerChar()) {
// this.getChildren().set(((gameModel.getMaxCol() - 1 - j) * board.length + i),
// new Circle(CIRCLE_RADIUS + SPACE, CIRCLE_RADIUS + SPACE, CIRCLE_RADIUS,
// COMPU_COLOR));
((Circle) this.getChildren().get(((gameModel.getMaxCol() - j - 1) * board.length + i)))
.setFill(COMPU_COLOR);
}
}
/**
* COMMENT: Connect4MoveMessage class seems redundant here.
*/
/*
*
*
*
* Connect4MoveMessage moveMessage = gameModel.getmoveMessage();
*
* // Fill out the circle in the slot that has been changed. if
* (moveMessage.getPlayer() == gameModel.getComputerChar()) {
* this.getChildren().set( ((gameModel.getMaxCol() - 1 - moveMessage.getY()) *
* gameModel.getMaxRow() + moveMessage.getX()), new Circle(CIRCLE_RADIUS +
* SPACE, CIRCLE_RADIUS + SPACE, CIRCLE_RADIUS, COMPU_COLOR)); } else if
* (moveMessage.getPlayer() == gameModel.getHumanChar()) {
* this.getChildren().set( ((gameModel.getMaxCol() - 1 - moveMessage.getY()) *
* gameModel.getMaxRow() + moveMessage.getX()), new Circle(CIRCLE_RADIUS +
* SPACE, CIRCLE_RADIUS + SPACE, CIRCLE_RADIUS, HUMAN_COLOR)); }
*/
}
/**
* This method issues a message to the user. In this project, it's used to
* declare who's the winner or if the game's tied. It also indicates if there
* was an error or not.
*
* @param error true if the issued message is an error and false if it's a
* normal message.
* @param msg A string for message.
*/
private void showAlert(boolean error, String msg) {
Alert alert = null;
if (error) {
alert = new Alert(AlertType.ERROR);
alert.setTitle("Error");
} else
alert = new Alert(AlertType.INFORMATION);
alert.setContentText(msg);
alert.showAndWait();
}
private void sub_update(Object arg) {
moveMessage = (Connect4MoveMessage) arg;
System.out.println("Row: " + moveMessage.getRow() + " Col: " + moveMessage.getColumn());
// Second player is 2 (red). First player is 1 (yellow).
if (moveMessage.getColor() == 2) {
int ind = (moveMessage.getRow()) * gameModel.getMaxRow() + moveMessage.getColumn();
((Circle) this.getChildren().get(ind)).setFill(COMPU_COLOR);
} else if (moveMessage.getColor() == 1) {
int ind = (moveMessage.getRow()) * gameModel.getMaxRow() + moveMessage.getColumn();
((Circle) this.getChildren().get(ind)).setFill(HUMAN_COLOR);
}
}
@Override
public void update(Observable o, Object arg) {
sub_update(arg);
gameModel = (Connect4Model) o;
if (gameModel.didWin(gameModel.getHumanChar())) {
Platform.runLater(new Runnable() {
public void run() {
showAlert(false, "First player win!");
}
});
//showAlert(false, "You win!");
deleteSerializedFile();
} else if (gameModel.didWin(gameModel.getComputerChar())) {
Platform.runLater(new Runnable() {
public void run() {
showAlert(false, "Second player win!");
}
});
//showAlert(false, "You lose!");
deleteSerializedFile();
} else if (gameModel.isTied()) {
Platform.runLater(new Runnable() {
public void run() {
showAlert(false, "Tied game.");
}
});
//showAlert(false, "Tied game.");
deleteSerializedFile();
}
}
/**
* Serialize the game model object.
*/
public void serializeGameModel() {
try {
FileOutputStream outputFile = new FileOutputStream("./" + SERIALIZED_MODEL_NAME);
ObjectOutputStream oos = new ObjectOutputStream(outputFile);
oos.writeObject(gameModel);
oos.close();
outputFile.close();
} catch (IOException ioe) {
System.err.println(ioe.getMessage());
}
}
/**
* Returns true if found a serialized file to deserialize from and false
* otherwise.
*
* @return True if found a serialized file to deserialize from.
*/
public boolean deserializeGameModel() {
try {
FileInputStream inputFile = new FileInputStream("./" + SERIALIZED_MODEL_NAME);
ObjectInputStream ois = new ObjectInputStream(inputFile);
gameModel = (Connect4Model) ois.readObject();
ois.close();
inputFile.close();
return true;
} catch (IOException ioe) {
System.out.println(ioe.getMessage());
return false;
} catch (ClassNotFoundException ex) {
System.err.println(ex.getStackTrace());
return false;
}
}
public Connect4Controller getController() {
return controller;
}
@Override
public void run() {
// TODO Auto-generated method stub
updateTile();
}
}

@ -0,0 +1,158 @@
package view;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* This DialogBox contains networking option for Connect4 networking game.
*
* @author Weixiang Zhang
*
*/
public class DialogBox extends Stage{
private Button ok;
private RadioButton rB1;
private RadioButton rB2;
private RadioButton rB3;
private RadioButton rB4;
private TextField serverName;
private TextField portNumber;
/**
* True if the server option is checked and false otherwise.
*
* @return True if the server option is checked and false otherwise.
*/
public boolean serverIsCheck() {
return rB1.isSelected();
}
/**
* True if human option is checked and false otherwise.
*
* @return True if human option is checked and false otherwise.
*/
public boolean humanIsCheck() {
return rB3.isSelected();
}
/**
*
* @return The server's name in the text field.
*/
public String getServerName() {
return serverName.getText();
}
/**
*
* @return The port number in the text field.
*/
public int getPort() {
return Integer.parseInt(portNumber.getText());
}
/**
* Returns the ok button object.
*
* @return Returns the ok button object.
*/
public Button getOk() {
return ok;
}
/**
* Constructs a dialog box that contains networking options.
*/
public DialogBox() {
super();
GridPane gridPane = new GridPane();
gridPane.setVgap(25);
gridPane.setPadding(new Insets(15, 15, 15, 15));
Scene newScene = new Scene(gridPane);
HBox hBox1 = new HBox();
hBox1.setSpacing(8);
Label label1 = new Label("Create: ");
ToggleGroup group1 = new ToggleGroup();
rB1 = new RadioButton("Server ");
rB2 = new RadioButton("Client");
rB1.setToggleGroup(group1);
rB2.setToggleGroup(group1);
hBox1.getChildren().addAll(label1,rB1,rB2);
gridPane.add(hBox1, 0, 0);
VBox vBox1 = new VBox();
vBox1.setSpacing(25);
HBox hBox2 = new HBox();
hBox2.setSpacing(8);
Label label2 = new Label("Play as: ");
ToggleGroup group2 = new ToggleGroup();
rB3 = new RadioButton("Human ");
rB4 = new RadioButton("Computer");
rB3.setToggleGroup(group2);
rB4.setToggleGroup(group2);
hBox2.getChildren().add(label2);
hBox2.getChildren().add(rB3);
hBox2.getChildren().add(rB4);
HBox hBox3 = new HBox();
hBox3.setSpacing(8);
Label server = new Label("Server");
Label port = new Label("Port");
serverName = new TextField("localhost");
portNumber = new TextField("4000");
hBox3.getChildren().addAll(server, serverName, port, portNumber);
vBox1.getChildren().addAll(hBox2, hBox3);
gridPane.add(vBox1, 0, 1);
HBox hBox4 = new HBox();
hBox4.setSpacing(8);
ok = new Button("OK");
Button cancel = new Button("Cancel");
hBox4.getChildren().addAll(ok, cancel);
gridPane.add(hBox4, 0, 2);
cancel.setOnAction(ActionEvent -> {
this.close();
});
this.setScene(newScene);
}
/**
* True if client button is checked.
*
* @return True if client button is checked and false otherwise.
*/
public boolean clientIsCheck() {
return rB2.isSelected();
}
/**
* True if computer button is checked.
*
* @return True if computer button is checked.
*/
public boolean computerIsCheck() {
return rB4.isSelected();
}
}
Loading…
Cancel
Save