4

I'm trying to make a Tic-Tac-Toe Android application, of course in Java. After debugging for maybe a few hours, fixing things here and there, I have come upon a problem that I can not solve by myself at this time, with this knowledge. I have deduced that the problem is found in the AndroidPerform() method where at the first line it is asked to find the best move and place it in the appropriately named variable bestMove, which is an object Move.

public void AndroidPerform(){
    Move bestMove = AndroidMove(NOUGHT);
    placeAMove(bestMove.x, bestMove.y, NOUGHT);
    minimaxActivity.setMove(bestMove.x, bestMove.y, NOUGHT);
}

Now, clearly, by looking at the code above we can see that the method AndroidMove(player) is called which is basically a minimax algorithm, or at least it should be. While debugging I've seen that it does find good moves, but when it comes to this line

Move bestMove = AndroidMove(NOUGHT);

It returns null for x and for y and here is where I get my problem which I can't fix. I've provided the two most important classes, actually it is in these classes where all of the work is done. Anyway, hope you can help, how ever it may be, thank you in advance.

MyGame Class

public class MyGame {

    // Name-constants to represent the seeds and cell contents
    public final int EMPTY = 0;
    public final int CROSS = 1;
    public final int NOUGHT = 2;

    // Name-constants to represent the various states of the game
    public final int PLAYING = 0;
    public final int CROSS_WON = 1;
    public final int NOUGHT_WON = 2;
    public final int DRAW = 3;

    // The game board and the game status
    public static final int ROWS = 3, COLS = 3; // number of rows and columns
    public static int[][] board = new int[ROWS][COLS]; // game board in 2D array
    //  containing (EMPTY, CROSS, NOUGHT)
    public static int currentState;  // the current state of the game
    // (PLAYING, DRAW, CROSS_WON, NOUGHT_WON)
    public static int currentPlayer; // the current player (CROSS or NOUGHT)
    public static int currentRow, currentCol; // current seed's row and column

    public int AndroidPlayer, HumanPlayer;
    MinimaxActivity minimaxActivity = new MinimaxActivity();

    class Move {
        int x, y, score, player;

        public Move(int score){
            this.score = score;
        }

        public Move(int x, int y) {
            this.x = x;
            this.y = y;
        }

        public Move(int x, int y, int player) {
            this.x = x;
            this.y = y;
            this.player = player;
        }


    }

    public void resetBoard() {
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                board[i][j] = 0;
            }
        }
    }

    List<Move> availableMoves;
    public Move AndroidMove(int player) {
        // Computer is always NOUGHT

        // Base case
        int State = CheckGameState();
        if (State == NOUGHT_WON){
            return new Move(10);
        } else if (State == CROSS_WON){
            return new Move(-10);
        } else if (State == DRAW){
            return new Move(0);
        }

        List<Move> moves = getAvailableStates();
        //if (moves.isEmpty()) return new Move(0);

        for (int i = 0; i < ROWS; ++i) {
            for (int j = 0; j < COLS; ++j) {
                 if (board[i][j] == EMPTY){
                     Move move = new Move(i, j, player);
                     placeAMove(i, j, player);

                     if (player == NOUGHT){
                         move.score = AndroidMove(CROSS).score;
                     } else {
                         move.score = AndroidMove(NOUGHT).score;
                     }
                     moves.add(move);

                     placeAMove(i, j, EMPTY);
                 }
            }
        }

        int bestMove = 0;
        if (player == NOUGHT) {
            int bestScore = -1000000;
            for (int i = 0; i < moves.size(); i++) {
                if (moves.get(i).score > bestScore) {
                    bestMove = i;
                    bestScore = moves.get(i).score;
                }
            }
        } else {
            int bestScore = 1000000;
            for (int i = 0; i < moves.size(); i++) {
                if (moves.get(i).score < bestScore) {
                    bestMove = i;
                    bestScore = moves.get(i).score;
                }
            }
        }
        return moves.get(bestMove);
    }

    public void AndroidPerform(){
        Move bestMove = AndroidMove(NOUGHT);
        placeAMove(bestMove.x, bestMove.y, NOUGHT);
        minimaxActivity.setMove(bestMove.x, bestMove.y, NOUGHT);
    }

    public void placeAMove(int x, int y, int player) {
        board[x][y] = player;   //player = 1 for X, 2 for O
    }
    public void placeAMove(Point point, int player) {
        board[point.x][point.y] = player;   //player = 1 for X, 2 for O
    }

    public List<Move> getAvailableStates() {
        availableMoves = new ArrayList<>();
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                if (board[i][j] == EMPTY) {
                    availableMoves.add(new Move(i, j));
                }
            }
        }
        return availableMoves;
    }

    public int CheckGameState() {
        /*
        0 - Playing
        1 - X Won
        2 - O Won
        3 - Draw
         */

        // Check Rows - Horizontal Lines
        for (int i = 0; i< ROWS; i++){
            if (board[i][0] == CROSS &&
                board[i][1] == CROSS &&
                board[i][2] == CROSS){
                return CROSS_WON;
            }
            if (board[i][0] == NOUGHT &&
                board[i][1] == NOUGHT &&
                board[i][2] == NOUGHT){
                return NOUGHT_WON;
            }
        }

        // Check Columns - Vertical Lines
        for (int i = 0; i< COLS; i++){
            if (board[0][i] == CROSS &&
                board[1][i] == CROSS &&
                board[2][i] == CROSS){
                return CROSS_WON;
            }
            if (board[0][i] == NOUGHT &&
                board[1][i] == NOUGHT &&
                board[2][i] == NOUGHT){
                return NOUGHT_WON;
            }
        }

        // Check Diagonal
        if (board[0][0] == CROSS &&
            board[1][1] == CROSS &&
            board[2][2] == CROSS){
            return CROSS_WON;
        }
        if (board[0][0] == NOUGHT &&
            board[1][1] == NOUGHT &&
            board[2][2] == NOUGHT){
            return NOUGHT_WON;
        }


        // Check Reverse-Diagonal
        if (board[0][2] == CROSS &&
            board[1][1] == CROSS &&
            board[2][0] == CROSS){
            return CROSS_WON;
        }
        if (board[0][2] == NOUGHT &&
            board[1][1] == NOUGHT &&
            board[2][0] == NOUGHT){
            return NOUGHT_WON;
        }

        // Check for Tie
        for (int i = 0; i < ROWS; i++) {
            for (int j = 0; j < COLS; j++) {
                if (board[i][j] != CROSS && board[i][j] != NOUGHT){
                    return PLAYING;
                }
            }
        }

        return DRAW;
    }

}

