2017-08-27 6 views
1

Actuellement, je travaille sur une IA pour un jeu simple au tour par tour. La façon dont je suis le jeu mis en place est la suivante (en pseudo-code):Boucle de jeu d'un jeu au tour par tour

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

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

la fonction move:

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

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

Il en résulte la récursion suivante:

  • début tourner
  • joueur/AI fait un mouvement
  • la fonction de déplacement est appelée
  • le joueur suivant commence leur tour
  • ...

Ce répète jusqu'à ce que le jeu est terminé - noter que le jeu est de longueur finie et ne va pas au-delà ~ 50 coups. Maintenant, même si la récursivité est finie, j'obtiens une erreur stackoverflow. Ma question est la suivante: y a-t-il un moyen de régler cela? Y a-t-il quelque chose qui ne va pas avec la récursion? Ou devrais-je implémenter une boucle de jeu à la place? Je comprends comment cela fonctionnerait si AI s devaient jouer les uns contre les autres, mais comment cela fonctionnerait-il si le programme devait attendre l'entrée de l'utilisateur?

EDIT
Voici les classes appropriées à la récursion:

Connect4 Classe:

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 classe: 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 Classe:

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);    
     } 
    } 
} 

La AI classe est fondamentalement la même que celle d'un dépouillé classe UserControlled, à l'exception de la méthode startTurn:

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

L'interface MoveListener est très simple:

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

La trace de la pile:

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 
+1

'while (! GameOver) {waitForUserInput(); actAccordingToInput(); makeAIMove(); } ' – luk2302

+0

Cela dépend toujours de ce qui se trouve sur votre pile. Si vous avez plusieurs ou plusieurs objets locaux qui sont alloués en pile, même quelques dizaines de niveaux de récursion peuvent déjà être trop importants. Comme @ luk2302 suggéré, le rendre itératif. Vous devriez toujours choisir la solution itérative, si la profondeur de récursivité n'est pas plafonnée à une limite suffisamment basse. – Ext3h

+0

@ Ext3h la chose est, je n'ai même pas commencé avec le 'AI' lui-même (à partir de maintenant il ne fait que des mouvements aléatoires), le programme entier n'est même pas 400 lignes de code dont la moitié n'est pas liée. Plutôt que d'augmenter la taille de la pile, je voudrais résoudre le problème d'une manière propre afin qu'il ne revienne pas et me mordre – Jonan

Répondre

1

En général, vous ne devez pas utiliser une récursivité sauf que vous êtes assez sûr de ce que vous faites. Pensez-y, à chaque fois que vous appelez l'étape suivante, vous enregistrez tout le contexte, avec toutes les variables locales dans la pile. Dans un jeu, ça pourrait être beaucoup de choses.

Une boucle de jeu commun dans un jeu tour par tour serait quelque chose comme:

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

prendre en compte aussi que récursion est lent, car il doit sauver tout le contexte et cela prend du temps, donc, En général, réfléchissez trois fois avant d'essayer d'utiliser une récursivité.

EDIT

Pour traiter l'entrée, vous pouvez utiliser quelque chose comme ceci:

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

Ici vous pouvez trouver comment cela fonctionne:

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

Avec cela, votre code conserve En cours d'exécution, gardez à l'esprit que dans la ligne de code suivante, vous n'aurez pas l'entrée. Quand il reçoit l'entrée, il appelle la méthode proccessUserInput.

Une autre façon de procéder consiste à vérifier chaque image si une touche a été enfoncée, et c'est également correct.

Ici vous pouvez trouver un moyen de le faire:

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

La façon dont vous devriez faire des choses dépend de la taille de votre projet. Si vous devez vérifier les touches tout le temps, c'est peut-être une bonne idée de créer un système d'événements pour cela. En revanche, je vous recommande d'utiliser un moteur de jeu comme Unreal ou Unity. En revanche, je vous recommande d'utiliser un moteur de jeu comme Unreal ou Unity. Si vous voulez garder Java, il y a beaucoup de bibliothèques pour les jeux qui traitent beaucoup de problèmes communs comme celui-ci.

Par exemple:

https://www.lwjgl.org/

Vous pouvez trouver beaucoup de tutoriels de jeux tour par tour fait avec cette bibliothèque.

Bonne chance!

+0

En utilisant une boucle comme ça, comment j'attendrais l'entrée de l'utilisateur (un appui sur un bouton) si le joueur n'est pas un «AI»? – Jonan

+0

D'abord, vous avez besoin d'au moins deux threads, l'un traitera tous les graphiques, donc il est toujours en cours d'exécution, et l'autre thread peut avoir votre code de jeu réel. Ainsi, vous pouvez simplement geler ce thread pendant qu'il attend l'entrée de l'utilisateur, en définissant éventuellement un délai d'expiration. En Java, vous avez le scanner qui attend une entrée de l'utilisateur dans la console, quelque chose comme ça fonctionne. Imaginez-vous comme un rappel. – leoxs

+0

Vous n'avez pas besoin d'avoir plus d'un thread si vous créez votre propre système de gestion des entrées. Il interroge simplement l'état de toutes les clés de chaque image et les stocke (peut-être aussi stocker les états précédents si vous devez implémenter des choses comme "OnButtonReleased" ou "ButtonHeld") auxquelles vous pouvez accéder à l'intérieur de votre gamelogic. – UnholySheep