JavaFX 我怎么能在这种情况下检查是否获胜?

JavaFX how could I check for winning in this occasion?

我会尽力解释我的问题。我总是很高兴找到有耐心的人可以帮助我改进并摆脱困境。 基本上我正在尝试构建一个 javafx tic-tac-toe 游戏,因为我觉得即使是像这样简单的事情我也可以学习。

所以我有一个启动程序的主 App.java 文件,并且有一些方法可以正确设置我要使用的 fxml 文件并对其进行更改。这样一来,如果将来我想为玩家添加配置文件或在本地与 AI 或两个玩家进行单人游戏的可能性,我可以轻松创建另一个 fxml 文件来处理其他场景。

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

/**
 * JavaFX App
 */
public class App extends Application {

    private static Scene scene;

    @Override
    public void start(Stage stage) throws IOException {
        scene = new Scene(loadFXML("primary"));
        stage.setScene(scene);
        stage.show();
    }

    static void setRoot(String fxml) throws IOException {
        scene.setRoot(loadFXML(fxml));
    }

    private static Parent loadFXML(String fxml) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(App.class.getResource(fxml + ".fxml"));
        return fxmlLoader.load();
    }

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

}

然后我有一个 PrimaryController.java 文件,基本上想用它来用节点填充场景。该结构是一个 AnchorPane,里面有一个 GridPane,我通过代码填充 GridPane,方法是向其中添加 MyPanes 元素,这些元素是对 StackPanes 进行了一些修改。 这里是控制器:

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.layout.GridPane;

public class PrimaryController implements Initializable {

    @FXML
    public GridPane grid;


    @Override
    public void initialize(URL location, ResourceBundle resources) {

        for (int i = 0; i<3; i++){
            for (int j = 0; j <3;j++){
                MyTiles tile = new MyTiles();
                grid.add(tile,j,i);
            }
        }

    }

}

这里是 MyTile class

import javafx.geometry.Pos;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;



public class MyTiles extends StackPane {

    private static boolean turnOne = true;
    private Text text = new Text(" ");


    public MyTiles(){
        setPrefSize(200,200);
        Rectangle rectangle = new Rectangle(200,200);
        rectangle.setFill(null);
        rectangle.setStroke(Color.BLACK);
        text.setFont(Font.font(72));
        setAlignment(Pos.CENTER);

        getChildren().addAll(rectangle,text);

        setOnMouseClicked(event -> {
            if (text.getText().equals(" ")){
                if (turnOne){
                    drawX(text);
                    turnOne = false;
                }else if (!turnOne){
                    drawY(text);
                    turnOne = true;
                }
            }else{
                return;
            }

        });

    }


    public String getTheText(){
        return text.getText();
    }

    private void drawX(Text text){
        text.setStroke(Color.BLUE);
        text.setFill(Color.LIGHTBLUE);
        text.setText("x");
    }
    private void drawY(Text text){
        text.setStroke(Color.RED);
        text.setFill(Color.PINK);
        text.setText("o");
    }
}

现在开始提问:你知道我可以用什么方法检查是否中奖吗?我的意思是我知道玩家获胜背后的逻辑,但在我的脑海中我必须: - 访问 GridPane 内的所有 MyTiles 对象 - 检查他们的文字是什么 - 检查是否有获胜组合。

我不知道该怎么做,有人可以给我一些提示、建议和示例吗?

也许在单独的文件中做事的策略不是最好的,但我想尝试是否有办法不把所有东西都放在主 App.java 文件中。

非常感谢任何帮助。我希望这个问题足够清楚! 再次感谢!

您应该将游戏状态分解为独立于用户界面本身的独立 classes。 (在 UI 开发术语中,这称为 "Model" 并且是模型-视图-控制器架构的一部分)。 JavaFX 提供了一些属性和绑定 API,使您的视图能够相对容易地观察模型并与其保持同步。

Tic-tac-toe 游戏的模型应该有每个方块的属性数组或列表,显示哪个玩家拥有 "occupied" 该方块,以及其他基本游戏状态(下一个玩家玩的是哪个玩家) ,有没有人赢了,游戏结束了,等等)。当前玩家应该有 API 才能在给定的空方格中移动。由于游戏状态是已进行移动的函数,因此最好将属性公开为只读,并在进行移动时在内部更新它们。

然后您可以实施算法以直接在模型中检查获胜者。使用 JavaFX 绑定,您可以确保只要其中一个方块的状态发生变化,它就会更新。

这是一个相当基本的实现:

首先是玩家占据一个方格的简单枚举:

public enum Player { O, X, NONE }

然后是主"model"class:

import java.util.ArrayList;
import java.util.List;

import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;


public class TicTacToe {

    private final List<ReadOnlyObjectWrapper<Player>> squares ;
    private final ReadOnlyObjectWrapper<Player> winner ;
    private final ReadOnlyObjectWrapper<Player> currentPlayer ;
    private final ReadOnlyBooleanWrapper gameOver ;

    public final static int SIZE = 3 ;