MinimaxActivity Class

public class MinimaxActivity extends AppCompatActivity {

private Board BoardGame;
private MyGame myGame;

private Button mBoardButtons[][];

private TextView mInfoTextView;
private TextView mPlayerOneCount;
private TextView mTieCount;
private TextView mPlayerTwoCount;
private TextView mPlayerOneText;
private TextView mPlayerTwoText;

private int mPlayerOneCounter = 0;
private int mTieCounter = 0;
private int mPlayerTwoCounter = 0;

private Button newGame, exitGame;

public int HUMAN = 1;
public int COMPUTER = 2;
Random random;

private int First=0;
private int Counter = 0;
private boolean isGameOver = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_play);

    mBoardButtons = new Button[3][3];
    mBoardButtons[0][0] = (Button) findViewById(R.id.one);
    mBoardButtons[0][1] = (Button) findViewById(R.id.two);
    mBoardButtons[0][2] = (Button) findViewById(R.id.three);
    mBoardButtons[1][0] = (Button) findViewById(R.id.four);
    mBoardButtons[1][1] = (Button) findViewById(R.id.five);
    mBoardButtons[1][2] = (Button) findViewById(R.id.six);
    mBoardButtons[2][0] = (Button) findViewById(R.id.seven);
    mBoardButtons[2][1] = (Button) findViewById(R.id.eight);
    mBoardButtons[2][2] = (Button) findViewById(R.id.nine);

    newGame = (Button) findViewById(R.id.newGame1);
    exitGame = (Button) findViewById(R.id.exitGame1);
    // Text Fields
    mInfoTextView = (TextView) findViewById(R.id.information);
    mPlayerOneCount = (TextView) findViewById(R.id.humanCount);
    mTieCount = (TextView) findViewById(R.id.tiesCount);
    mPlayerTwoCount = (TextView) findViewById(R.id.androidCount);
    mPlayerOneText = (TextView) findViewById(R.id.human);
    mPlayerTwoText = (TextView) findViewById(R.id.android);
    // Counters
    mPlayerOneCount.setText(Integer.toString(mPlayerOneCounter));
    mTieCount.setText(Integer.toString(mTieCounter));
    mPlayerTwoCount.setText(Integer.toString(mPlayerTwoCounter));

    random = new Random();

    exitGame.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            MinimaxActivity.this.finish();
        }
    });

    final CharSequence[] items = {"Computer", "Player"};

    final AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
    alertDialog.setCancelable(false);
    alertDialog.setTitle("Who goes first?");
    alertDialog.setSingleChoiceItems(items, -1, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int item) {
            if (items[item] == "Computer") {
                First = 1; // Computer
            } else if (items[item] == "Player") {
                First = 2; // Player
            }
            dialog.dismiss();

            //BoardGame = new Board();
            myGame = new MyGame();

            if (First == 1) {
                startNewGame(true); // True For Computer
            }
            if (First == 2) {
                startNewGame(false); // False For Player
            }

        }
    });
    alertDialog.show();

    newGame.setOnClickListener(new View.OnClickListener() { // FIX STARTNEWGAME
        @Override
        public void onClick(View v) {
            if (Counter % 2 == 0) {
                startNewGame(false);
                Counter++;
            } else {
                startNewGame(true);
                Counter++;
            }
            // Here we stop, counter can be used for session concept
        }
    });

    // http://developer.android.com/guide/topics/ui/dialogs.html
    // Adding a persistent multiple-choice or single-choice list
}

