2017-08-27 275 views
1

目前,我正在進行一個簡單回合制遊戲的AI。我有遊戲設置的方法是如下(在僞代碼):回合制遊戲的遊戲循環

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

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

move功能:

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

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

這將導致以下遞歸:

  • 啓動轉
  • 玩家/人工智能移動
  • 移動功能被稱爲
  • 下一個玩家開始輪到自己
  • ...

重複此過程,直到遊戲結束 - 注意,遊戲的長度是有限的,不晃過〜50個移動。現在,即使遞歸是有限的,我也會遇到一個stackoverflow錯誤。我的問題是:有沒有辦法解決這個問題?畢竟遞歸有什麼問題嗎?或者我應該實現一個遊戲循環嗎?我知道如果AI是彼此對抗,這將如何工作,但如果程序必須等待用戶輸入,這將如何工作?

EDIT
下面是相關類遞歸:

Connect4類:

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類: 包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類:

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

AI類基本相同的剝離下來UserControlled類,除了startTurn方法:

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

MoveListener界面非常簡單:

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

堆棧跟蹤:

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

它總是取決於你的堆棧上有什麼。如果你有很多或巨大的本地對象被堆棧分配,甚至幾十個遞歸級別可能已經太多了。正如@ luk2302建議的那樣,使其迭代。如果遞歸深度沒有被限制在足夠低的範圍內,則應始終選擇迭代解決方案。 – Ext3h

+0

@Ext3h事情是,我甚至還沒有開始使用「AI」本身(到目前爲止它只是隨機移動),整個程序甚至沒有400行代碼,其中一半不相關。我不想增加堆棧大小,我想以一種乾淨的方式解決問題,所以它不會回來並咬我 – Jonan

回答

1

一般情況下,你不應該除非你很清楚你在做什麼使用遞歸。

想一想,每次調用下一步時,都會保存所有上下文,並將所有局部變量保存在堆棧中。在一場比賽中,這可能是很多東西。

在回合遊戲中常見的遊戲循環會是這樣的:

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

考慮到一點,就是遞歸是緩慢的,因爲它必須保存所有的情況下,它需要時間,因此,一般來說,在嘗試使用遞歸之前先考慮三次。

編輯

要處理,你可以使用類似這樣的輸入:

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

在這裏,你可以找到它是如何工作的:

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

有了這個,你的代碼保持因此請記住,在下一行代碼中,您不會有輸入。當它獲得輸入時,它將調用proccessUserInput方法。

另一種方法是檢查每一幀是否有任何按鍵被按下,這也沒關係。

在這裏你可以找到一個方法來做到這一點:

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

,你應該做的事情的方式取決於項目的大小。如果你一直在檢查按鍵,可能最好爲此建立某種事件系統。

另一方面,我建議你使用像Unreal或Unity這樣的遊戲引擎。如果你想和Java保持一致,那麼有很多遊戲庫可以處理很多這樣的常見問題。

例如:

https://www.lwjgl.org/

你可以找到很多與該庫由基於回合制遊戲的教程。

祝你好運!

+0

使用這樣的循環,我將如何等待用戶輸入按下按鈕)如果玩家不是'AI'? – Jonan

+0

首先,你至少需要兩個線程,一個會處理所有的圖形,所以它總是在運行,另一個線程可以有你的實際遊戲代碼。 因此,您可以在等待用戶輸入時凍結該線程,可能會設置超時。 在java中,你有掃描器等待控制檯中的用戶輸入,就像那樣。 想象一下它就像一個回調。 – leoxs

+0

如果您創建自己的輸入處理系統,則實際上不需要擁有多個線程。它只是輪詢每個幀的所有鍵的狀態並存儲它(也可以存儲以前的狀態,如果你需要實現諸如「OnButtonReleased」或「ButtonHeld」),然後你可以在你的gamelogic中訪問它們。 – UnholySheep