    public TicTacToe() {
        squares = new ArrayList<>();
        for (int i = 0 ; i < SIZE*SIZE ; i++) {
            squares.add(new ReadOnlyObjectWrapper<>(Player.NONE));
        }

        Observable[] squareArray = squares.toArray(new Observable[SIZE*SIZE]);

        winner = new ReadOnlyObjectWrapper<>(Player.NONE);
        winner.bind(Bindings.createObjectBinding(this::checkForWinner, squareArray));

        currentPlayer = new ReadOnlyObjectWrapper<>(Player.O);

        gameOver = new ReadOnlyBooleanWrapper() ;
        gameOver.bind(new BooleanBinding() {

            {
                bind(winner);
                bind(squareArray);
            }

            @Override
            protected boolean computeValue() {
                return checkGameOver();
            }

        }); 
    }

    public ReadOnlyObjectProperty<Player> squareProperty(int square) {
        return squares.get(square).getReadOnlyProperty();
    }

    public final Player getSquare(int square) {
        return squareProperty(square).get();
    }

    public ReadOnlyObjectProperty<Player> winnerProperty() {
        return winner.getReadOnlyProperty();
    }

    public final Player getWinner() {
        return winnerProperty().get();
    }

    public ReadOnlyObjectProperty<Player> currentPlayerProperty() {
        return currentPlayer.getReadOnlyProperty();
    }

    public final Player getCurrentPlayer() {
        return currentPlayerProperty().get();
    }

    public ReadOnlyBooleanProperty gameOverProperty() {
        return gameOver.getReadOnlyProperty();
    }

    public final boolean isGameOver() {
        return gameOverProperty().get();
    }

    public boolean isEmpty(int square) {
        return getSquare(square) == Player.NONE ;
    }

    public void move(int square) {
        squares.get(square).set(currentPlayer.get());
        currentPlayer.set(opponent(currentPlayer.get()));
    }

    public void reset() {
        squares.forEach(s -> s.set(Player.NONE));
        currentPlayer.set(Player.O);
    }

    private boolean checkGameOver() {
        if (getWinner() != Player.NONE) return true ;
        return squares.stream()
                .map(ReadOnlyObjectProperty::get)
                .filter(Player.NONE::equals)
                .findAny()
                .isEmpty();
    }

    private Player checkForWinner() {
        // To do: if either player has won, return that player.
        // If no player has won (yet) return Player.NONE
        return Player.NONE ;
    }


    private Player opponent(Player player) {
        // Note Java 14 is required for switch expressions
        return switch(player) {
            case NONE -> Player.NONE ;
            case O -> Player.X ;
            case X -> Player.O ;
        };
    }

}

现在很容易将 UI 绑定到该模型。例如:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.geometry.Insets?>

<BorderPane xmlns="http://javafx.com/javafx/8.0.171"
    xmlns:fx="http://javafx.com/fxml/1"
    fx:controller="org.jamesd.examples.tictactoe.TicTacToeController">

    <top>
        <VBox styleClass="controls">
            <Label fx:id="winner" />
            <Button fx:id="reset" onAction="#reset" text="Reset" />
        </VBox>
    </top>

    <center>
        <GridPane fx:id="board" styleClass="board" alignment="CENTER">
        </GridPane>
    </center>
</BorderPane>

和控制器:

import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;

public class TicTacToeController {

    private final TicTacToe game = new TicTacToe() ;

    @FXML
    private GridPane board ;

    @FXML
    private Label winner ;
    @FXML
    private Button reset ;


    public void initialize() {
        for (int row = 0 ; row < TicTacToe.SIZE ; row++) {
            for (int column = 0 ; column < TicTacToe.SIZE ; column++) {
                board.add(createTile(row, column), column, row);
            }
        }
        winner.textProperty().bind(game.winnerProperty().asString("Winner: %s"));
        reset.disableProperty().bind(game.gameOverProperty().not());
    }

    @FXML
    private void reset() {
        game.reset();
    }

    public Pane createTile(int row, int column) {
        StackPane pane = new StackPane();
        pane.getStyleClass().add("square");
        Label o = new Label("O");
        Label x = new Label("X");
        pane.getChildren().addAll(o, x);
        int square = row * TicTacToe.SIZE + column;
        ReadOnlyObjectProperty<Player> player = game.squareProperty(square) ;
        o.visibleProperty().bind(player.isEqualTo(Player.O));
        x.visibleProperty().bind(player.isEqualTo(Player.X));

        pane.setOnMouseClicked(e -> {
            if (game.isEmpty(square) && ! game.isGameOver()) {
                game.move(square);              
            }
        });

        return pane ;
    }

}

为了完整起见,样式 classes 挂接到外部 CSS 文件:

.board {
  -fx-padding: 2 ;
}

.square {
  -fx-background-color: black, -fx-background ; 
  -fx-background-insets: 0, 1;
  -fx-min-width: 200 ;
  -fx-min-height: 200 ;
}
.square .label {
  -fx-font-size:108;
  -fx-font-family: comic-sans ;
  -fx-text-fill:#00b041 ;
}
.controls {
  -fx-spacing: 5 ;
  -fx-padding: 5 ;
  -fx-alignment: center ;
}

并且应用程序启动 class 看起来像

import java.io.IOException;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class App extends Application {


    @Override
    public void start(Stage stage) throws IOException {

        FXMLLoader loader = new FXMLLoader(getClass().getResource("TicTacToeGrid.fxml"));
        Scene scene = new Scene(loader.load());
        scene.getStylesheets().add(getClass().getResource("tictactoe.css").toExternalForm());
        stage.setScene(scene);
        stage.show();
    }


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

}