private void startNewGame(boolean GoesFirst) {

    MyResetBoard(); // Look at board reset

    mPlayerOneText.setText("Human:");
    mPlayerTwoText.setText("Android:");


    if(GoesFirst){
        // Computer Goes First

        mInfoTextView.setText("Android's Turn.");
        //myGame.AndroidPerform();
        setMove(random.nextInt(3), random.nextInt(3), COMPUTER);
        GoesFirst = false;
    }else{
        //Player Goes First

        mInfoTextView.setText("Human's Turn.");
        GoesFirst = true;
    }
    isGameOver = false;
}

private void MyResetBoard(){
    myGame.resetBoard();
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            mBoardButtons[i][j].setText("");
            mBoardButtons[i][j].setEnabled(true);
            mBoardButtons[i][j].setOnClickListener(new ButtonClickListener(i,j));
            mBoardButtons[i][j].setBackgroundResource(R.drawable.empty);
        }
    }
}

private class ButtonClickListener implements View.OnClickListener {

    int x,y;

    public ButtonClickListener(int i, int j) {
        this.x = i;
        this.y = j;
    }

    @Override
    public void onClick(View v) {
        if (!isGameOver){ // If the game is not over
            if (mBoardButtons[x][y].isEnabled()){
                setMove(x, y, myGame.CROSS); // Human makes a move

                int winner = myGame.CheckGameState();

                if (winner == myGame.PLAYING) { // If still playing
                    mInfoTextView.setText(R.string.turn_computer);
                    myGame.AndroidPerform();
                    //int move = mGame.getComputerMove();
                    //setMove(TicTacToeGame.PLAYER_TWO, move);
                    winner = myGame.CheckGameState();
                }

                if (winner == myGame.PLAYING){
                    mInfoTextView.setText(R.string.turn_human);
                }
                else if (winner == myGame.DRAW) { // If draw
                    mInfoTextView.setText(R.string.result_tie);
                    mTieCounter++;
                    mTieCount.setText(Integer.toString(mTieCounter));
                    isGameOver = true;
                } else if (winner == myGame.CROSS_WON) { // X Won
                    mInfoTextView.setText(R.string.result_human_wins);
                    mPlayerOneCounter++;
                    mPlayerOneCount.setText(Integer.toString(mPlayerOneCounter));
                    isGameOver = true;
                } else if (winner == myGame.NOUGHT_WON){ // O Won
                    mInfoTextView.setText(R.string.result_android_wins);
                    mPlayerTwoCounter++;
                    mPlayerTwoCount.setText(Integer.toString(mPlayerTwoCounter));
                    isGameOver = true;
                }


            }
        }
    }
}

public void setMove(int x, int y, int player){
    myGame.placeAMove(x, y, player);
    mBoardButtons[x][y].setEnabled(false);
    if (player == 1) {
        mBoardButtons[x][y].setBackgroundResource(R.drawable.x);
    } else {
        mBoardButtons[x][y].setBackgroundResource(R.drawable.o);
    }
}
trincot
  • 317,000
  • 35
  • 244
  • 286
Filip Markoski
  • 333
  • 3
  • 19
  • Thanks for posting your code, but can you be more specific as to what the problem is? Is the activity crashing? If so, what's the error message? Is there a problem with the logic or the displaying of the results? If so, what exactly is going wrong? – ajpolt Nov 21 '15 at 20:40

3 Answers3

3

EDIT: Looks like this helped fix the crash but you're looking for help on Minimax now. You're on the right track, but there are a few problems with your implementation. Here's a sample of how I might do it, using some of your code:

public static final int SCORE_DRAW = 0;
public static final int SCORE_WIN = 999;
public static final int SCORE_LOSS = -999;
public static final int SCORE_ILLEGAL_MOVE = Integer.MIN_VALUE;

/**
 * Look at all moves for the specified player, score them, and return the move with the highest score
 *
 * @param currentPlayer the player who will be making the move
 * @param board current state of the board
 *
 * @return the move with the highest score
 */
@NonNull
public Move pickBestMove(int currentPlayer, int[][] board){
    List<Move> availableMoves = getAvailableMoves(board);

    if(availableMoves.size() < 1){
        //Something went wrong.
        throw new IllegalStateException("No available moves to pick from.");
    }

    //Default to the first available move
    Move bestMove = availableMoves.get(0);

    //Iterate through the available moves
    for(Move move : availableMoves){
        move.player = currentPlayer;
        move.score = getScoreForMove(move, board, currentPlayer);

        if(move.score == SCORE_WIN){
            //If the move would win the game, it's obviously the best move.
            return move;
        }else if(move.score > bestMove.score){
            //Found a better move. update bestMove.
            bestMove = move;
        }
    }

    //Return the best one
    return bestMove;
}

/**
 * Score a move by making the move and then assuming each player will play optimally until
 * we reach the end of the game.
 *
 * @param move the move to score
 * @param board current state of the game board. This will be copied.
 * @param scoredPlayer the player for whom to score the move
 *
 * @return a score for the move, assuming each player plays optimally
 */
