Skip to content
Snippets Groups Projects
Commit 84e267d0 authored by Trym Hamer Gudvangen's avatar Trym Hamer Gudvangen
Browse files

Merge branch 'feat/lose-state' into 'feat/part-three'

feat: improve loose game state

See merge request !29
parents 77a185af f45bed88
No related branches found
No related tags found
2 merge requests!29feat: improve loose game state,!7Feat/part three
Pipeline #230353 passed
......@@ -8,7 +8,6 @@ import edu.ntnu.idatt2001.group_30.paths.view.views.HelpView;
import edu.ntnu.idatt2001.group_30.paths.view.views.HomeView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Objects;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
......@@ -23,7 +22,7 @@ import javafx.scene.image.ImageView;
*/
public class PlaytroughController extends Controller {
private Game game;
private Playthrough playthrough;
/* reactive state */
private final StringProperty gameTitle = new SimpleStringProperty("Playing: " + INSTANCE.getStory().getTitle());
......@@ -35,9 +34,7 @@ public class PlaytroughController extends Controller {
private final StringProperty health = new SimpleStringProperty();
private final StringProperty score = new SimpleStringProperty();
private final StringProperty gold = new SimpleStringProperty();
private final BooleanProperty gameOver = new SimpleBooleanProperty(false);
private final BooleanProperty gameWon = new SimpleBooleanProperty(false);
private boolean gameAlreadyWon = false;
private final IntegerProperty playthroughState = new SimpleIntegerProperty(PlaythroughState.PLAYING.ordinal());
/**
* Creates a new instance of the controller.
......@@ -45,38 +42,36 @@ public class PlaytroughController extends Controller {
*/
public PlaytroughController() {
super(HomeView.class, HelpView.class);
startNewGame();
startNewPlaythrough();
}
/**
* Starts a new game based on the data in the PathsSingleton.
* It also resets the reactive properties.
* Starts a new Playthrough based on the data in the PathsSingleton.
* It also resets the playthroughState to PLAYING.
*/
public void startNewGame() {
public void startNewPlaythrough() {
assert INSTANCE.getPlayer() != null;
assert INSTANCE.getStory() != null;
assert INSTANCE.getGoals() != null;
/* cleanup previous game */
gameAlreadyWon = false;
gameOver.set(false);
gameWon.set(false);
/* clear old state */
playthroughState.setValue(PlaythroughState.PLAYING.ordinal());
/* start new game */
game = new Game(new Player(INSTANCE.getPlayer()), INSTANCE.getStory(), INSTANCE.getGoals());
Passage openingPassage = game.begin();
updateReactiveProperties(openingPassage);
Game game = new Game(new Player(INSTANCE.getPlayer()), INSTANCE.getStory(), INSTANCE.getGoals());
playthrough = new Playthrough(game);
playthrough.beginPlaythrough();
updateReactiveProperties(playthrough.getCurrentPassage());
}
/**
* Makes a turn in the game.
* The player chooses a link, and the game is updated accordingly.
* @param link the link the player chooses.
* Makes a turn in the game based on the given link.
* @param link the link to follow.
*/
public void chooseLink(Link link) {
game.go(link);
link.getActions().forEach(action -> action.execute(game.getPlayer()));
updateReactiveProperties(getStory().getPassage(link));
public void makeTurn(Link link) {
PlaythroughState state = playthrough.makeTurn(link);
playthroughState.setValue(state.ordinal());
updateReactiveProperties(playthrough.getCurrentPassage());
}
/**
......@@ -87,25 +82,10 @@ public class PlaytroughController extends Controller {
updateCurrentPassage(passage);
updatePlayerData();
updateGoals();
updateGameState();
updateInventory();
updateGameTitle();
}
/**
* Computes the current state of the game.
* Updates the reactive properties based on the current state of the game.
*/
public void updateGameState() {
if (gameAlreadyWon) return;
if (game.isGameWon()) {
gameWon.setValue(true);
gameAlreadyWon = true;
}
gameOver.setValue(game.isGameOver());
}
/**
* Updates the reactive properties based on the current passage.
* @param passage the current passage.
......@@ -131,7 +111,7 @@ public class PlaytroughController extends Controller {
*/
private void updateGoals() {
goals.clear();
game.getGoals().forEach(goal -> goals.put(goal, goal.isFulfilled(getPlayer())));
playthrough.getGame().goals().forEach(goal -> goals.put(goal, goal.isFulfilled(getPlayer())));
}
/**
......@@ -146,13 +126,19 @@ public class PlaytroughController extends Controller {
* Updates the game title based on the game state.
*/
private void updateGameTitle() {
System.out.println("value: " + gameWon.getValue() + " " + gameOver.getValue() + " " + gameAlreadyWon);
if (gameOver.getValue()) {
PlaythroughState state = PlaythroughState
.fromNumber(playthroughState.getValue())
.orElse(PlaythroughState.PLAYING);
if (state == PlaythroughState.PLAYING) {
gameTitle.setValue("Playing: " + INSTANCE.getStory().getTitle());
} else if (state == PlaythroughState.DEAD) {
gameTitle.setValue("You died and lost the game!");
} else if (gameWon.getValue()) {
gameTitle.setValue("You won! (You can still play on)");
} else if (state == PlaythroughState.STUCK) {
gameTitle.setValue("You are stuck and lost the game!");
} else if (state == PlaythroughState.WON) {
gameTitle.setValue("You won the game!");
} else {
gameTitle.setValue("Playing: " + INSTANCE.getStory().getTitle());
gameTitle.setValue("You won the game, but continued playing!");
}
}
......@@ -196,20 +182,12 @@ public class PlaytroughController extends Controller {
return goals;
}
/**
* Helper method that returns the current story.
* @return the current story.
*/
private Story getStory() {
return game.getStory();
}
/**
* Helper method that returns the current player.
* @return the current player.
*/
private Player getPlayer() {
return game.getPlayer();
return playthrough.getGame().player();
}
/**
......@@ -217,7 +195,7 @@ public class PlaytroughController extends Controller {
* @return the name of the player.
*/
public String getPlayerName() {
return game.getPlayer().getName();
return playthrough.getGame().player().getName();
}
/**
......@@ -244,22 +222,6 @@ public class PlaytroughController extends Controller {
return gold;
}
/**
* Returns the game over state as an observable BooleanProperty.
* @return the game over state.
*/
public BooleanProperty getGameOver() {
return gameOver;
}
/**
* Returns the game won state as an observable BooleanProperty.
* @return the game won state.
*/
public BooleanProperty getGameWon() {
return gameWon;
}
/**
* Returns the title of the game as an observable StringProperty.
* The title of the game will be the title of the story while playing. If not playing, it will show the status of the game.
......@@ -276,4 +238,31 @@ public class PlaytroughController extends Controller {
public ImageView getCharacterImageView() {
return INSTANCE.getCharacterImageView();
}
/**
* Gets the playthrough state as an observable IntegerProperty.
* @return the image of the character.
*/
public IntegerProperty getPlaythroughState() {
return playthroughState;
}
/**
* Sets the playthrough state to WON_BUT_KEEP_PLAYING.
*/
public void setWonButKeepPlaying() {
playthrough.setWonButKeepPlaying();
playthroughState.setValue(PlaythroughState.WON_BUT_KEEP_PLAYING.ordinal());
}
/**
* A game can only be continued if it is in the PLAYING or WON_BUT_KEEP_PLAYING state.
* @return whether the game can be kept playing.
*/
public boolean canKeepPlaying() {
return (
playthroughState.get() == PlaythroughState.PLAYING.ordinal() ||
playthroughState.get() == PlaythroughState.WON_BUT_KEEP_PLAYING.ordinal()
);
}
}
......@@ -9,31 +9,25 @@ import java.util.List;
*
* @author Nicolai H. Brand, Trym Hamer Gudvangen
*/
public class Game {
private final Player player;
private final Story story;
private final List<Goal> goals;
public record Game(Player player, Story story, List<Goal> goals) {
/**
* This method constructs a Game object with the given parameters.
* @param player The player who will be playing game.
* @param story The story for the game that the player will play through.
* @param goals A list of goals that determines the desired goals of the game:
*
* @param player The player who will be playing game.
* @param story The story for the game that the player will play through.
* @param goals A list of goals that determines the desired goals of the game:
* @throws IllegalArgumentException This exception is thrown if any argument is null.
*/
public Game(Player player, Story story, List<Goal> goals) throws IllegalArgumentException {
public Game {
if (player == null || story == null || goals == null) {
throw new IllegalArgumentException("Player, story, and goals cannot be null");
}
this.player = player;
this.story = story;
this.goals = goals;
}
/**
* This method starts a game.
* @return the first passage of the story.
*
* @return the first passage of the story.
*/
public Passage begin() {
return this.story.getOpeningPassage();
......@@ -45,8 +39,9 @@ public class Game {
/**
* This method takes in a link and returns the corresponding passage of the story.
* @param link a link to a passage in the story.
* @return the corresponding passage for the link.
*
* @param link a link to a passage in the story.
* @return the corresponding passage for the link.
*/
public Passage go(Link link) {
return this.story.getPassage(link);
......@@ -54,6 +49,7 @@ public class Game {
/**
* If the player is out of health, the game is defined as over.
*
* @return {@code true} if the player is out of health, else {@code false}.
*/
public boolean isGameOver() {
......@@ -63,6 +59,7 @@ public class Game {
/**
* This method checks if the player has fulfilled all the goals of the game.
* If the player has fulfilled all the goals, the game is defined as won.
*
* @return {@code true} if the player has fulfilled all the goals, else {@code false}.
*/
public boolean isGameWon() {
......@@ -72,28 +69,4 @@ public class Game {
*/
return this.goals.stream().allMatch(goal -> goal.isFulfilled(this.player));
}
/**
* This method returns the player for the game.
* @return the player object for the game.
*/
public Player getPlayer() {
return player;
}
/**
* This method returns the story for the game.
* @return the story object for the game
*/
public Story getStory() {
return story;
}
/**
* This method returns all the goals of the game.
* @return all the goals of the game as a List{@code <Goal>}
*/
public List<Goal> getGoals() {
return goals;
}
}
package edu.ntnu.idatt2001.group_30.paths.model;
import java.util.Objects;
/**
* The Playthrough class represents a playthrough of a game.
* Its responsibility is to keep track of the current state of the playthrough, and to update it when the player makes a turn.
* It also keeps track of the current passage the player is in, as this needs to be known in order to figure out the game state.
*
* @author Nicolai H. Brand.
*/
public class Playthrough {
private final Game game;
private PlaythroughState gameState;
private Passage currentPassage;
/**
* Creates a new instance of the Playthrough class.
* @param game the game to play through.
*/
public Playthrough(Game game) {
Objects.requireNonNull(game, "Game cannot be null");
this.game = game;
this.gameState = PlaythroughState.PLAYING;
}
/**
* Begins the playthrough by starting the game.
* @return the current state of the playthrough.
*/
public PlaythroughState beginPlaythrough() {
currentPassage = game.begin();
updateGameState();
return gameState;
}
/**
* Makes a turn in the game.
* @param link the link to follow.
* @return the current state of the playthrough.
*/
public PlaythroughState makeTurn(Link link) {
Objects.requireNonNull(link, "Link cannot be null");
currentPassage = game.go(link);
link.getActions().forEach(action -> action.execute(game.player()));
updateGameState();
return gameState;
}
/**
* Updates the state of the playthrough.
* This is done by checking if the game is over, and if so, what the outcome is.
*/
private void updateGameState() {
if (gameState == PlaythroughState.WON_BUT_KEEP_PLAYING) {
return;
}
/*
* if you win, but also die in the same turn, this is counted as a loss: you can't win if you're dead :-)
*/
if (game.isGameOver()) {
gameState = PlaythroughState.DEAD;
return;
}
if (game.isGameWon()) {
gameState = PlaythroughState.WON;
return;
}
if (!currentPassage.hasLinks()) {
gameState = PlaythroughState.STUCK;
}
}
/**
* Sets the game state to WON_BUT_KEEP_PLAYING.
* This is used when the player has won the game, but still wants to keep playing.
*/
public void setWonButKeepPlaying() {
gameState = PlaythroughState.WON_BUT_KEEP_PLAYING;
}
/**
* Returns the current state of the playthrough.
* @return the current state of the playthrough.
*/
public Passage getCurrentPassage() {
return currentPassage;
}
/**
* Returns the game.
* @return the game.
*/
public Game getGame() {
return game;
}
}
package edu.ntnu.idatt2001.group_30.paths.model;
import java.util.Arrays;
import java.util.Optional;
/**
* The PlaythroughState enum represents the different states a playthrough can be in.
* It is used to keep track of the current state of the playthrough, and to update it when the player makes a turn.
* Note:
* A good systems language (c or c++) would have automatically converted the enum symbols to integers.
* This is not the case in Java, so we have to do it manually. Not very idiomatic, but it works.
*
* @author Nicolai H. Brand.
*/
public enum PlaythroughState {
PLAYING(0),
WON(1),
DEAD(2),
STUCK(3),
WON_BUT_KEEP_PLAYING(4);
private final int number;
/**
* Creates a new instance of the PlaythroughState enum.
* @param number the number of the state.
*/
PlaythroughState(int number) {
this.number = number;
}
/**
* Gets the number of the state.
* @return the number of the state.
*/
public int getNumber() {
return number;
}
/**
* Gets the PlaythroughState from a number.
* @param number the number of the state.
* @return the PlaythroughState.
*/
public static Optional<PlaythroughState> fromNumber(int number) {
return Arrays.stream(PlaythroughState.values()).filter(value -> value.getNumber() == number).findFirst();
}
}
......@@ -2,12 +2,14 @@ package edu.ntnu.idatt2001.group_30.paths.view.views;
import edu.ntnu.idatt2001.group_30.paths.controller.PlaytroughController;
import edu.ntnu.idatt2001.group_30.paths.model.Link;
import edu.ntnu.idatt2001.group_30.paths.model.PlaythroughState;
import edu.ntnu.idatt2001.group_30.paths.model.goals.Goal;
import edu.ntnu.idatt2001.group_30.paths.view.components.common.DefaultButton;
import edu.ntnu.idatt2001.group_30.paths.view.components.common.DefaultText;
import edu.ntnu.idatt2001.group_30.paths.view.components.common.Ref;
import edu.ntnu.idatt2001.group_30.paths.view.components.pop_up.AlertDialog;
import java.net.URL;
import java.util.Optional;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
......@@ -22,7 +24,6 @@ import javafx.scene.control.Separator;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Box;
import javafx.scene.text.Text;
/**
......@@ -71,24 +72,36 @@ public class PlaythroughView extends View<VBox> {
add(mainContent);
/* game state */
listenForGameStateUpdate();
}
/**
* Sets up an event listener for the game state.
*/
private void listenForGameStateUpdate() {
controller
.getGameWon()
.getPlaythroughState()
.addListener((observable, oldValue, newValue) -> {
if (newValue) {
boolean continuing = AlertDialog.showConfirmation(
"Congratulations, you completed all your goals and won the game." +
"You can still continue to the story if its not finished. Do you want to continue the story?",
"You won!"
Optional<PlaythroughState> state = PlaythroughState.fromNumber((Integer) newValue);
if (state.isEmpty()) return;
switch (state.get()) {
case WON -> {
boolean continuePlaying = AlertDialog.showConfirmation(
"Congratulations, you completed all your goals and won the game." +
"You can still continue to the story if its not finished. Do you want to continue the story?",
"You won!"
);
if (!continuePlaying) controller.goToHome(); else controller.setWonButKeepPlaying();
}
case DEAD -> AlertDialog.showConfirmation(
"You ran out of health and died. Press restart to try again.",
"You lost!"
);
case STUCK -> AlertDialog.showConfirmation(
"You got stuck and cannot continue as no paths lead to you of this mess!. Press restart to try again.",
"You lost!"
);
if (!continuing) controller.goToHome();
}
});
controller
.getGameOver()
.addListener((observable, oldValue, newValue) -> {
if (newValue) {
AlertDialog.showConfirmation("You lost!", "You lost!");
controller.goTo(HomeView.class);
}
});
}
......@@ -121,7 +134,7 @@ public class PlaythroughView extends View<VBox> {
"Are you sure you want to restart the game? All progress will be lost.",
"Confirm restart"
)
) controller.startNewGame();
) controller.startNewPlaythrough();
}
)
);
......@@ -178,13 +191,21 @@ public class PlaythroughView extends View<VBox> {
return playtrough;
}
/**
* Populates the links box with the given links.
* Sets the links to disabled if the player cannot keep playing.
* @param linksBox The box to populate.
* @param links The links to populate the box with.
*/
private void populateLinksBox(VBox linksBox, ObservableList<Link> links) {
linksBox.getChildren().clear();
for (Link link : links) {
Button button = DefaultButton.medium(link.getText(), e -> controller.chooseLink(link));
Button button = DefaultButton.medium(link.getText(), e -> controller.makeTurn(link));
button.setWrapText(true);
button.setPrefWidth(300);
button.getStyleClass().add("link-button");
button.setDisable(!controller.canKeepPlaying());
button.setOpacity(controller.canKeepPlaying() ? 1 : 0.5);
linksBox.getChildren().add(button);
}
}
......@@ -349,6 +370,11 @@ public class PlaythroughView extends View<VBox> {
return box;
}
/**
* Shows the inventory in the given content pane.
* @param inventory The inventory to show.
* @param content The pane to show the inventory in.
*/
private void showInventory(ObservableList<String> inventory, Pane content) {
content.getChildren().clear();
for (String item : inventory) {
......@@ -369,7 +395,6 @@ public class PlaythroughView extends View<VBox> {
goalBox.setAlignment(Pos.TOP_LEFT);
goalBox.setSpacing(10);
goalBox.getChildren().add(DefaultText.small(goal.toString()));
//goalBox.getChildren().add(DefaultText.small(completed ? "Completed" : "Not completed"));
URL imageUrl = getClass().getResource("/images/checkbox-" + (completed ? "marked" : "blank") + ".png");
if (imageUrl != null) {
ImageView imageView = new ImageView(imageUrl.toString());
......
......@@ -4,18 +4,23 @@ A story of War an Eilor
It was a dark and stormy night. You were walking through the woods when you came upon a large, old house.
[Try to open the door](Enter the house)
[Look inside the window](Window peek)
[Look sussy inside the window](Window peek)
[Look sussy inside the window](Sussy window peek)
::Enter the house
The door is locked. You try to open it, but it won't budge. You pick up an apple.
[Try to force the door open with your strength](Go in)
<ScoreAction>\150/
<HealthAction>\150/
<InventoryAction>\Apple/
::Window peek
You see a mysterious tall blonde and powerful young man in the house. He is wearing a black cloak and holding a wand.
[Try to open the door](Enter the house)
<ScoreAction>\50/
<HealthAction>\150/
::Sussy window peek
You looked sussily trough the window. Eilor saw you, gave a death stare, and you died.
::Go in
You take a deep breath and enter the house through the unlocked window. As you make your way through the dark corridors, you hear the sound of footsteps approaching. Suddenly, the tall blonde man appears before you, wand at the ready. Who are you? he demands. You explain that you were just passing through and sought shelter from the storm. He eyes you suspiciously but eventually nods his head in understanding. I am Eilor, he says, and this is my home. I apologize for my hostility, but I have been on edge lately. You see, our land is on the brink of war. Eilor explains to you that their land has been under siege from an invading army for months, and they have been fighting tirelessly to defend their home. He asks if you would be willing to help them in their struggle.
......
......@@ -2,7 +2,6 @@ package edu.ntnu.idatt2001.group_30.paths.model;
import static org.junit.jupiter.api.Assertions.*;
import edu.ntnu.idatt2001.group_30.paths.model.*;
import edu.ntnu.idatt2001.group_30.paths.model.goals.Goal;
import edu.ntnu.idatt2001.group_30.paths.model.goals.GoldGoal;
import edu.ntnu.idatt2001.group_30.paths.model.goals.HealthGoal;
......@@ -64,17 +63,17 @@ public class GameTest {
@Test
void can_get_player() {
// The player object from the class is created in the setup method and passed to the Game's constructor
assertEquals(player, game.getPlayer());
assertEquals(player, game.player());
}
@Test
void can_get_story() {
assertEquals(story, game.getStory());
assertEquals(story, game.story());
}
@Test
void can_get_goals() {
assertEquals(goals, game.getGoals());
assertEquals(goals, game.goals());
}
@Test
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment