Client-Server Tic Tac Toe

Prompt / Client

For a programming assignment in CSC 460 / Operating Systems, students were tasked to create a 4×4 Tic Tac Toe game consisting of three classes: Client, Dispatcher, and ServerThread.

The Client class is ran by the player and instantiates a socket with the correct hostName and portNumber to connect to the Dispatcher class which handles new players. It creates an inputStream and outputStream with that socket to communicate and creates a PrintWriter and BufferedReader to handle those streams.

After creating the connection with the dispatcher, the Client class creates the 4×4 Tic Tac Toe board and calls its playgame function

The playgame function takes the BufferedReader and PrintWriter to communicate with the serverThread later on. After creating a scanner to read the player’s turn and booleans turn and gameover to track the game, we enter a while loop that runs until the gameover boolean is true.

if (turn == true){ is for the player’s turn. We use a do/while to take the user’s turn until they provide a valid turn, checking that the row and column are both in the bounds of the board and not already taken. If the turn is valid, we set tha=ose coordinates to ‘O’ and use out.println to sent the move to the serverThread before setting the turn back to false.

else { is the CPU’s turn. We assign the CPU’s message to the String input and parse its contents with .split, assigning the different parts to String[] data. If the first part of the message is “CLIENT” then that’s the first move of the game and the CPU has decided it is the player’s turn to go first so it sets the turn to true. Otherwise we check the length of data to see if there is an endgame condition attached to the end of the message such as “WIN”, “TIE”, or “LOSE”. We print the appropriate message to the player and switch the gameover boolean. If the message doesn’t have an endgame message, we take the coordinates, print out the CPU’s move to the player, and update our board.

The while loop will continue until the gameover boolean is switched, after which we print the final gameboard.

The only other function in this class is the printboard() function that simply uses two for loops to iterate through the board and print the current Xs and Os in a grid.

import java.io.*;
import java.net.*;
import java.util.*;

public class Client {
    private static String hostName = "localhost";
    private static DataInputStream inputStream;
    private static DataOutputStream outputStream;
    private static PrintWriter out;
    private static BufferedReader cpuin;
    private static Socket toServerSocket;
    private static char[][] board;
    private static int row, col;

    public static void main(String[] args) {
        int portNumber = 9877;
        try {
            toServerSocket = new Socket(hostName, portNumber);
            inputStream = new DataInputStream(toServerSocket.getInputStream());
            outputStream = new DataOutputStream(toServerSocket.getOutputStream());
            out = new PrintWriter(outputStream, true);
            cpuin = new BufferedReader(new InputStreamReader(inputStream));
        } catch (IOException e) {
            System.err.println("Error: Failed to open I/O streams");
            System.exit(1);
        }
        board = new char[4][4];
        for (int x = 0; x <= 3; x++)
            for (int y = 0; y <= 3; y++)
                board[x][y] = ' ';
        row = -1;
        col = -1;
        try {
            playgame(cpuin, out);
        } catch (IOException e) {
            System.err.println("Error: Failed to start game");
            System.exit(1);
        }
    }
    public static void playgame(BufferedReader cpuin, PrintWriter out) throws IOException {
        Scanner inp = new Scanner(System.in);
        boolean turn = false;
        boolean gameover = false;

        while (!gameover) {
            if (turn == true) {
                do {
                    printboard();
                    System.out.print("\n    Enter your move (row and column) : ");
                    String move = inp.nextLine();
                    String[] parts = move.split("\\s+");
                    if (parts.length == 2) {
                        row = Integer.parseInt(parts[0]);
                        col = Integer.parseInt(parts[1]);
                    } else {
                        row = 4;
                        col = 4;
                    }
                } while (row < 0 || row > 3 || col > 3 || col < 0 || board[row][col] != ' ');
                board[row][col] = 'O';
                out.println("MOVE " + row + " " + col);
                turn = false;
            }
            else {
                String input = cpuin.readLine();
                String[] data;
                data = input.split(" ");
                if (data[0].equals("CLIENT")) {}
                else{
                    if (data.length > 3){
                        // game result
                        if (data[3].equals("WIN")) {
                            System.out.println("\nCongratulations! You Win");
                        }
                        if (data[3].equals("TIE")) {
                            System.out.println("\nThe game ended in a tie!");
                        }
                        if (data[3].equals("LOSE")) {
                            System.out.println("\nSorry, you lost!");
                        }
                        gameover = true;
                    }
                    else {
                        row = Integer.parseInt(data[1]);
                        col = Integer.parseInt(data[2]);
                        System.out.println("\nCPU MOVE: " + row + " " + col + "\n");
                        board[row][col] = 'X';
                    }
                }
                turn = true;
            }
        }
        System.out.println("\nHere is the final game board\n");
        printboard();
    }
    private static void printboard() {
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                System.out.print(board[i][j] + " ");
                if (j != 3) {
                    System.out.print("|");
                }
            }
            System.out.println();
            if (i != 3) {
                System.out.print("___________");
                System.out.println();
            }
        }
    }
}

Dispatcher

The Dispatcher class is the first we run when starting the game. It attaches its listener serverSocket to the port 9877 and creates a new socket client.

Inside of a constant while loop, limited by the number of maxConnections, we accept new players through our port and assign them to our client socket and pass it to a new ServerThread called gameThread.