private int getScoreForMove(@NonNull Move move, int[][] board, int scoredPlayer){
    //Make a copy of the board so we can change it
    int[][] boardCopy = copyArray(board);

    if(boardCopy[move.x][move.y] != EMPTY){
        //this is not a legal move, score negative infinity.
        return SCORE_ILLEGAL_MOVE;
    }else{
        //the move is legal. Update the board with the proposed move
        boardCopy[move.x][move.y] = move.player;

        //Get the game state based on the proposed move.
        int newState = checkGameState(boardCopy);

        //check if it's a draw, win, or a loss
        if(newState == DRAW){
            //This move would cause a draw.
            return SCORE_DRAW;
        }else if(newState == NOUGHT_WON){
            //Somebody wins with this move. Check if it's the player we're scoring
            return scoredPlayer == NOUGHT ? SCORE_WIN : SCORE_LOSS;
        }else if(newState == CROSS_WON){
            //Somebody wins with this move. Check if it's the player we're scoring
            return scoredPlayer == CROSS ? SCORE_WIN : SCORE_LOSS;
        }

        //Game isn't over yet. Assume the next player will make their best possible move,
        //and check what the resulting score would be.
        int nextPlayer = move.player == NOUGHT ? CROSS : NOUGHT;

        //note that we change the player for pickBestMove, but not the scored player
        return getScoreForMove(pickBestMove(nextPlayer, boardCopy), boardCopy, scoredPlayer);
    }
}

public void performComputerMove(){
    Move bestMove = pickBestMove(NOUGHT, mBoard);
    minimaxActivity.setMove(bestMove.x, bestMove.y, NOUGHT);
}

/**
 * Perform a deep copy of the array. Most library array copy methods 
 * (Arrays.copyOf, System.arrayCopy) create a shallow copy and are
 * therefore unsuitable for making an editable copy of a 2d array
 *
 * @param toCopy the array to copy
 * @return a deep copy of the array
 */
private int[][] copyArray(int[][] toCopy) {
    int[][] newArray = new int[toCopy.length][toCopy[0].length];
    for (int i = 0; i < toCopy.length; i++) {
        for(int j = 0; j < toCopy[i].length; j++) {
            newArray[i][j] = toCopy[i][j];
        }
    }

    return newArray;
}

Also, in the activity, when it's Android's turn to go first, instead of picking a random move, you should use pickBestMove():

mInfoTextView.setText("Android's Turn.");
myGame.performComputerMove();

Hopefully that's understandable. Let me know if you have any questions.

Previous Answer:

Your question doesn't specify what your actual problem is, but I think it must be a crash, after looking into the code.

The problem is not that bestMove is null, or that bestMove.x or .y are null. The problem is that in MyGame, when you call MinimaxActivity.setMove(), setMove then does this:

myGame.placeAMove(x, y, player);

Unfortunately, myGame itself will be null when this is called from MyGame. The reason is pretty simple. In MyGame you have:

MinimaxActivity minimaxActivity = new MinimaxActivity();

This means you are creating a brand-new MinimaxActivity every time you create a new MyGame. You're then calling setMove on the newly-created MinimaxActivity instead of the one you've already set up. When you create your MinimaxActivity, myGame starts out as null. Since you never set it, you will get an exception:

public void setMove(int x, int y, int player) {
    myGame.placeAMove(x, y, player);  //---myGame = null!!! Crash.
    ...
}

Conveniently, though, you already have a Minimax activity where you've set up a new MyGame.

So to fix this, you need to actually pass the existing instance of the activity to MyGame. The easiest place is probably in the constructor of MyGame:

MinimaxActivity minimaxActivity;

public MyGame(MinimaxActivity minimaxActivity) {
    this.minimaxActivity = minimaxActivity;
}

Then, of course, in MinimaxActivity, instead of new MyGame(), you'll have to do this:

myGame = new MyGame(MinimaxActivity.this);
ajpolt
  • 1,002
  • 6
  • 10
  • You are correct. I tested it and it worked. The problem was a crash and now it's fixed thanks to your suggestion. However, the algorithm isn't perfect, it continues to make dumb moves. Also, the reason why I didn't specify my problem was because I thought that the MiniMax algorithm was responsible for the crash. Anyway, If you can improve the MiniMax algorithm itself, that would be splendid. Because that's the most important part of the project as well as this question. – Filip Markoski Nov 21 '15 at 22:38
3

Alright I think I solved your problem of both x, y, and score is returning null. When you change the code noted below the variables won't all be null.

Also put the class Move in a new java file. Add "extends Move" after "public class myGame"

The reason why they were becoming null is because the variables x,y,and score were in the Move class and cannot be accessed from myGame class without public static in front of the variables x,y,and score.

