1

Currently, I'm working on an AI for a simple turn-based game. The way I have the game set up is as following (in pseudo-code):

players = [User, AI];
(for player : players){
    player.addEventlistener(MoveListener (moveData)->move(moveData));
}

players[game.getTurn()].startTurn();

the move function:

move(data){
    game.doStuff(data);
    if(game.isOver())
        return;

    game.nextTurn();
    players[game.getTurn()].startTurn();
}

This results in the following recursion:

  • start turn
  • player/AI makes a move
  • move function gets called
  • the next player starts their turn
  • ...

This repeats until the game is over - note that the game is of finite length and doesn't go past ~50 moves. Now, even though the recursion is finite, I get a stackoverflow error. My question is: is there any way to fix this? Is there something wrong with the recursion after all? Or should I implement a game loop instead? I understand how this would work if AIs were to play against each other, but how would this work if the program had to wait for user input?

EDIT
Here are the relevant classes to the recursion:

Connect4 class:

package connect4;

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class Connect4 extends Application {
    Group root = new Group();
    GameSquare[][] squares;
    GameButton[] buttons;
    int currentTurn;
    int columns = 7;
    int rows = 6;
    Text gameState;
    Player[] players;
    Game game;

    @Override
    public void start(Stage primaryStage) {        
        int size = 50;
        int padding = 10;

        gameState = new Text();
        gameState.setX(padding);
        gameState.setY((rows+1)*size+(rows+3)*padding);
        root.getChildren().add(gameState);

        buttons = new GameButton[columns];
        for(int i = 0; i < buttons.length; i++){
            buttons[i] = new GameButton(i);
            buttons[i].setMaxWidth(size);
            buttons[i].setMaxHeight(size);
            buttons[i].setLayoutX(i*size+(i+1)*padding);
            buttons[i].setLayoutY(padding);

            buttons[i].setMouseTransparent(true);                
            buttons[i].setVisible(false); 

            root.getChildren().add(buttons[i]);
        }

        players = new Player[2];

        players[0] = new UserControlled(buttons);
        players[1] = new AI();

        MoveListener listener = (int i) -> {move(i);};

        for(Player player : players)
            player.addListener(listener);

        game = new Game(columns, rows, players.length);

        squares = new GameSquare[columns][rows];

        for(int x = 0; x < columns; x++){
            for(int y = 0; y < rows; y++){
                squares[x][y] = new GameSquare(
                        x*size+(x+1)*padding, 
                        (y+1)*size+(y+2)*padding,
                        size,
                        size,
                        size,
                        size
                );
                root.getChildren().add(squares[x][y]);
            }
        }

        players[game.getTurn()].startTurn(game);
        updateTurn();
        updateSquares();

        draw(primaryStage);
    }

    public void move(int i){
        game.move(i);
        updateSquares();

        if(game.isGameOver()){
            if(game.isTie()){
                tie();
                return;
            } else {
                win();
                return;
            }
        }

        updateTurn();
        players[game.getTurn()].startTurn(game);
    }

    private void updateSquares(){
        int[][] board = game.getBoard();
        for(int x = 0; x < columns; x++){
            for(int y = 0; y < rows; y++){
                squares[x][y].setOwner(board[x][y]);
            }
        }
    }

    private void updateTurn(){
        gameState.setText("Player " + game.getTurn() + "'s turn");
    }

    public static void main(String[] args) {
        launch(args);
    }

    private void draw(Stage primaryStage){
        Scene scene = new Scene(root, 500, 500);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void win(){
        gameState.setText("Player " + game.getWinner() + " has won the game!");
    }

    private void tie(){
        gameState.setText("It's a tie!");
    }
}

Game class: package connect4;

public class Game {
    private int turn = 0;
    private int[][] board;
    private int columns;
    private int rows;
    private int players;
    private boolean gameOver = false;
    private boolean tie = false;
    private int winner = -1;

    public Game(int columns, int rows, int playerCount){
        this.columns = columns;
        this.rows = rows;
        board = new int[columns][rows];

        for(int x = 0; x < columns; x++){
            for(int y = 0; y < rows; y++){
                board[x][y] = -1;
            }
        }

        players = playerCount;
    }

    public int[][] getBoard(){
        return board;
    }

    public int getTurn(){
        return turn;
    }

    private void updateTurn(){
        turn++;
        if(turn >= players)
            turn = 0;
    }

    public boolean isGameOver(){
        return gameOver;
    }

    private void win(int player){
        gameOver = true;
        winner = player;
    }

    public int getWinner(){
        return winner;
    }

    private void tie(){
        gameOver = true;
        tie = true;
    }

    public boolean isTie(){
        return tie;
    }

    public void move(int i){
        if(gameOver)
            return;

        if(columnSpaceLeft(i) == 0){
            return;
        }

        board[i][columnSpaceLeft(i)-1] = turn;
        checkWin(turn);        
        checkFullBoard();        

        if(gameOver)
            return;

        updateTurn();
    }

    private void checkFullBoard(){
        for(int i = 0; i < columns; i++){
            if(columnSpaceLeft(i) != 0)
                return;
        }
        tie();
    }

    public int columnSpaceLeft(int column){
        for(int i = 0; i < board[column].length; i++){
            if(board[column][i] != -1)
                return i;
        }
        return board[column].length;
    }

    public int[] getAvailableColumns(){        
        int columnCount = 0;
        for(int i = 0; i < board.length; i++){
            if(columnSpaceLeft(i) != 0)
                columnCount++;
        }

        int[] columns = new int[columnCount];
        int i = 0;
        for(int j = 0; j < board.length; j++){
            if(columnSpaceLeft(i) != 0){
                columns[i] = j;
                i++;
            }
        }

        return columns;
    }

    private Boolean checkWin(int player){
        //vertical
        for(int x = 0; x < columns; x++){
            int count = 0;
            for(int y = 0; y < rows; y++){
                if(board[x][y] == player)
                    count++;
                else
                    count = 0;
                if(count >= 4){
                    win(player);
                    return true;
                }
            }
        }

        //horizontal
        for(int y = 0; y < rows; y++){
            int count = 0;
            for(int x = 0; x < columns; x++){
                if(board[x][y] == player)
                    count++;
                else
                    count = 0;
                if(count >= 4){
                    win(player);
                    return true;
                }
            }
        }

        //diagonal
        for(int x = 0; x < columns; x++){
            for(int y = 0; y < rows; y++){
                int count = 0;
                //diagonaal /
                if(!(x > columns-4 || y < 3) && board[x][y] == player){
                    count ++;
                    for(int i = 1; i <= 3; i++){
                        if(board[x+i][y-i] == player){
                            count++;
                            if(count >= 4){
                                win(player);
                                return true;
                            }
                        } else {
                            count = 0;
                            break;
                        }
                    }
                }

                //diagonal \                
                if(!(x > columns-4 || y > rows-4) && board[x][y] == player){
                    count ++;
                    for(int i = 1; i <= 3; i++){
                        if(board[x+i][y+i] == player){
                            count++;                            
                            if(count >= 4){
                                win(player);
                                return true;
                            }
                        } else {
                            count = 0;
                            break;
                        }
                    }
                }
            }
        }

        return false;
    }
}

UserControlled class:

package connect4;

import java.util.ArrayList;
import java.util.List;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;

public class UserControlled implements Player  {
    private List<MoveListener> listeners = new ArrayList<MoveListener>();
    private GameButton[] buttons;
    private boolean active = false;

    public UserControlled(GameButton[] buttons){
        this.buttons = buttons;
    }

    @Override
    public void addListener(MoveListener listener){
        listeners.add(listener);
    }

    @Override
    public void startTurn(Game game){
        System.out.println(0);
        active = true;
        for(int i = 0; i < buttons.length; i++){
            if(game.columnSpaceLeft(i) != 0){
                setButton(i, true);
                buttons[i].setOnAction(new EventHandler<ActionEvent>() {
                    @Override public void handle(ActionEvent e) {
                        move(( (GameButton) e.getTarget()).getColumn());
                    }
                });
            }
        }
    }

    private void move(int i){
        if(!active)
            return;
        active = false;
        disableButtons();
        for(MoveListener listener : listeners)
            listener.onMove(i);
    }

    private void disableButtons(){
        for(int i = 0; i < buttons.length; i++){
            setButton(i, false);  
        }
    }

    private void setButton(int i, boolean enable){
        if(enable){
            buttons[i].setMouseTransparent(false);                
            buttons[i].setVisible(true);
        } else {
            buttons[i].setMouseTransparent(true);                
            buttons[i].setVisible(false);              
        }
    }
}

The AI class is basically the same as a stripped down UserControlled class, except the startTurn method:

int[] columns = game.getAvailableColumns();
move(columns[rng.nextInt(columns.length)]);

The MoveListener interface is very simple:

public interface MoveListener {
    void onMove(int i);
}

The stack trace:

Exception in thread "JavaFX Application Thread" java.lang.StackOverflowError
    at javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:142)
    at javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:49)
    at javafx.scene.text.Text.setText(Text.java:370)
    //note that the three lines above this are different every time
    //as the application crashes at a different point
    at connect4.Connect4.updateTurn(Connect4.java:107)
    at connect4.Connect4.move(Connect4.java:93)
    at connect4.Connect4.lambda$start$0(Connect4.java:49)
    at connect4.AI.move(AI.java:13)
    at connect4.AI.startTurn(AI.java:24)
    at connect4.Connect4.move(Connect4.java:94)
    at connect4.Connect4.lambda$start$0(Connect4.java:49)
    at connect4.AI.move(AI.java:13)
    ...etc