import java.net.*;
import java.io.*;

public class Dispatcher {
    private static int port=9877, maxConnections=0;

    public static void main(String[] args) {
        int i=0;
        try{
            ServerSocket listener = new ServerSocket(port);
            Socket client ;
            while((i++ < maxConnections) || (maxConnections == 0)){
                client = listener.accept();
                ServerThread gameThread = new ServerThread(client);
            }
        } catch (IOException ioe) {
            System.out.println("IOException on socket listen: " + ioe);
            ioe.printStackTrace();
        }
    }
}

ServerThread

The ServerThread class is the longest of the three since it not only handles the player’s turn and generates its own, but also checks for endgame conditions after each turn.

The ServerThread class is passed Socket s which is the client. It creates its own inputStream and outputStream like the client, making a PrintWriter and BufferedReader and a board before calling its function run().

run() starts by creating an int counter to track the turns and check for a Tie scenario. String response will hold the player’s move which will be parsed out later on. Boolean gameover serves the same purpose as in Client but in ServerThread rather than just setting turn to false we randomly choose true or false to determine who moves first.

We enter a while loop like in Client that continues until gameover is true and start by checking if turn == true. If it is then it is the player’s turn and we use realLine() to assign their turn to the String response. We assign their turn to a row and col integer respectively and add an ‘O’ at those coordinates.

After increasing the counter by one we check if either the checkwin() function returns true or if the counter equals 16, in which case we return the appropriate message for each and set gameover to true. Lastly we set turn to true to make it the CPU’s turn.

else { holds the CPU’s turn which is much more simple since we call outside functions to do certain tasks. We start by calling makemove() which generates a random row and column until it finds an empty one. We then increment counter and append our board with an ‘X’. Like with the player’s turn we check if checkwin() returns true or if counter is 16, printing the appropriate messages and set gameover to true. If there is no endgame message to send we use out.println() to send the CPU’s turn to the player and set turn to true.

the private boolean function checkwin() is used to check the board for any “four in a row” combinations that would end the game. The first two for loops check the rows and columns respectively for straight lines while the last two if statements check both diagonals. If any of the if statements are true, we return true to signal a win, otherwise we return false.

import java.net.*;
import java.io.*;
import java.util.Random;

public class ServerThread extends Thread {
    private Socket toclientsocket;
    private DataInputStream instream;
    private DataOutputStream outstream;
    private PrintWriter out;
    private BufferedReader in;
    private Random gen;
    private char[][] board;
    private int row, col;

    public ServerThread(Socket s) throws IOException {
        toclientsocket = s;
        gen = new Random();
        instream = new DataInputStream(toclientsocket.getInputStream());
        outstream = new DataOutputStream(toclientsocket.getOutputStream());
        out = new PrintWriter(outstream, true);
        in = new BufferedReader(new InputStreamReader(instream));
        board = new char[4][4];
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                board[i][j] = ' ';
            }
        }
        row = -1;
        col = -1;
        run();
    }
    public void run() {
        int counter = 0;
        String response = "";
        boolean gameover = false;
        boolean turn = (gen.nextInt() % 2 == 0) ? true : false;
        if (turn) {
            out.println("CLIENT");
            System.out.println("CLIENT");
        }
        while (!gameover) {
            if (turn == true) {
                try{
                    response = in.readLine();
                    String[] data = response.split(" ");
                    row = Integer.parseInt(data[1]);
                    col = Integer.parseInt(data[2]);
                }
                catch (IOException e){
                    System.out.println("An Error Occurred While Getting Player's Turn");
                    System.exit(1);
                }
                board[row][col] = 'O';
                counter++;
                if (checkwin() || counter == 16) {
                    gameover = true;
                    if(checkwin()) {
                        out.println("MOVE -1 -1 WIN");
                    }
                    else {
                        out.println("MOVE -1 -1 TIE");
                    }
                }
                turn = false;
            }
            else {
                makemove();
                counter++;
                board[row][col] = 'X';
                if (checkwin() || counter == 16) {
                    gameover = true;
                    if (checkwin()) {
                        out.println("MOVE " + row + " " + col + " LOSE");
                    } else {
                        out.println("MOVE " + row + " " + col + " TIE");
                    }
                }
                else{
                    out.println("MOVE " + row + " " + col);
                }
                turn = true;
            }
        }
    }
    public void makemove() {
        do {
            row = gen.nextInt(4);
            col = gen.nextInt(4);
        }
        while (board[row][col] != ' ');
    }
    private boolean checkwin() {
        for (int i = 0; i < 4; i++) {
            if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][2] == board[i][3] && board[i][0] != ' ') {
                return true;
            }
        }
        for (int j = 0; j < 4; j++) {
            if (board[0][j] == board[1][j] && board[1][j] == board[2][j] && board[2][j] == board[3][j] && board[0][j] != ' ') {
                return true;
            }
        }
        if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[2][2] == board[3][3] && board[0][0] != ' ') {
            return true;
        }
        if (board[0][3] == board[1][2] && board[1][2] == board[2][1] && board[2][1] == board[3][0] && board[3][0] != ' ') {
            return true;
        }
        return false;
    }
}

Playthrough

In the video to the left you can see the player’s terminal as they play a full game against the computer.