public class MyGame { needs to be public class MyGame extends Move{

// Name-constants to represent the seeds and cell contents
public final int EMPTY = 0;
public final int CROSS = 1;
public final int NOUGHT = 2;

// Name-constants to represent the various states of the game
public final int PLAYING = 0;
public final int CROSS_WON = 1;
public final int NOUGHT_WON = 2;
public final int DRAW = 3;

// The game board and the game status
public static final int ROWS = 3, COLS = 3; // number of rows and columns
public static int[][] board = new int[ROWS][COLS]; // game board in 2D array
//  containing (EMPTY, CROSS, NOUGHT)
public static int currentState;  // the current state of the game
// (PLAYING, DRAW, CROSS_WON, NOUGHT_WON)
public static int currentPlayer; // the current player (CROSS or NOUGHT)
public static int currentRow, currentCol; // current seed's row and column

public int AndroidPlayer, HumanPlayer;
MinimaxActivity minimaxActivity = new MinimaxActivity();

public void resetBoard() {
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            board[i][j] = 0;
        }
    }
}

List<Move> availableMoves;
public Move AndroidMove(int player) {
    // Computer is always NOUGHT

    // Base case
    int State = CheckGameState();
    if (State == NOUGHT_WON){
        return new Move(10);
    } else if (State == CROSS_WON){
        return new Move(-10);
    } else if (State == DRAW){
        return new Move(0);
    }

    List<Move> moves = getAvailableStates();
    //if (moves.isEmpty()) return new Move(0);

    for (int i = 0; i < ROWS; ++i) {
        for (int j = 0; j < COLS; ++j) {
             if (board[i][j] == EMPTY){
                 Move move = new Move(i, j, player);
                 placeAMove(i, j, player);

                 if (player == NOUGHT){
                     move.score = AndroidMove(CROSS).score;
                 } else {
                     move.score = AndroidMove(NOUGHT).score;
                 }
                 moves.add(move);

                 placeAMove(i, j, EMPTY);
             }
        }
    }

    int bestMove = 0;
    if (player == NOUGHT) {
        int bestScore = -1000000;
        for (int i = 0; i < moves.size(); i++) {
            if (moves.get(i).score > bestScore) {
                bestMove = i;
                bestScore = moves.get(i).score;
            }
        }
    } else {
        int bestScore = 1000000;
        for (int i = 0; i < moves.size(); i++) {
            if (moves.get(i).score < bestScore) {
                bestMove = i;
                bestScore = moves.get(i).score;
            }
        }
    }
    return moves.get(bestMove);
}

public void AndroidPerform(){
    Move bestMove = AndroidMove(NOUGHT);
    placeAMove(bestMove.x, bestMove.y, NOUGHT);
    minimaxActivity.setMove(bestMove.x, bestMove.y, NOUGHT);
}

public void placeAMove(int x, int y, int player) {
    board[x][y] = player;   //player = 1 for X, 2 for O
}
public void placeAMove(Point point, int player) {
    board[point.x][point.y] = player;   //player = 1 for X, 2 for O
}

public List<Move> getAvailableStates() {
    availableMoves = new ArrayList<>();
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            if (board[i][j] == EMPTY) {
                availableMoves.add(new Move(i, j));
            }
        }
    }
    return availableMoves;
}

public int CheckGameState() {
    /*
    0 - Playing
    1 - X Won
    2 - O Won
    3 - Draw
     */

    // Check Rows - Horizontal Lines
    for (int i = 0; i< ROWS; i++){
        if (board[i][0] == CROSS &&
            board[i][1] == CROSS &&
            board[i][2] == CROSS){
            return CROSS_WON;
        }
        if (board[i][0] == NOUGHT &&
            board[i][1] == NOUGHT &&
            board[i][2] == NOUGHT){
            return NOUGHT_WON;
        }
    }

    // Check Columns - Vertical Lines
    for (int i = 0; i< COLS; i++){
        if (board[0][i] == CROSS &&
            board[1][i] == CROSS &&
            board[2][i] == CROSS){
            return CROSS_WON;
        }
        if (board[0][i] == NOUGHT &&
            board[1][i] == NOUGHT &&
            board[2][i] == NOUGHT){
            return NOUGHT_WON;
        }
    }

    // Check Diagonal
    if (board[0][0] == CROSS &&
        board[1][1] == CROSS &&
        board[2][2] == CROSS){
        return CROSS_WON;
    }
    if (board[0][0] == NOUGHT &&
        board[1][1] == NOUGHT &&
        board[2][2] == NOUGHT){
        return NOUGHT_WON;
    }


    // Check Reverse-Diagonal
    if (board[0][2] == CROSS &&
        board[1][1] == CROSS &&
        board[2][0] == CROSS){
        return CROSS_WON;
    }
    if (board[0][2] == NOUGHT &&
        board[1][1] == NOUGHT &&
        board[2][0] == NOUGHT){
        return NOUGHT_WON;
    }

    // Check for Tie
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            if (board[i][j] != CROSS && board[i][j] != NOUGHT){
                return PLAYING;
            }
        }
    }

    return DRAW;
}

}

The Move class needs to be in a separate Java file than myGame class.

public class Move {

int x, y, score, player; needs to be public static int x, y, score, player;

    public Move(int score){
        this.score = score;
    }

