Reputation: 45
I am coding a game (Pig) where you roll dice, totaling each roll. You can keep rolling until you roll a one and lose all your points, or decide to stop and save your points. Once it's the user's turn, I need to wait for them to choose whether to roll again or score their current points. I have two JavaFX Buttons, Roll and Keep, that should only be active while it's the users turn. How to I wait for one of the buttons to be pressed and proceed based on which one is pressed? I know I could use a while loop and check if either button is pressed each loop, but I think there is some way to do this using events even if I can't see it.
Here is what my UI looks like:
This is the method that is called when ever the program checks whether to roll a gain or not.
boolean willHold(Player p, int turnTotal){
switch(p){
case HumanPlayer hp ->{
turnTotalText.setText("Turn Total: " + turnTotal + " | what will you do?");
// TODO wait for roll or keep to be pressed
// return true keep is pressed first and return false if roll is pressed first
return false;
}
case DumbAI da -> {}
case ThresholdAI ta -> {}
default -> throw new IllegalStateException("Unexpected value: " + p);
}
}
Upvotes: 0
Views: 123
Reputation: 209674
This is something of an annoying "don't do it like that" answer. However, I think it's fairly well accepted that the approach you appear to be using is not really a viable way to go for this, so this answer is intended to start you on a track which gives a more useful overall design.
Approaching this in a purely procedural manner is not an approach that lends itself nicely to a UI application. A better overall approach is to think in a model-based event-driven paradigm.
The basic idea is to have a class (a model) that represents the current state of the game (list of players and scores, current player, turn total, etc.). Define some methods that represent operations on the game (roll, hold, etc.) and in those methods update the state. Since you intend to use this in a JavaFX application, represent the state using JavaFX observable properties. That way your UI can easily observe the properties and respond to changes in the value.
Here's a quick example of a model class. This might need some changes; I haven't actually tried to build a UI around this, and experience suggests that you (at least, I) rarely get the model exactly right first time.
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
public class Pig {
public static final int TARGET_SCORE = 100;
private final Die die = new Die();
private final List<Player> players = new ArrayList<>();
private int currentPlayerIndex = 0;
private final ReadOnlyObjectWrapper<Player> currentPlayer = new ReadOnlyObjectWrapper<>();
public final ReadOnlyObjectProperty<Player> currentPlayerProperty() {
return currentPlayer.getReadOnlyProperty();
}
public final Player getCurrentPlayer() {
return currentPlayerProperty().get();
}
private final ReadOnlyIntegerWrapper turnTotal = new ReadOnlyIntegerWrapper(0);
public final ReadOnlyIntegerProperty turnTotalProperty() {
return turnTotal.getReadOnlyProperty();
}
public final int getTurnTotal() {
return turnTotalProperty().get();
}
private final ReadOnlyObjectWrapper<Player> winner = new ReadOnlyObjectWrapper<>(null);
public final ReadOnlyObjectProperty<Player> winnerProperty() {
return winner.getReadOnlyProperty();
}
public final Player getWinner() {
return winnerProperty().get();
}
private final ReadOnlyIntegerWrapper lastRoll = new ReadOnlyIntegerWrapper();
public final ReadOnlyIntegerProperty lastRollProperty() {
return lastRoll.getReadOnlyProperty();
}
public final int getLastRoll() {
return lastRollProperty().get();
}
public Pig(Player... players) {
if (players.length == 0) {
throw new IllegalArgumentException("You must specify at least one player");
}
Collections.addAll(this.players, players);
currentPlayer.set(this.players.get(currentPlayerIndex));
}
public Pig(String... playerNames) {
this(Stream.of(playerNames).map(Player::new).toArray(Player[]::new));
}
public Die getDie() {
return die;
}
public void roll() {
int rollValue = die.nextRoll();
lastRoll.set(rollValue);
if (rollValue == 1) {
nextPlayer();
} else {
turnTotal.set(turnTotal.get() + rollValue);
}
}
public void hold() {
Player current = currentPlayer.get();
current.setScore(current.getScore() + turnTotal.get());
turnTotal.set(0);
if (current.getScore() >= TARGET_SCORE) {
winner.set(current);
currentPlayer.set(null);
} else {
nextPlayer();
}
}
private void nextPlayer() {
currentPlayerIndex = (currentPlayerIndex + 1) % players.size();
currentPlayer.set(players.get(currentPlayerIndex));
turnTotal.set(0);
}
}
The two support classes are
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class Player {
private final String name;
private final IntegerProperty score = new SimpleIntegerProperty();
public Player(String name) {
this.name = name;
setScore(0);
}
public IntegerProperty scoreProperty() {
return score;
}
public final int getScore() {
return scoreProperty().get();
}
public final void setScore(int score) {
scoreProperty().set(score);
}
public String getName() {
return name;
}
@Override
public String toString() {
return getName() + " [" + getScore() + "]";
}
}
and
import java.util.Random;
public class Die {
private final Random rng = new Random();
public int nextRoll() {
return 1 + rng.nextInt(6);
}
}
Note you can use this class without any UI, just by calling the methods in a procedural way. (My guess is you're already doing this as a warm up to this exercise; so you can try refactoring code you might have already written to use this model.) If you're implementing a "computer player", you just call the roll()
or hold()
methods, deciding which on the basis of the current state (turn score, total score, maybe other players' scores).
If you're implementing a UI-based human player, you can set up a UI like the one in the screen shot in the question. Add a listener to the model's currentPlayerProperty()
and disable the UI if the current player is not the human player, enable it otherwise. Then if the buttons are pressed, you know the current player is the human player, so just call roll()
in the action event handler for the "Roll" button, and hold()
in the action event handler for the "Keep" button.
UI code looks something like this:
Player human = new Player("Human player");
Player computer = new Player("Computer");
Pig game = new Pig(human, computer);
Pane humanUI = createPlayerUI(game, human);
game.currentPlayerProperty().addListener((_, currentPlayer, _) -> {
humanUI.setDisable(currentPlayer != human);
if (currentPlayer == computer) {
// invoke computer strategy here....
}
});
// ...
private Pane createPlayerUI(Pig game, Player player) {
// ...
Button hold = new Button("Hold");
hold.setOnAction( _ -> game.hold());
Button roll = new Button("Roll");
roll.setOnAction( _ -> game.roll());
Label turnTotal = new Label();
turnTotal.textProperty().bind(game.turnTotalProperty().map(
total -> String.format("Turn total: %d", total)));
// ...
VBox ui = new VBox();
// do layout...
return ui;
}
This structure avoids the need for what you're asking; you don't need to "wait" until a sequence of button presses is complete. The rest of the implementation is left as an exercise.
Some related material:
MVC-like design in JavaFX:
An example Tic-Tac-Toe game (Uses JavaFX 1.7, so the code is a bit dated but the concepts may be helpful.)
Upvotes: 5