Jonan
  • 2,485
  • 3
  • 24
  • 42
  • 1
    `while(!gameOver){ waitForUserInput(); actAccordingToInput(); makeAIMove(); }` – luk2302 Aug 27 '17 at 15:11
  • It always depends on what is on your stack. If you have many or huge local objects which are stack allocated, even a few dozen recursion levels can already be too much. As @luk2302 suggested, make it iterative. You should always choose the iterative solution, if the recursion depth isn't capped at a sufficiently low limit. – Ext3h Aug 27 '17 at 15:26
  • @Ext3h the thing is, I haven't even started with the `AI` itself (as of now it just makes random moves), the whole program isn't even 400 lines of code of which half is not related. Rather than to increase the stack size, I'd like to solve the issue in a clean way so it won't come back and bite me – Jonan Aug 27 '17 at 15:28
  • Then you will need to provide more than just the pseudo code. The fault might be in whatever library you used for the event handling, as that one might be allocating excessive amounts of data on the stack. Worst case, you could even have ended up with a full copy of "data" for each recursion! A suitable library would have handled the event in a deferred context (turning your recursion into an iteration)- – Ext3h Aug 27 '17 at 15:31
  • You must share the stack trace and relevant code for a better look into what could be the cause for it. – Naman Aug 27 '17 at 15:34
  • @luk2302 how would you wait for the user input in this context? – Jonan Aug 27 '17 at 15:48
  • You could look at this example implementation of [tic-tac-toe in JavaFX](https://gist.github.com/jewelsea/5115901) or [this other tic-tac-toe implementation](https://github.com/james-d/TicTacToe) to get some ideas on how to implement this. The key point is that the naive solution with a loop `while(!gameOver){ waitForUserInput(); actAccordingToInput(); makeAIMove(); }` would not be directly implementable in an event based system (such as JavaFX or many other GUI frameworks), as you need to relinquish control of the UI thread to wait for input rather than actively waiting. – jewelsea Aug 28 '17 at 19:03
  • @jewelsea thank you, using `properties` and `changeListeners` I got my code to work without having to work with multiple threads (which caused UI issues) or recursion – Jonan Aug 28 '17 at 20:45

1 Answers1

2

In general, you shouldn't use a recursion except you are pretty sure about what you are doing.

Think this, every time you call the next step, you are saving all the context, with all the local variables in the stack. In a game, it could be a lot of stuff.

A common game loop in a turn based game would be something like:

while(!gameFinished()){
    for(player in players){
         player.doTurn();
    }
}

Take into account too, that recursion is slow, because it has to save all the context and it takes time, so, in general, think three times before trying to use a recursion.

EDIT

To process the input you could use something like this:

CompletableFuture.supplyAsync(this::waitUserInput)  
             .thenAccept(this::processUserInput)

Here you can find how it works:

http://www.deadcoderising.com/java8-writing-asynchronous-code-with-completablefuture/

With this, your code keeps running, so keep in mind that in the next line of code you won't have the input. When it gets the input, it will call the proccessUserInput method.

Another way to do this, is to check every frame if any key was pressed, and that's okay too.

Here you can find a way to do that:

How do I check if the user is pressing a key?

The way you should do things depends on the size of your project. If you will be checking for key presses all the time, maybe it's a good idea to build some sort of event system for that.

In the other hand, I recommend you to use a game engine like Unreal or Unity. If you want to keep with Java, there are a lot of libraries for games that handle a lot of common problems like this.

For example:

https://www.lwjgl.org/

You can find a lot of tutorials of turn-based games made with that library.

Good luck!

leoxs
  • 425
  • 1
  • 5
  • 15
  • Using a loop like this, how would I wait for user input (a button press) if the player isn't an `AI`? – Jonan Aug 27 '17 at 16:03
  • First, you need at least two threads, one will process all the graphics, so it is always running, and the other thread can have your actual game code. So, you can just freeze that thread while it waits for the user input, possibly setting a timeout. In java, you have the Scanner that waits for a user input in the console, something like that works. Imagine it like a callback. – leoxs Aug 27 '17 at 16:08
  • You don't actually need to have more than one thread if you create your own input handling system. It simply polls the state of all keys each frame and stores it (also maybe storing previous states if you need to implement things like "OnButtonReleased" or "ButtonHeld") which you can then access inside your gamelogic. – UnholySheep Aug 27 '17 at 16:16
  • Yep, that's true, but using threads could be better now that everybody has multicore processors. It's common to see games running slow using 100% of only one core. – leoxs Aug 27 '17 at 16:22
  • @leoxs I don't have any experience with threads in java. Could you write an example or point me in the right direction? – Jonan Aug 27 '17 at 16:23
  • @Jonan I edited with more info, I'm not sure if it's enough – leoxs Aug 27 '17 at 17:37
  • @leoxs I have the waiting for the user input part done now. I'm still left with one problem though: how do I update the UI based on the constantly changing variables of the `Game` class? If I do that directly from the second thread, I get the following error: `Exception in thread "JavaFX Application Thread" java.lang.ArrayIndexOutOfBoundsException` – Jonan Aug 27 '17 at 19:32
  • It doesn't seem to be a problem of the thread, it should work. Are you sure that the array has the index you are trying to get? Are both threads pointing to the same array? the memory is shared between threads, so it should work, you only have to take care to not modify it at the same time, and Java has a lot of tools for that, like Atomic Arrays. It seems like you are trying to get an index bigger than the size-1 of the list. Remember that indexes in Java starts at 0. Maybe you can see better what's happening with the debugger – leoxs Aug 27 '17 at 20:20