    public Move(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public Move(int x, int y, int player) {
        this.x = x;
        this.y = y;
        this.player = player;
    }


}
Felix
  • 31
  • 3
  • I think that they can be accessed, but rather, the problem was that (as @ajpolt mentioned before) of object misrepresenting each other. However it may be, thank you for your answer. Since posting this question I've tried to go on another route which has turned out fruitful, plainly meaning the everything is working correctly. – Filip Markoski Nov 22 '15 at 11:27
  • Move.x and Move.y are ints in this case, and since int is a primitive it cannot be null (this would not be the case if the code had used Integer instead of int, but it's using int). If the problem were what you are suggesting, the app just wouldn't compile. I also disagree strongly with "public class MyGame extends Move" since this makes no sense. I agree that Move should probably be in its own file, even though it's not terribly important in this case for anything besides organization and clean code. – ajpolt Nov 22 '15 at 17:03
2

Working Code, changes made in both of the two classes.

MinimaxActivity Class

public class MinimaxActivity extends AppCompatActivity {

private Board BoardGame;
private MyGame myGame;

private Button mBoardButtons[][];

private TextView mInfoTextView;
private TextView mPlayerOneCount;
private TextView mTieCount;
private TextView mPlayerTwoCount;
private TextView mPlayerOneText;
private TextView mPlayerTwoText;

private int mPlayerOneCounter = 0;
private int mTieCounter = 0;
private int mPlayerTwoCounter = 0;

private Button newGame, exitGame;

public int HUMAN = 1;
public int COMPUTER = 2;
Random random;

private int First=0;
private int Counter = 0;
private boolean isGameOver = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_play);

    mBoardButtons = new Button[3][3];
    mBoardButtons[0][0] = (Button) findViewById(R.id.one);
    mBoardButtons[0][1] = (Button) findViewById(R.id.two);
    mBoardButtons[0][2] = (Button) findViewById(R.id.three);
    mBoardButtons[1][0] = (Button) findViewById(R.id.four);
    mBoardButtons[1][1] = (Button) findViewById(R.id.five);
    mBoardButtons[1][2] = (Button) findViewById(R.id.six);
    mBoardButtons[2][0] = (Button) findViewById(R.id.seven);
    mBoardButtons[2][1] = (Button) findViewById(R.id.eight);
    mBoardButtons[2][2] = (Button) findViewById(R.id.nine);

    newGame = (Button) findViewById(R.id.newGame1);
    exitGame = (Button) findViewById(R.id.exitGame1);
    // Text Fields
    mInfoTextView = (TextView) findViewById(R.id.information);
    mPlayerOneCount = (TextView) findViewById(R.id.humanCount);
    mTieCount = (TextView) findViewById(R.id.tiesCount);
    mPlayerTwoCount = (TextView) findViewById(R.id.androidCount);
    mPlayerOneText = (TextView) findViewById(R.id.human);
    mPlayerTwoText = (TextView) findViewById(R.id.android);
    // Counters
    mPlayerOneCount.setText(Integer.toString(mPlayerOneCounter));
    mTieCount.setText(Integer.toString(mTieCounter));
    mPlayerTwoCount.setText(Integer.toString(mPlayerTwoCounter));

    random = new Random();

    exitGame.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            MinimaxActivity.this.finish();
        }
    });

    final CharSequence[] items = {"Computer", "Player"};

    final AlertDialog.Builder alertDialog = new AlertDialog.Builder(this);
    alertDialog.setCancelable(false);
    alertDialog.setTitle("Who goes first?");
    alertDialog.setSingleChoiceItems(items, -1, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int item) {
            if (items[item] == "Computer") {
                First = 1; // Computer
            } else if (items[item] == "Player") {
                First = 2; // Player
            }
            dialog.dismiss();

            myGame = new MyGame(MinimaxActivity.this);

            if (First == 1) {
                startNewGame(true); // True For Computer
            }
            if (First == 2) {
                startNewGame(false); // False For Player
            }

        }
    });
    alertDialog.show();

    newGame.setOnClickListener(new View.OnClickListener() { // FIX STARTNEWGAME
        @Override
        public void onClick(View v) {
            if (Counter % 2 == 0) {
                startNewGame(false);
                Counter++;
            } else {
                startNewGame(true);
                Counter++;
            }
        }
    });

}

private void startNewGame(boolean GoesFirst) {

    MyResetBoard(); // Look at board reset

    mPlayerOneText.setText("Human:");
    mPlayerTwoText.setText("Android:");

    if(GoesFirst){
        // Computer Goes First
        mInfoTextView.setText("Android's Turn.");
        setMove(random.nextInt(3), random.nextInt(3), COMPUTER);
        GoesFirst = false;
    }else{
        //Player Goes First
        mInfoTextView.setText("Human's Turn.");
        GoesFirst = true;
    }
    isGameOver = false;
}

private void MyResetBoard(){
    myGame.resetBoard();
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            mBoardButtons[i][j].setText("");
            mBoardButtons[i][j].setEnabled(true);
            mBoardButtons[i][j].setOnClickListener(new ButtonClickListener(i,j));
            mBoardButtons[i][j].setBackgroundResource(R.drawable.empty);
        }
    }
}

private class ButtonClickListener implements View.OnClickListener {

    int x,y;

    public ButtonClickListener(int i, int j) {
        this.x = i;
        this.y = j;
    }

    @Override
    public void onClick(View v) {
        if (!isGameOver){ // If the game is not over
            if (mBoardButtons[x][y].isEnabled()){
                setMove(x, y, HUMAN); // Human makes a move CROSS


                int winner = myGame.CheckGameState();

                if (winner == myGame.PLAYING) { // If still playing
                    mInfoTextView.setText(R.string.turn_computer);
                    int[] result = myGame.move();
                    setMove(result[0], result[1], COMPUTER);
                    winner = myGame.CheckGameState();
                }

                winner = myGame.CheckGameState();

                if (winner == myGame.PLAYING){
                    mInfoTextView.setText(R.string.turn_human);
                }
                else if (winner == myGame.DRAW) { // If draw
                    mInfoTextView.setText(R.string.result_tie);
                    mTieCounter++;
                    mTieCount.setText(Integer.toString(mTieCounter));
                    isGameOver = true;
                } else if (winner == myGame.CROSS_WON) { // X Won
                    mInfoTextView.setText(R.string.result_human_wins);
                    mPlayerOneCounter++;
                    mPlayerOneCount.setText(Integer.toString(mPlayerOneCounter));
                    isGameOver = true;
                } else if (winner == myGame.NOUGHT_WON){ // O Won
                    mInfoTextView.setText(R.string.result_android_wins);
                    mPlayerTwoCounter++;
                    mPlayerTwoCount.setText(Integer.toString(mPlayerTwoCounter));
                    isGameOver = true;
                }


            }
        }
    }
}

public void setMove(int x, int y, int player){
    myGame.placeAMove(x, y, player);
    mBoardButtons[x][y].setEnabled(false);
    if (player == 1) {
        mBoardButtons[x][y].setBackgroundResource(R.drawable.x);
    } else {
        mBoardButtons[x][y].setBackgroundResource(R.drawable.o);
    }
}
}

MyGame Class

public class MyGame {

// Name-constants to represent the seeds and cell contents
public final int EMPTY = 0;
public final int CROSS = 1;
public final int NOUGHT = 2;

// Name-constants to represent the various states of the game
public final int PLAYING = 0;
public final int CROSS_WON = 1;
public final int NOUGHT_WON = 2;
public final int DRAW = 3;

// The game board and the game status
public static final int ROWS = 3, COLS = 3; // number of rows and columns
public static int[][] board = new int[ROWS][COLS]; // game board in 2D array
//  containing (EMPTY, CROSS, NOUGHT)
public static int currentState;  // the current state of the game
// (PLAYING, DRAW, CROSS_WON, NOUGHT_WON)
public static int currentPlayer; // the current player (CROSS or NOUGHT)
public static int currentRow, currentCol; // current seed's row and column

MinimaxActivity minimaxActivity;

public MyGame(MinimaxActivity minimaxActivity) {
    this.minimaxActivity = minimaxActivity;
}

public void resetBoard() {
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            board[i][j] = 0;
        }
    }
}

/** Get next best move for computer. Return int[2] of {row, col} */
public int[] move() {
    int[] result = minimax(2, NOUGHT); // depth, max turn
    return new int[] {result[1], result[2]};   // row, col
}

public int[] minimax(int depth, int player){
    // Generate possible next moves in a List of int[2] of {row, col}.
    List<int[]> nextMoves = generateMoves();

    // mySeed(NOUGHT) is maximizing; while oppSeed(CROSS) is minimizing
    int bestScore = (player == NOUGHT) ? Integer.MIN_VALUE : Integer.MAX_VALUE;
    int currentScore;
    int bestRow = -1;
    int bestCol = -1;

    if (nextMoves.isEmpty() || depth == 0){
        bestScore = evaluate();
    } else {
        for (int[] move : nextMoves){
            board[move[0]][move[1]] = player;
            if (player == NOUGHT) { // NOUGHT is Maximizing Player
                currentScore = minimax(depth - 1, CROSS)[0];
                if (currentScore > bestScore) {
                    bestScore = currentScore;
                    bestRow = move[0];
                    bestCol = move[1];
                }
            } else { // CROSS is Minimizing Player
                    currentScore = minimax(depth - 1, NOUGHT)[0];
                    if (currentScore < bestScore) {
                        bestScore = currentScore;
                        bestRow = move[0];
                        bestCol = move[1];
                    }
            }
            // Undo move
            board[move[0]][move[1]] = EMPTY;
        }
    }
    return new int[] {bestScore, bestRow, bestCol};
}

private int evaluate() {
    int score = 0;
    // Evaluate score for each of the 8 lines (3 rows, 3 columns, 2 diagonals)
    score += evaluateLine(0, 0, 0, 1, 0, 2);  // row 0
    score += evaluateLine(1, 0, 1, 1, 1, 2);  // row 1
    score += evaluateLine(2, 0, 2, 1, 2, 2);  // row 2
    score += evaluateLine(0, 0, 1, 0, 2, 0);  // col 0
    score += evaluateLine(0, 1, 1, 1, 2, 1);  // col 1
    score += evaluateLine(0, 2, 1, 2, 2, 2);  // col 2
    score += evaluateLine(0, 0, 1, 1, 2, 2);  // diagonal
    score += evaluateLine(0, 2, 1, 1, 2, 0);  // alternate diagonal
    return score;
}

/** The heuristic evaluation function for the given line of 3 cells
     @Return +100, +10, +1 for 3-, 2-, 1-in-a-line for computer.
             -100, -10, -1 for 3-, 2-, 1-in-a-line for opponent.
             0 otherwise */
private int evaluateLine(int row1, int col1, int row2, int col2, int row3, int col3) {
    int score = 0;

    // First cell
    if (board[row1][col1] == NOUGHT) {
        score = 1;
    } else if (board[row1][col1] == CROSS) {
        score = -1;
    }

    // Second cell
    if (board[row2][col2] == NOUGHT) {
        if (score == 1) {   // cell1 is mySeed
            score = 10;
        } else if (score == -1) {  // cell1 is oppSeed
            return 0;
        } else {  // cell1 is empty
            score = 1;
        }
    } else if (board[row2][col2] == CROSS) {
        if (score == -1) { // cell1 is oppSeed
            score = -10;
        } else if (score == 1) { // cell1 is mySeed
            return 0;
        } else {  // cell1 is empty
            score = -1;
        }
    }

    // Third cell
    if (board[row3][col3] == NOUGHT) {
        if (score > 0) {  // cell1 and/or cell2 is mySeed
            score *= 10;
        } else if (score < 0) {  // cell1 and/or cell2 is oppSeed
            return 0;
        } else {  // cell1 and cell2 are empty
            score = 1;
        }
    } else if (board[row3][col3] == CROSS) {
        if (score < 0) {  // cell1 and/or cell2 is oppSeed
            score *= 10;
        } else if (score > 1) {  // cell1 and/or cell2 is mySeed
            return 0;
        } else {  // cell1 and cell2 are empty
            score = -1;
        }
    }
    return score;
}

private List<int[]> generateMoves() {
    List<int[]> nextMoves = new ArrayList<int[]>(); // allocate List

    int State = CheckGameState();
    // If gameover, i.e., no next move
    if (State == 1 || // X Won
        State == 2 || // O Won
        State == 3)   // Draw
    {
        return nextMoves;   // return empty list
    }

    // Search for empty cells and add to the List
    for (int row = 0; row < ROWS; ++row) {
        for (int col = 0; col < COLS; ++col) {
            if (board[row][col] == EMPTY) {
                nextMoves.add(new int[] {row, col});
            }
        }
    }
    return nextMoves;
}

public void placeAMove(int x, int y, int player) {
    board[x][y] = player;   //player = 1 for X, 2 for O
}

public int CheckGameState() {
    /*
    0 - Playing
    1 - X Won
    2 - O Won
    3 - Draw
     */

    // Check Rows - Horizontal Lines
    for (int i = 0; i< ROWS; i++){
        if (board[i][0] == CROSS &&
            board[i][1] == CROSS &&
            board[i][2] == CROSS){
            return CROSS_WON;
        }
        if (board[i][0] == NOUGHT &&
            board[i][1] == NOUGHT &&
            board[i][2] == NOUGHT){
            return NOUGHT_WON;
        }
    }

    // Check Columns - Vertical Lines
    for (int i = 0; i< COLS; i++){
        if (board[0][i] == CROSS &&
            board[1][i] == CROSS &&
            board[2][i] == CROSS){
            return CROSS_WON;
        }
        if (board[0][i] == NOUGHT &&
            board[1][i] == NOUGHT &&
            board[2][i] == NOUGHT){
            return NOUGHT_WON;
        }
    }

    // Check Diagonal
    if (board[0][0] == CROSS &&
        board[1][1] == CROSS &&
        board[2][2] == CROSS){
        return CROSS_WON;
    }
    if (board[0][0] == NOUGHT &&
        board[1][1] == NOUGHT &&
        board[2][2] == NOUGHT){
        return NOUGHT_WON;
    }


    // Check Reverse-Diagonal
    if (board[0][2] == CROSS &&
        board[1][1] == CROSS &&
        board[2][0] == CROSS){
        return CROSS_WON;
    }
    if (board[0][2] == NOUGHT &&
        board[1][1] == NOUGHT &&
        board[2][0] == NOUGHT){
        return NOUGHT_WON;
    }

    // Check for Tie
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            if (board[i][j] != CROSS && board[i][j] != NOUGHT){
                return PLAYING;
            }
        }
    }

    return DRAW;
}

}
Filip Markoski
  • 333
  • 3
  • 19
  • Congrats! This looks like it should work. I'm wondering why you chose to use 2 as the depth though. Seems unnecessary when in such a trivial game you could just generate all possible outcomes and score based on win/loss. Or maybe you found some theorem that proves that 2-moves-ahead is all you need here? Just curious. – ajpolt Nov 22 '15 at 17:27
  • How to do easy, medium, hard possibilities? – Nitin Karale Jan 31 '20 at 12:50