Bladeren bron

Added original game files

Ondřej Schlaichert 8 maanden geleden
bovenliggende
commit
825f1f453b
29 gewijzigde bestanden met toevoegingen van 2435 en 0 verwijderingen
  1. 107 0
      src/main/java/eu/oschl/console/CommandProcessor.java
  2. 65 0
      src/main/java/eu/oschl/console/Console.java
  3. 37 0
      src/main/java/eu/oschl/console/ConsoleColor.java
  4. 53 0
      src/main/java/eu/oschl/console/Session.java
  5. 29 0
      src/main/java/eu/oschl/console/commands/Command.java
  6. 130 0
      src/main/java/eu/oschl/console/commands/Enter.java
  7. 120 0
      src/main/java/eu/oschl/console/commands/Explore.java
  8. 86 0
      src/main/java/eu/oschl/console/commands/GoBack.java
  9. 56 0
      src/main/java/eu/oschl/console/commands/Help.java
  10. 62 0
      src/main/java/eu/oschl/console/commands/Inventory.java
  11. 70 0
      src/main/java/eu/oschl/console/commands/PressButton.java
  12. 90 0
      src/main/java/eu/oschl/console/commands/Slay.java
  13. 85 0
      src/main/java/eu/oschl/console/commands/TakeItem.java
  14. 61 0
      src/main/java/eu/oschl/console/commands/UseItem.java
  15. 24 0
      src/main/java/eu/oschl/console/exceptions/InvalidInput.java
  16. 404 0
      src/main/java/eu/oschl/schmorn/Setup.java
  17. 131 0
      src/main/java/eu/oschl/textadventure/Game.java
  18. 71 0
      src/main/java/eu/oschl/textadventure/Inventory.java
  19. 82 0
      src/main/java/eu/oschl/textadventure/entities/Enemy.java
  20. 42 0
      src/main/java/eu/oschl/textadventure/entities/Entity.java
  21. 24 0
      src/main/java/eu/oschl/textadventure/exceptions/InvalidGameState.java
  22. 58 0
      src/main/java/eu/oschl/textadventure/map/Blockage.java
  23. 162 0
      src/main/java/eu/oschl/textadventure/map/Passage.java
  24. 159 0
      src/main/java/eu/oschl/textadventure/map/Room.java
  25. 48 0
      src/main/java/eu/oschl/textadventure/objects/Button.java
  26. 42 0
      src/main/java/eu/oschl/textadventure/objects/GameObject.java
  27. 71 0
      src/main/java/eu/oschl/textadventure/objects/InventoryItem.java
  28. 19 0
      src/main/java/eu/oschl/textadventure/objects/PickableObject.java
  29. 47 0
      src/main/java/eu/oschl/textadventure/objects/Weapon.java

+ 107 - 0
src/main/java/eu/oschl/console/CommandProcessor.java

@@ -0,0 +1,107 @@
+package eu.oschl.console;
+
+import eu.oschl.textadventure.Game;
+import eu.oschl.console.commands.*;
+import eu.oschl.console.exceptions.InvalidInput;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This class processes user input commands in the console application.
+ * It matches input against predefined commands and executes the corresponding action.
+ *
+ * @author Ondřej Schlaichert
+ */
+class CommandProcessor {
+    private final ArrayList<Command> commands;
+
+    public CommandProcessor(ArrayList<Command> commands) {
+        this.commands = commands;
+    }
+
+    /**
+     * Factory method to create a CommandProcessor with a predefined set of commands.
+     *
+     * @param game the game instance to be used by the commands
+     * @return a new CommandProcessor instance with initialized commands
+     */
+    public static CommandProcessor create(Game game) {
+        var commands = new ArrayList<>(List.of(
+                new Explore(game),
+                new Enter(game),
+                new GoBack(game),
+                new Inventory(game),
+                new TakeItem(game),
+                new UseItem(game),
+                new PressButton(game),
+                new Slay(game)
+        ));
+
+        commands.add(new Help(commands));
+
+        return new CommandProcessor(commands);
+    }
+
+    /**
+     * Adds a new command to the command processor.
+     *
+     * @param command the command to be added
+     */
+    public void addCommand(Command command) {
+        this.commands.add(command);
+    }
+
+    /**
+     * Processes the input command string and executes the corresponding command.
+     *
+     * @param input the input command string
+     * @throws InvalidInput if the input is invalid or no matching command is found
+     */
+    public void processInput(String input) throws InvalidInput {
+        var inputTokens = input.trim().split(" ");
+
+        if (inputTokens.length == 0) {
+            throw new InvalidInput("Input cannot be empty");
+        }
+
+        for (Command command : commands) {
+            for (String trigger : command.getTriggers()) {
+                var triggerTokens = trigger.trim().split(" ");
+
+                if (doesInputMatchTrigger(inputTokens, triggerTokens)) {
+                    command.execute(Arrays.copyOfRange(inputTokens, triggerTokens.length, inputTokens.length));
+                    return;
+                }
+            }
+        }
+
+        throw new InvalidInput("Command not found");
+    }
+
+    /**
+     * Checks if the input tokens match the trigger tokens.
+     *
+     * @param inputTokens the tokens from the user input
+     * @param triggerTokens the tokens from the command trigger
+     * @return true if the input matches the trigger, false otherwise
+     */
+    private boolean doesInputMatchTrigger(String[] inputTokens, String[] triggerTokens) {
+        if (inputTokens.length == 0) {
+            return false;
+        }
+
+        for (var i = 0; i < triggerTokens.length; i++) {
+            if (i >= inputTokens.length) {
+                return false;
+            }
+
+            if (!inputTokens[i].equalsIgnoreCase(triggerTokens[i])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}

+ 65 - 0
src/main/java/eu/oschl/console/Console.java

@@ -0,0 +1,65 @@
+package eu.oschl.console;
+
+import java.util.InputMismatchException;
+import java.util.Scanner;
+
+/**
+ * This class provides methods for console input and output operations.
+ * It allows printing messages in different colors and reading user input.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Console {
+    private static final Scanner scanner = new Scanner(System.in);
+
+    /**
+     * Prints a message to the console with the specified color.
+     *
+     * @param message the message to print
+     * @param color the color in which to print the message
+     */
+    public static void print(String message, ConsoleColor color) {
+        System.out.print(color.getCode() + message + ConsoleColor.DEFAULT.getCode());
+    }
+
+    /**
+     * Prints a message to the console in the default color.
+     *
+     * @param message the message to print
+     */
+    public static void print(String message) {
+        print(message, ConsoleColor.DEFAULT);
+    }
+
+
+    /**
+     * Prints a line break to the console.
+     */
+    public static void printLine() {
+        print("\n");
+    }
+
+    /**
+     * Reads a string input from the console with a prompt.
+     *
+     * @param prompt the prompt message to display
+     * @return the input string entered by the user
+     */
+    public static String readString(String prompt) {
+        print(prompt + " ", ConsoleColor.YELLOW);
+        return scanner.nextLine();
+    }
+
+    /**
+     * Reads a string input from the console without a prompt.
+     *
+     * @return the input string entered by the user
+     */
+    public static int readInt() throws InputMismatchException {
+        try {
+            return Integer.parseInt(scanner.nextLine().trim());
+        } catch (NumberFormatException e) {
+            throw new InputMismatchException("Input was not an integer value");
+        }
+    }
+}

+ 37 - 0
src/main/java/eu/oschl/console/ConsoleColor.java

@@ -0,0 +1,37 @@
+package eu.oschl.console;
+
+/**
+ * This enum defines various console colors using ANSI escape codes.
+ * These colors can be used to format text output in the console.
+ *
+ * @author Ondřej Schlaichert
+ */
+public enum ConsoleColor {
+    DEFAULT("\u001B[0m"),
+    BLACK("\u001B[30m"),
+    RED("\u001B[31m"),
+    GREEN("\u001B[32m"),
+    YELLOW("\u001B[33m"),
+    BLUE("\u001B[34m"),
+    MAGENTA("\u001B[35m"),
+    CYAN("\u001B[36m"),
+    WHITE("\u001B[37m"),
+    BG_BLACK("\u001B[40m"),
+    BG_RED("\u001B[41m"),
+    BG_GREEN("\u001B[42m"),
+    BG_YELLOW("\u001B[43m"),
+    BG_BLUE("\u001B[44m"),
+    BG_MAGENTA("\u001B[45m"),
+    BG_CYAN("\u001B[46m"),
+    BG_WHITE("\u001B[47m");
+
+    private final String code;
+
+    ConsoleColor(String code) {
+        this.code = code;
+    }
+
+    public String getCode() {
+        return code;
+    }
+}

+ 53 - 0
src/main/java/eu/oschl/console/Session.java

@@ -0,0 +1,53 @@
+package eu.oschl.console;
+
+import eu.oschl.textadventure.Game;
+import eu.oschl.console.exceptions.InvalidInput;
+
+import java.util.Random;
+
+/**
+ * This class represents a game session in the console application.
+ * It processes user commands and handles the game loop.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Session {
+    private final Game game;
+    private final CommandProcessor commandProcessor;
+    private final Random random;
+
+    public Session(Game game) {
+        this.game = game;
+        this.commandProcessor = CommandProcessor.create(game);
+        this.random = new Random();
+    }
+
+    /**
+     * Starts the game session, displaying the prologue and entering the main game loop.
+     * The loop continues until the game is no longer running, after which it displays the epilogue and the game ends.
+     */
+    public void start() {
+        Console.print(game.getPrologue());
+        Console.printLine();
+
+        while (game.isRunning()) {
+            var input = Console.readString(">");
+
+            if (input.isEmpty()) {
+                continue;
+            }
+
+            try {
+                this.commandProcessor.processInput(input);
+            } catch (InvalidInput e) {
+                var message = game.getUnknownCommandMessages()[random.nextInt(game.getUnknownCommandMessages().length)];
+                Console.print(message, ConsoleColor.RED);
+            }
+
+            Console.printLine();
+        }
+
+        Console.printLine();
+        Console.print(game.getEpilogue());
+    }
+}

+ 29 - 0
src/main/java/eu/oschl/console/commands/Command.java

@@ -0,0 +1,29 @@
+package eu.oschl.console.commands;
+
+/***
+ * This interface defines the structure for commands in the console application.
+ *
+ * @author Ondřej Schlaichert
+ */
+public interface Command {
+    /**
+     * Returns an array of strings that trigger this command.
+     *
+     * @return an array of trigger strings
+     */
+    String[] getTriggers();
+
+    /**
+     * Returns a description of what the command does.
+     *
+     * @return a string description of the command
+     */
+    String getDescription();
+
+    /**
+     * Executes the command with the provided arguments.
+     *
+     * @param args an array of strings representing the command arguments
+     */
+    void execute(String[] args);
+}

+ 130 - 0
src/main/java/eu/oschl/console/commands/Enter.java

@@ -0,0 +1,130 @@
+package eu.oschl.console.commands;
+
+import eu.oschl.textadventure.Game;
+import eu.oschl.textadventure.exceptions.InvalidGameState;
+import eu.oschl.textadventure.map.Passage;
+import eu.oschl.console.Console;
+import eu.oschl.console.ConsoleColor;
+
+/**
+ * This command allows the player to enter a passage in the game.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Enter implements Command {
+    private final Game game;
+
+    public Enter(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String[] getTriggers() {
+        return new String[]{
+                "enter",
+                "go through",
+                "go to",
+                "move through",
+                "move to",
+                "move",
+                "walk through",
+                "walk to",
+                "walk",
+                "pass through",
+                "pass to",
+                "pass",
+                "through",
+                "open",
+        };
+    }
+
+    @Override
+    public String getDescription() {
+        return "walk through a passage";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        if (args.length == 0) {
+            Console.print("Where?", ConsoleColor.RED);
+            return;
+        }
+
+        String input = String.join(" ", args);
+
+        var passages = this.game.getCurrentRoom().getPassages();
+        Passage passage = null;
+
+        for (Passage p : passages) {
+            if (p.getName().equalsIgnoreCase(input)) {
+                passage = p;
+                break;
+            }
+        }
+
+        if (passage == null) {
+            for (Passage p : passages) {
+                if (p.getOtherRoom(game.getCurrentRoom()).getName().equalsIgnoreCase(input)) {
+                    passage = p;
+                    break;
+                }
+            }
+        }
+
+        if (passage == null) {
+            Console.print("This passage does not exist.", ConsoleColor.RED);
+            return;
+        }
+
+        var printEnterText = !passage.getOtherRoom(game.getCurrentRoom()).wasEntered();
+
+        var result = passage.passThrough(false);
+
+        if (!result) {
+            if (game.getCurrentRoom().getEnemy().isPresent()) {
+                Console.print(
+                        game.getCurrentRoom().getEnemy().get().getName() + " blocks the way. It is only possible to go back.",
+                        ConsoleColor.RED
+                );
+            } else if (passage.getBlockage().isPresent()) {
+                Console.print(passage.getBlockage().get().getDescription(), ConsoleColor.RED);
+            } else {
+                Console.print("The passage is blocked.", ConsoleColor.RED);
+            }
+            return;
+        }
+
+        printSuccessfulPassage(printEnterText);
+    }
+
+    /**
+     * Prints a message indicating that the player has successfully passed through a passage.
+     *
+     * @param printEnterText whether to print the enter text of the current room
+     */
+    private void printSuccessfulPassage(boolean printEnterText) {
+        if (game.getLastPassage().isEmpty()) {
+            throw new InvalidGameState("Last passages stack is empty even though a passage was passed");
+        }
+
+        Console.print("Passed through the ");
+        Console.print(game.getLastPassage().get().getName(), ConsoleColor.YELLOW);
+        Console.print(" and entered ");
+        Console.print(game.getCurrentRoom().getName(), ConsoleColor.BLUE);
+        Console.print(".");
+
+        if (game.getCurrentRoom().isBlockedByEnemy()) {
+            Console.printLine();
+            Console.print("...", ConsoleColor.WHITE);
+            Console.printLine();
+            Console.print("There is somebody in here.", ConsoleColor.WHITE);
+        }
+
+        if (printEnterText && game.getCurrentRoom().getEnterText().isPresent()) {
+            Console.printLine();
+            Console.printLine();
+            Console.print(game.getCurrentRoom().getEnterText().get());
+            Console.printLine();
+        }
+    }
+}

+ 120 - 0
src/main/java/eu/oschl/console/commands/Explore.java

@@ -0,0 +1,120 @@
+package eu.oschl.console.commands;
+
+import eu.oschl.textadventure.Game;
+import eu.oschl.textadventure.map.Passage;
+import eu.oschl.console.Console;
+import eu.oschl.console.ConsoleColor;
+
+/**
+ * This command allows the player to explore the current room and its surroundings.
+ * It provides information about objects, passages, and enemies in the room.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Explore implements Command {
+    private final Game game;
+
+    public Explore(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String[] getTriggers() {
+        return new String[]{
+                "look around",
+                "look",
+                "explore around",
+                "explore",
+                "examine around",
+                "examine",
+        };
+    }
+
+    @Override
+    public String getDescription() {
+        return "find out information about the current area";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        if (args.length > 0) {
+            Console.print("This doesn't make any sense.", ConsoleColor.RED);
+            return;
+        }
+
+        var room = this.game.getCurrentRoom();
+        var objects = room.getObjects();
+        var passages = room.getPassages();
+
+        Console.print(room.getName(), ConsoleColor.BG_BLUE);
+        Console.printLine();
+        Console.print(room.getDescription(), ConsoleColor.BLUE);
+
+        if (room.getEnemy().isPresent()) {
+            if (room.getEnemy().get().isAlive()) {
+                Console.printLine();
+                Console.printLine();
+
+                Console.print(room.getEnemy().get().getName(), ConsoleColor.BG_RED);
+                Console.print(" blocks the way.", ConsoleColor.RED);
+                Console.printLine();
+                Console.print(room.getEnemy().get().getDescription(), ConsoleColor.RED);
+                Console.printLine();
+                Console.printLine();
+                Console.print(room.getEnemy().get().getName(), ConsoleColor.RED);
+                Console.print(" must be defeated to continue further.");
+
+                return;
+            } else {
+                Console.printLine();
+                Console.printLine();
+                Console.print("The corpse of ", ConsoleColor.WHITE);
+                Console.print(room.getEnemy().get().getName(), ConsoleColor.BG_RED);
+                Console.print(" lies on the ground.", ConsoleColor.WHITE);
+            }
+        }
+
+        if (!objects.isEmpty()) {
+            Console.printLine();
+            Console.printLine();
+
+            if (objects.size() == 1) {
+                Console.print("there is something in this room: ", ConsoleColor.MAGENTA);
+            } else {
+                Console.print("there are " + objects.size() + " things in this room: ", ConsoleColor.MAGENTA);
+            }
+            for (var object : objects) {
+                Console.printLine();
+                Console.print(" * " + object.getName(), ConsoleColor.MAGENTA);
+                Console.print(", " + object.getDescription(), ConsoleColor.WHITE);
+            }
+        }
+
+        if (!passages.isEmpty()) {
+            Console.printLine();
+            Console.printLine();
+
+            if (passages.size() == 1) {
+                Console.print("there is a passage leading out of here: ", ConsoleColor.YELLOW);
+            } else {
+                Console.print("there are " + passages.size() + " passages leading out of here: ", ConsoleColor.YELLOW);
+            }
+
+            for (Passage passage : passages) {
+                Console.printLine();
+                Console.print(" * " + passage.getName(), ConsoleColor.YELLOW);
+
+                if (passage.getDescription().isPresent()) {
+                    Console.print(", " + passage.getDescription().get(), ConsoleColor.WHITE);
+                }
+
+                if (passage.isSeeThrough() || passage.getOtherRoom(room).wasEntered()) {
+                    Console.print(" - leading to ");
+                    Console.print(passage.getOtherRoom(room).getName(), ConsoleColor.BLUE);
+                } else {
+                    Console.print(" - it's impossible to see what's on the other side", ConsoleColor.RED);
+                }
+            }
+        }
+    }
+}

+ 86 - 0
src/main/java/eu/oschl/console/commands/GoBack.java

@@ -0,0 +1,86 @@
+package eu.oschl.console.commands;
+
+import eu.oschl.textadventure.Game;
+import eu.oschl.textadventure.map.Passage;
+import eu.oschl.console.Console;
+import eu.oschl.console.ConsoleColor;
+
+/**
+ * This command allows the player to go back to the previous location in the game.
+ * It checks if there is a previous passage and allows the player to return to it.
+ * If there is no previous passage, it informs the player that they are at the starting point.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class GoBack implements Command {
+    private final Game game;
+
+    public GoBack(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String[] getTriggers() {
+        return new String[]{
+                "go back",
+                "return",
+                "back",
+                "exit",
+                "leave",
+        };
+    }
+
+    @Override
+    public String getDescription() {
+        return "return to previous location";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        if (args.length > 0) {
+            Console.print("This doesn't make any sense.", ConsoleColor.RED);
+            return;
+        }
+
+        if (game.getLastPassage().isEmpty()) {
+            Console.print("Can't go back, the current room is where it all began.", ConsoleColor.RED);
+            return;
+        }
+
+        var passage = game.getLastPassage().get();
+
+        var result = passage.passThrough(true);
+        if (!result) {
+            if (game.getCurrentRoom().getEnemy().isPresent()) {
+                Console.print(
+                        game.getCurrentRoom().getEnemy().get().getName() + " blocks the way. It is only possible to go back.",
+                        ConsoleColor.RED
+                );
+            } else if (passage.getBlockage().isPresent()) {
+                Console.print(passage.getBlockage().get().getDescription(), ConsoleColor.RED);
+            } else {
+                Console.print("The passage is blocked.", ConsoleColor.RED);
+            }
+            return;
+        }
+
+        game.removeLastPassage();
+
+        printSuccessfulPassage(passage);
+    }
+
+    private void printSuccessfulPassage(Passage passage) {
+        Console.print("Passed through the ");
+        Console.print(passage.getName(), ConsoleColor.YELLOW);
+        Console.print(" and entered ");
+        Console.print(game.getCurrentRoom().getName(), ConsoleColor.BLUE);
+        Console.print(".");
+
+        if (game.getCurrentRoom().isBlockedByEnemy()) {
+            Console.printLine();
+            Console.print("...", ConsoleColor.WHITE);
+            Console.printLine();
+            Console.print("There is somebody in here.", ConsoleColor.WHITE);
+        }
+    }
+}

+ 56 - 0
src/main/java/eu/oschl/console/commands/Help.java

@@ -0,0 +1,56 @@
+package eu.oschl.console.commands;
+
+import eu.oschl.console.Console;
+import eu.oschl.console.ConsoleColor;
+
+import java.util.ArrayList;
+
+/**
+ * This command displays a list of all available actions in the console application.
+ * It provides a way for users to see what commands they can use.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Help implements Command {
+    private final ArrayList<Command> commands;
+
+    public Help(ArrayList<Command> commands) {
+        this.commands = commands;
+    }
+
+    @Override
+    public String[] getTriggers() {
+        return new String[]{
+                "help",
+                "actions",
+                "commands",
+                "list actions",
+                "list commands",
+        };
+    }
+
+    @Override
+    public String getDescription() {
+        return "display currently available actions";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        if (args.length > 0) {
+            Console.print("This doesn't make any sense.", ConsoleColor.RED);
+            return;
+        }
+
+        Console.print("available actions:", ConsoleColor.BG_CYAN);
+
+        for (var command : commands) {
+            if (command == this) {
+                continue;
+            }
+
+            Console.printLine();
+            Console.print(" * [" + command.getTriggers()[0] + "]", ConsoleColor.YELLOW);
+            Console.print(" - " + command.getDescription(), ConsoleColor.WHITE);
+        }
+    }
+}

+ 62 - 0
src/main/java/eu/oschl/console/commands/Inventory.java

@@ -0,0 +1,62 @@
+package eu.oschl.console.commands;
+
+import eu.oschl.textadventure.Game;
+import eu.oschl.console.Console;
+import eu.oschl.console.ConsoleColor;
+
+/**
+ * This command allows the player to view their current inventory in the game.
+ * It displays all items and weapons the player possesses.
+ * If the inventory is empty, it informs the player accordingly.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Inventory implements Command {
+    private final Game game;
+
+    public Inventory(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String[] getTriggers() {
+        return new String[]{
+                "inventory",
+                "items",
+        };
+    }
+
+    @Override
+    public String getDescription() {
+        return "see current possessions";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        if (args.length > 0) {
+            Console.print("This doesn't make any sense.", ConsoleColor.RED);
+            return;
+        }
+
+        var inventory = game.getInventory();
+
+        if (inventory.getItems().isEmpty() && inventory.getWeapon().isEmpty()) {
+            Console.print("No possessions.", ConsoleColor.MAGENTA);
+            return;
+        }
+
+        Console.print("current posessions:", ConsoleColor.BG_MAGENTA);
+
+        if (inventory.getWeapon().isPresent()) {
+            Console.printLine();
+            Console.print("* " + inventory.getWeapon().get().getName(), ConsoleColor.RED);
+            Console.print(", " + inventory.getWeapon().get().getDescription(), ConsoleColor.WHITE);
+        }
+
+        for (var item : inventory.getItems()) {
+            Console.printLine();
+            Console.print("* " + item.getName(), ConsoleColor.MAGENTA);
+            Console.print(", " + item.getDescription(), ConsoleColor.WHITE);
+        }
+    }
+}

+ 70 - 0
src/main/java/eu/oschl/console/commands/PressButton.java

@@ -0,0 +1,70 @@
+package eu.oschl.console.commands;
+
+import eu.oschl.textadventure.Game;
+import eu.oschl.textadventure.objects.Button;
+import eu.oschl.console.Console;
+import eu.oschl.console.ConsoleColor;
+
+/**
+ * This command allows the player to press a button in the game.
+ * It checks if the specified object is a button and attempts to press it.
+ * If the button has already been pressed, it informs the player.
+ * If the object is not a button, it informs the player that it cannot be pressed.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class PressButton implements Command {
+    private final Game game;
+
+    public PressButton(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String[] getTriggers() {
+        return new String[]{
+                "press",
+                "push",
+                "activate",
+                "trigger",
+        };
+    }
+
+    @Override
+    public String getDescription() {
+        return "press a button";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        if (args.length == 0) {
+            Console.print("What button?", ConsoleColor.RED);
+            return;
+        }
+
+        for (var object : this.game.getCurrentRoom().getObjects()) {
+            if (object.getName().equalsIgnoreCase(String.join(" ", args))) {
+                if (object instanceof Button button) {
+                    var result = button.press();
+
+                    if (result) {
+                        Console.print(button.getPressText(), ConsoleColor.MAGENTA);
+                    } else {
+                        Console.print("Button ", ConsoleColor.RED);
+                        Console.print(button.getName(), ConsoleColor.MAGENTA);
+                        Console.print(" has already been pressed.", ConsoleColor.RED);
+                    }
+
+                } else {
+                    Console.print("It's impossible to press ", ConsoleColor.RED);
+                    Console.print(object.getName(), ConsoleColor.MAGENTA);
+                    Console.print(". It's not a button.", ConsoleColor.RED);
+                }
+                return;
+            }
+        }
+
+        Console.print("That item is not here.", ConsoleColor.RED);
+    }
+}
+

+ 90 - 0
src/main/java/eu/oschl/console/commands/Slay.java

@@ -0,0 +1,90 @@
+package eu.oschl.console.commands;
+
+import eu.oschl.textadventure.Game;
+import eu.oschl.console.Console;
+import eu.oschl.console.ConsoleColor;
+
+/**
+ * This command allows the player to slay an enemy in the game.
+ * It checks if the specified enemy is present in the current room,
+ * and if the player has a weapon strong enough to defeat it.
+ * If successful, it displays a message indicating the enemy has been slain.
+ * If not, it informs the player why they cannot slay the enemy.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Slay implements Command {
+    private final Game game;
+
+    public Slay(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String[] getTriggers() {
+        return new String[]{
+                "slay",
+                "kill",
+                "attack",
+                "fight",
+                "defeat",
+                "destroy",
+        };
+    }
+
+    @Override
+    public String getDescription() {
+        return "fight an enemy";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        if (args.length == 0) {
+            Console.print("Who?", ConsoleColor.RED);
+            return;
+        }
+
+        if (
+                this.game.getCurrentRoom().getEnemy().isEmpty() ||
+                !this.game.getCurrentRoom().getEnemy().get().getName().equalsIgnoreCase(String.join(" ", args))
+        ) {
+            Console.print("This enemy is not here.", ConsoleColor.RED);
+            return;
+        }
+
+        var enemy = this.game.getCurrentRoom().getEnemy().get();
+
+        if (enemy.strength > 0 && game.getInventory().getWeapon().isEmpty()) {
+            Console.print(enemy.getName() + " is too strong. A weapon is required.", ConsoleColor.RED);
+            return;
+        }
+
+        if (enemy.strength > 0 && enemy.strength > game.getInventory().getWeapon().get().getDamage()) {
+            Console.print(enemy.getName() + " is too strong. A stronger weapon is required.", ConsoleColor.RED);
+            return;
+        }
+
+        var result = enemy.kill();
+
+        if (result) {
+            if (game.getInventory().getWeapon().isPresent()) {
+                Console.print(game.getInventory().getWeapon().get().getAttackText());
+            }
+
+            if (enemy.getKillText().isPresent()) {
+                Console.printLine();
+                Console.print(enemy.getKillText().get());
+                Console.printLine();
+            }
+
+            Console.printLine();
+            Console.print(enemy.getName(), ConsoleColor.BG_RED);
+            Console.print(" lies murdered on the floor.");
+        } else {
+            Console.print("It's impossible to kill ", ConsoleColor.RED);
+            Console.print(enemy.getName(), ConsoleColor.RED);
+            Console.print(".", ConsoleColor.RED);
+        }
+    }
+}
+

+ 85 - 0
src/main/java/eu/oschl/console/commands/TakeItem.java

@@ -0,0 +1,85 @@
+package eu.oschl.console.commands;
+
+import eu.oschl.textadventure.Game;
+import eu.oschl.textadventure.objects.PickableObject;
+import eu.oschl.textadventure.objects.Weapon;
+import eu.oschl.console.Console;
+import eu.oschl.console.ConsoleColor;
+
+/**
+ * This command allows the player to take an item from the current room.
+ * It checks if the specified object is pickable and attempts to pick it up.
+ * If the item is a weapon, it checks if it is stronger than the current one.
+ * If the item cannot be picked up, it informs the player accordingly.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class TakeItem implements Command {
+    private final Game game;
+
+    public TakeItem(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String[] getTriggers() {
+        return new String[]{
+                "take",
+                "grab",
+                "pick up",
+        };
+    }
+
+    @Override
+    public String getDescription() {
+        return "take an item";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        if (args.length == 0) {
+            Console.print("What item?", ConsoleColor.RED);
+            return;
+        }
+
+        if (game.getCurrentRoom().isBlockedByEnemy()) {
+            Console.print("The way is blocked. It's impossible to pick up items.", ConsoleColor.RED);
+            return;
+        }
+
+        for (var object : this.game.getCurrentRoom().getObjects()) {
+            if (object.getName().equalsIgnoreCase(String.join(" ", args))) {
+                if (object instanceof PickableObject item) {
+                    var result = item.pickUp();
+
+                    if (item instanceof Weapon) {
+                        if (result) {
+                            Console.print("Weapon ", ConsoleColor.MAGENTA);
+                            Console.print(item.getName(), ConsoleColor.BG_MAGENTA);
+                            Console.print(" obtained.", ConsoleColor.MAGENTA);
+                        } else {
+                            Console.print("This weapon is weaker than the current one.", ConsoleColor.RED);
+                        }
+                    } else {
+                        if (result) {
+                            Console.print("Item ", ConsoleColor.MAGENTA);
+                            Console.print(item.getName(), ConsoleColor.BG_MAGENTA);
+                            Console.print(" added to inventory.", ConsoleColor.MAGENTA);
+                        } else {
+                            Console.print("Carrying too many items.", ConsoleColor.MAGENTA);
+                        }
+                    }
+
+                } else {
+                    Console.print("Can't pick up ", ConsoleColor.RED);
+                    Console.print(object.getName(), ConsoleColor.MAGENTA);
+                    Console.print(".", ConsoleColor.RED);
+                }
+                return;
+            }
+        }
+
+        Console.print("That item is not here.", ConsoleColor.RED);
+    }
+}
+

+ 61 - 0
src/main/java/eu/oschl/console/commands/UseItem.java

@@ -0,0 +1,61 @@
+package eu.oschl.console.commands;
+
+import eu.oschl.textadventure.Game;
+import eu.oschl.console.Console;
+import eu.oschl.console.ConsoleColor;
+
+/**
+ * This command allows the player to use an item in the game.
+ * It checks if the specified item exists in the player's inventory and attempts to use it.
+ * If the item is used successfully, it displays a message; otherwise, it informs the player that the item cannot be used.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class UseItem implements Command {
+    private final Game game;
+
+    public UseItem(Game game) {
+        this.game = game;
+    }
+
+    @Override
+    public String[] getTriggers() {
+        return new String[]{
+                "use",
+        };
+    }
+
+    @Override
+    public String getDescription() {
+        return "use an item";
+    }
+
+    @Override
+    public void execute(String[] args) {
+        if (args.length == 0) {
+            Console.print("What item?", ConsoleColor.RED);
+            return;
+        }
+
+        var item = this.game.getInventory().getItems().stream()
+                .filter(inventoryItem -> inventoryItem.getName().equalsIgnoreCase(String.join(" ", args)))
+                .findFirst()
+                .orElse(null);
+
+        if (item == null) {
+            Console.print("That item does not exist.", ConsoleColor.RED);
+            return;
+        }
+
+        var result = item.use();
+
+        if (result) {
+            Console.print(item.getUseText(), ConsoleColor.MAGENTA);
+        } else {
+            Console.print("It's impossible to use ", ConsoleColor.RED);
+            Console.print(item.getName(), ConsoleColor.MAGENTA);
+            Console.print(" here.", ConsoleColor.RED);
+        }
+    }
+}
+

+ 24 - 0
src/main/java/eu/oschl/console/exceptions/InvalidInput.java

@@ -0,0 +1,24 @@
+package eu.oschl.console.exceptions;
+
+/**
+ * This exception is thrown when the input provided by the user is invalid.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class InvalidInput extends Exception {
+    public InvalidInput() {
+        super();
+    }
+
+    public InvalidInput(String message) {
+        super(message);
+    }
+
+    public InvalidInput(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public InvalidInput(Throwable cause) {
+        super(cause);
+    }
+}

+ 404 - 0
src/main/java/eu/oschl/schmorn/Setup.java

@@ -0,0 +1,404 @@
+package eu.oschl.schmorn;
+
+import eu.oschl.textadventure.map.Blockage;
+import eu.oschl.textadventure.Game;
+import eu.oschl.textadventure.map.Passage;
+import eu.oschl.textadventure.map.Room;
+import eu.oschl.textadventure.entities.Enemy;
+import eu.oschl.textadventure.objects.Button;
+import eu.oschl.textadventure.objects.InventoryItem;
+import eu.oschl.textadventure.objects.Weapon;
+
+/**
+ * This class is responsible for setting up the game object
+ *
+ * @author Ondřej Schlaichert
+ */
+class Setup {
+    public static Game createGame() {
+        // Base game setup
+        var prologue = """
+                The sun sets over a misty day as the witch Oglunda, ruler of the Seven Swamps, rides her carriage down the Royal Road. An agreement has been made, and she comes now to fulfil her side of it.
+                
+                The prince is waiting. Face illuminated by flickering candlelight, Schmorn, high prince of the Grey Princedom, stares out of the window of his highest tower and watches as the witch's carriage makes its way towards the gate. When the carriage stops, he signals the guards, who reluctantly raise the iron bars.
+                
+                Moments later, Oglunda climbs the tower steps and enters. She sits across from the prince at a heavy wooden desk.
+                
+                "So, you're sure?" she asks, her voice raspy. "This is what you want? You've looked upon the world, weighed all the possibilities, and decided that what you desire most is unlimited power, no matter the cost?"
+                
+                "Yes," Schmorn replies.
+                
+                "Do you believe this will give you what you lack? The kingdoms snicker behind closed doors, whispering of your blatant incompetence, mistreatment of your subjects, the living conditions of your people. 'An insecure prince for an insecure land,' they say. Have you considered that this will not change their minds? That this is not the right way to go about it?"
+                
+                "They will fear me. All of them. None will be a match for what I'll be capable of," the prince replies, voice shaky with morbid excitement. "So keep your mouth shut, witch. You have not come here to speak of things already decided. Have you brought it?"
+                
+                "I have," Oglunda replies, reaches into her cloak and withdraws a small envelope, emptying its contents onto the table. In front of them now lies a small, silver ring, simple and unadorned, yet unnaturally beautiful.
+                
+                "I warn you for the last time, Schmorn, high prince of the Grey Princedom. Once you wear it, there will be consequences. Both for your realm, and for your soul."
+                
+                Schmorn, barely listening, eyes glued to the small ring, grabs it in his fingers and raises his hand toward a chandelier above. The ring reflects the golden light, casting delicate glimmers all around the room. He raises the ring finger of his left hand and determinedly puts it on.
+                
+                As he does so, his body starts to undulate and shift, and just a few moments later, the prince is gone completely. On the table now lies the ring, and, right next to it, squirms a large, repulsive worm.
+                
+                "I gave you a chance, idiot," the witch says, pockets the ring, grabs the worm, and makes her way down the tower. In the courtyard, people gather, murmuring.
+                
+                "Look here!" she calls and she raises Schmorn high above her head for everyone to see. "This is your prince. This is what remains of him. Take me to your deepest well!"
+                
+                The former prince's subjects lead Oglunda towards an ancient stone well in the middle of the courtyard, where she stops and triumphantly casts the worm into the depths. The people cheer, finally free of the rule of an incompetent, dangerous tyrant.
+                
+                And Schmorn falls...
+                """;
+
+        var epilogue = """
+                Schmorn stares at the small ring in Oglunda's lifeless hand. There is nothing stopping him now. He can turn himself back into a human, regain his former glory, and rule the Grey Princedom once again. Perhaps there is something he had learned from all this, something that will make him a better ruler. Perhaps he will be able to change the way his subjects see him, and they will no longer fear him, but respect him.
+                
+                The door to the dungeon swings open, and outside stands a crowd of Schmorn's subjects. They see Oglunda's lifeless body, and they see Schmorn, the worm, holding the ring. They roar in anger, and before Schmorn gathers his bearings, they storm the dungeon, ready to kill him for what he has done. They throw stones and other objects at him, and Schmorn, unable to defend himself, is overwhelmed. He falls to the ground, and the crowd closes in on him.
+                
+                And so, Schmorn, the worm, dies in the Royal Dungeon, surrounded by his subjects, who have finally had enough of his tyranny. The Grey Princedom is free from his rule, and the people can finally live in peace.
+                """;
+
+        var unknownCommandMessages = new String[]{
+                "Schmorn doesn't know how to do that.",
+                "Schmorn does not understand what that means.",
+                "Schmorn is confused.",
+                "Schmorn is not sure what to do.",
+        };
+
+        // Rooms
+        var royalWell = new Room(
+                "Royal Well",
+                "The bottom of the deepest well of the Grey Princedom. Fortunately for the prince, there is no water in it."
+        );
+
+        var crossroads = new Room(
+                "Crossroads",
+                "A damp chamber in the Royal Dungeon, full of unnerving, dark tunnels and entrances to other locations."
+        );
+
+        var sewerEntrance = new Room(
+                "Sewer Entrance",
+                "An entrance room of the Royal Sewer."
+        );
+
+        var swordsmithRatsLair = new Room(
+                "Swordsmith Rat's Lair",
+                "The lair of the Rat Swordsmith. He is a master of his craft, but he is also a rat.",
+                "Schmorn enters the chamber and is greeted by an unnerving sight. The Swordsmith Rat is lying on the floor, surrounded by his tools and weapons. He looks up at Schmorn with a mixture of fear and confusion - somehow, he seems to recognise Schmorn despite his new worm-like appearance. Once a swordsmith of the greatest reputation, he has been broken down by his imprisonment and has produced no remarkable weapons to speak of."
+        );
+
+        var prisonEntrance = new Room(
+                "Prison Entrance",
+                "An entrance room to Schmorn's Royal Prison."
+        );
+
+        var cellA = new Room(
+                "Cell A",
+                "The first cell of the Royal Prison."
+        );
+
+        var cellB = new Room(
+                "Cell B",
+                "The second cell of the Royal Prison."
+        );
+
+        var guardRoom = new Room(
+                "Guard Room",
+                "In days long gone, when the Grey Princedom wasn't in disarray, this room was occuppied by a guard who took care of the prisoners."
+        );
+
+        var prisonsEnd = new Room(
+                "Prison's End",
+                "The prison does not continue beyond this point."
+        );
+
+        var darkEntrance = new Room(
+                "Dark Entrance",
+                "An entrance to the deepest and darkest parts of the Royal Dungeon."
+        );
+
+        var blackAltar = new Room(
+                "Black Altar",
+                "An enormous, menacing room, full of bones and dead bodies. The centerpiece is a large, black altar made of stone, surrounded by a circle of candles. On the surface are strange runes and symbols, which appear to be bleeding.",
+                "As Schmorn enters, fear and nausea overcomes him. The air is putrid, the room stinks of death. In the middle stands a stone altar, surrounded by candles. On the surface are strange runes and symbols, which appear to be bleeding. Schmorn knows he is not alone. There is a being here, one not of this world."
+        );
+
+        var shadowChamber = new Room(
+                "Shadow Chamber",
+                "A dark chamber in the Royal Dungeon, where lanterns don't work and shadows reign."
+        );
+
+        var pathwaysEnd = new Room(
+                "Pathway's End",
+                "The Pathway of Darkness, the darkest part of the Royal Dungeon, does not continue beyond this point."
+        );
+
+        var royalKitchensEntrance = new Room(
+                "Royal Kitchens Entrance",
+                "An entrance to the Royal Kitchens."
+        );
+
+        var pantry = new Room(
+                "Pantry",
+                "A room with food and supplies for the Royal Kitchens."
+        );
+
+        var kitchensEnd = new Room(
+                "Kitchen's End",
+                "The Royal Kitchens end here."
+        );
+
+        var dungeonExit = new Room(
+                "Dungeon Exit",
+                "This is where it all ends. The exit from the Royal Dungeon, leading to the outside world.",
+                """
+                        As Schmorn exits the elevator, he isn't shocked to find Oglunda standing in the middle of the room, waiting for him, blocking the exit. She is holding a small, silver ring in her hand, the same one that Schmorn put on before he was transformed into a worm. The witch looks at him with a mixture of pity and disgust, and then she speaks.
+                    
+                        "And so, here you are, you determined failure of a man. You have made it this far, but you will not leave this place. Not as a worm, not as a prince, not as anything. I stand in your path, and I will not let you pass. You have made your choice, and now you must live with the consequences."
+                    
+                        Schmorn stares at her, fearful yet defiant. In his worm mouth, he holds the might Unbreakable Ladle, the most powerful weapon in his kingdom. He is ready to fight. Oglunda does not know what's coming.
+                        """
+        );
+
+        var royalWellCrossroadsPassage = new Passage("crack in the wall", true);
+        royalWellCrossroadsPassage.addRoom(royalWell);
+        royalWellCrossroadsPassage.addRoom(crossroads);
+
+        var crossroadsSewerEntrancePassage = new Passage("sewer hatch", true);
+        crossroadsSewerEntrancePassage.addRoom(crossroads);
+        crossroadsSewerEntrancePassage.addRoom(sewerEntrance);
+
+        var sewerEntranceSwordsmithRatsLairPassage = new Passage("sliding door", false);
+        sewerEntranceSwordsmithRatsLairPassage.addRoom(sewerEntrance);
+        sewerEntranceSwordsmithRatsLairPassage.addRoom(swordsmithRatsLair);
+        var sewerEntranceSwordsmithRatsLairPassageBlockage = new Blockage(
+                "The door can't be opened and does not have a keyhole.",
+                1
+        );
+        sewerEntranceSwordsmithRatsLairPassage.setBlockage(sewerEntranceSwordsmithRatsLairPassageBlockage);
+
+        var crossroadsPrisonEntrancePassage = new Passage("prison threshold", true);
+        crossroadsPrisonEntrancePassage.addRoom(crossroads);
+        crossroadsPrisonEntrancePassage.addRoom(prisonEntrance);
+
+        var prisonEntranceCellAPassage = new Passage("cell door A", false);
+        prisonEntranceCellAPassage.addRoom(prisonEntrance);
+        prisonEntranceCellAPassage.addRoom(cellA);
+
+        var cellACellBPassage = new Passage("cell door B", false);
+        cellACellBPassage.addRoom(cellA);
+        cellACellBPassage.addRoom(cellB);
+
+        var cellBGuardRoomPassage = new Passage("guard room door", true);
+        cellBGuardRoomPassage.addRoom(cellB);
+        cellBGuardRoomPassage.addRoom(guardRoom);
+
+        var cellBPrisonsEndPassage = new Passage("iron door", true);
+        cellBPrisonsEndPassage.addRoom(cellB);
+        cellBPrisonsEndPassage.addRoom(prisonsEnd);
+
+        var crossroadsDarkEntrancePassage = new Passage("dark passage", false);
+        crossroadsDarkEntrancePassage.addRoom(crossroads);
+        crossroadsDarkEntrancePassage.addRoom(darkEntrance);
+
+        var crossroadsDarkEntrancePassageBlockage = new Blockage(
+                "The door is locked. It has a large rusty keyhole, which is impossible to see through, as it is obscured by dark magic.",
+                1
+        );
+        crossroadsDarkEntrancePassage.setBlockage(crossroadsDarkEntrancePassageBlockage);
+
+        var darkEntranceBlackAltarPassage = new Passage("black door", false);
+        darkEntranceBlackAltarPassage.addRoom(darkEntrance);
+        darkEntranceBlackAltarPassage.addRoom(blackAltar);
+
+        var blackAltarShadowChamberPassage = new Passage("shadow curtain", false);
+        blackAltarShadowChamberPassage.addRoom(blackAltar);
+        blackAltarShadowChamberPassage.addRoom(shadowChamber);
+
+        var blackAltarPathwaysEndPassage = new Passage("rotten door", false);
+        blackAltarPathwaysEndPassage.addRoom(blackAltar);
+        blackAltarPathwaysEndPassage.addRoom(pathwaysEnd);
+
+        var crossroadsRoyalKitchensEntrancePassage = new Passage("kitchen door", true);
+        crossroadsRoyalKitchensEntrancePassage.addRoom(crossroads);
+        crossroadsRoyalKitchensEntrancePassage.addRoom(royalKitchensEntrance);
+
+        var crossroadsRoyalKitchensEntrancePassageBlockage = new Blockage(
+                "The door is locked by a small, delicate, yet resilient lock.",
+                1
+        );
+        crossroadsRoyalKitchensEntrancePassage.setBlockage(crossroadsRoyalKitchensEntrancePassageBlockage);
+
+        var royalKitchensEntrancePantryPassage = new Passage("pantry door", true);
+        royalKitchensEntrancePantryPassage.addRoom(royalKitchensEntrance);
+        royalKitchensEntrancePantryPassage.addRoom(pantry);
+
+        var pantryKitchensEndPassage = new Passage("backdoor", true);
+        pantryKitchensEndPassage.addRoom(pantry);
+        pantryKitchensEndPassage.addRoom(kitchensEnd);
+
+        var crossroadsDungeonExitPassage = new Passage(
+                "stone lift",
+                "a magical lift leading out of the dungeon",
+                false
+        );
+        crossroadsDungeonExitPassage.addRoom(crossroads);
+        crossroadsDungeonExitPassage.addRoom(dungeonExit);
+
+        var crossroadsDungeonExitPassageBlockage = new Blockage(
+                "The lift is dark, inactive and does not move. Inside, there is a magical stone tablet with three mysteriously shaped holes.",
+                3
+        );
+        crossroadsDungeonExitPassage.setBlockage(crossroadsDungeonExitPassageBlockage);
+
+
+        // Add game objects
+        var swordsmithRatsLairEntrance = new Button(
+                "steel button",
+                "an ornamented button made of slightly rusted steel",
+                "Schmorn presses the button and hears a distant scraping sound.",
+                sewerEntranceSwordsmithRatsLairPassageBlockage
+        );
+        prisonEntrance.addObject(swordsmithRatsLairEntrance);
+
+        var sword = new Weapon(
+                "ratsword",
+                "a steel sword, expertly crafted by the Swordsmith Rat",
+                "Schmorn attacks with the ratsword.",
+                10
+        );
+        swordsmithRatsLair.addObject(sword);
+
+        var darkKey = new InventoryItem(
+                "dark key",
+                "a huge, menacing key, made out of black iron",
+                "Schmorn used the key to unlock the dark passage.",
+                new Room[]{crossroads},
+                crossroadsDarkEntrancePassageBlockage
+        );
+        guardRoom.addObject(darkKey);
+
+        var kitchenKey = new InventoryItem(
+                "kitchen key",
+                "a small, golden, delicate key",
+                "Schmorn used the key to unlock the kitchen door.",
+                new Room[]{crossroads},
+                crossroadsRoyalKitchensEntrancePassageBlockage
+        );
+        shadowChamber.addObject(kitchenKey);
+
+        var frozenStone = new InventoryItem(
+                "frozen stone",
+                "a mysteriously shaped stone that is so cold it hurts to touch",
+                "Schmorn inserted the stone into the lift panel.",
+                new Room[]{crossroads},
+                crossroadsDungeonExitPassageBlockage
+        );
+        prisonsEnd.addObject(frozenStone);
+
+        var bloodStone = new InventoryItem(
+                "blood stone",
+                "a blood-red stone, always wet with fresh blood, which cannot be washed off",
+                "Schmorn inserted the stone into the lift panel.",
+                new Room[]{crossroads},
+                crossroadsDungeonExitPassageBlockage
+        );
+        pathwaysEnd.addObject(bloodStone);
+
+        var fireStone = new InventoryItem(
+                "fire stone",
+                "a mysteriously shaped stone that is so hot it's almost impossible to carry",
+                "Schmorn inserted the stone into the lift panel.",
+                new Room[]{crossroads},
+                crossroadsDungeonExitPassageBlockage
+        );
+        kitchensEnd.addObject(fireStone);
+
+        var unbreakableLadle = new Weapon(
+                "unbreakable ladle",
+                "a mighty weapon, the most prized possession of the Head Chef, stronger than any sword",
+                "Schmorn performs a powerful attack with the mighy Unbreakable Ladle.",
+                20
+        );
+        royalKitchensEntrance.addObject(unbreakableLadle);
+
+        // Add enemies
+        var swordsmithRat = new Enemy(
+                "Swordsmith Rat",
+                "A world-renowned swordsmith, locked up in the Royal Dungeon by prince Schmorn in hopes of getting him to craft powerful weapons for his armies.",
+                "The swordsmith does not protest or fight back, accepting his fate. Schmorn kills him with a single unarmed blow.",
+                0
+        );
+        swordsmithRatsLair.setEnemy(swordsmithRat);
+
+        var awakenedPrisoner = new Enemy(
+                "Awakened Prisoner",
+                "From this prisoner's appearance, it is clear that she is already dead. Nevertheless, she is moving and dangerous.",
+                5
+        );
+        cellA.setEnemy(awakenedPrisoner);
+
+        var jeremy = new Enemy(
+                "Jeremy",
+                "A rat lord who has once rallied all the rats by singing and dancing. Jealous of his charisma and influence, Schmorn has locked him up in his Royal Dungeon.",
+                5
+        );
+        cellB.setEnemy(jeremy);
+
+        var x = new Enemy(
+                "X",
+                "A terrifying, dark creature, not of this world. Despite the candlelight, only darkness can be seen where X is standing; it's impossible to make out any of its features. The creature is a guardian of the altar.",
+                "As the blade of the Ratsword sinks into the creature's body, it lets out a horrible screech. The sound is so loud that it shatters the candles surrounding the altar, and the darkness surrounding X dissipates. The creature's body evaporates into smoke, leaving nothing behind.",
+                5
+        );
+        blackAltar.setEnemy(x);
+
+        var headChef = new Enemy(
+                "Head Chef",
+                "The head chef of the Royal Kitchens. He does not approve of visitors.",
+                15
+        );
+        pantry.setEnemy(headChef);
+
+        var oglunda = new Enemy(
+                "Oglunda",
+                "The witch Oglunda, ruler of the Seven Swamps. She is the one who cursed Schmorn.",
+                """
+                        
+                        "It cannot be," the witch says, as Schmorn raises the Ladle. Her voice shakes as she realises the power of the weapon in his hands. "You have no right to wield it! You are not worthy!"
+                        
+                        Schmorn does not listen. He swings the Ladle with all his might, and the witch is thrown back, hitting the wall with a sickening thud. She falls to the ground, lifeless.""",
+                15,
+                true
+        );
+        dungeonExit.setEnemy(oglunda);
+
+        // Create game object
+        var game = new Game(
+                prologue,
+                epilogue,
+                unknownCommandMessages
+        );
+
+        game.addRoom(royalWell);
+        game.addRoom(crossroads);
+        game.addRoom(sewerEntrance);
+        game.addRoom(swordsmithRatsLair);
+        game.addRoom(prisonEntrance);
+        game.addRoom(cellA);
+        game.addRoom(cellB);
+        game.addRoom(guardRoom);
+        game.addRoom(prisonsEnd);
+        game.addRoom(darkEntrance);
+        game.addRoom(blackAltar);
+        game.addRoom(shadowChamber);
+        game.addRoom(pathwaysEnd);
+        game.addRoom(royalKitchensEntrance);
+        game.addRoom(pantry);
+        game.addRoom(kitchensEnd);
+        game.addRoom(dungeonExit);
+
+        game.setCurrentRoom(royalWell);
+
+        return game;
+    }
+}

+ 131 - 0
src/main/java/eu/oschl/textadventure/Game.java

@@ -0,0 +1,131 @@
+package eu.oschl.textadventure;
+
+import eu.oschl.textadventure.exceptions.InvalidGameState;
+import eu.oschl.textadventure.map.Passage;
+import eu.oschl.textadventure.map.Room;
+
+import java.util.ArrayList;
+import java.util.Optional;
+import java.util.Stack;
+
+/**
+ * Encapsulates the entire game, including its state, rooms, inventory, and control flow.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Game {
+    private final String prologue;
+    private final String epilogue;
+    private final String[] unknownCommandMessages;
+
+    private final Inventory inventory;
+    private final ArrayList<Room> rooms;
+    private Room currentRoom;
+    private final Stack<Passage> previousPassages;
+    private boolean runnning;
+
+    public Game(String prologue, String epilogue, String[] unknownCommandMessages) {
+        this.prologue = prologue;
+        this.epilogue = epilogue;
+        this.unknownCommandMessages = unknownCommandMessages;
+
+        this.inventory = new Inventory();
+        this.rooms = new ArrayList<>();
+        this.previousPassages = new Stack<>();
+        this.runnning = true;
+    }
+
+    public String getPrologue() {
+        return prologue;
+    }
+
+    public String getEpilogue() {
+        return epilogue;
+    }
+
+    public String[] getUnknownCommandMessages() {
+        return unknownCommandMessages;
+    }
+
+    public Inventory getInventory() {
+        return inventory;
+    }
+
+    public ArrayList<Room> getRooms() {
+        return rooms;
+    }
+
+    public Room getCurrentRoom() {
+        return currentRoom;
+    }
+
+    public Stack<Passage> getPreviousPassages() {
+        return previousPassages;
+    }
+
+    public boolean isRunning() {
+        return runnning;
+    }
+
+    /**
+     * Adds a room to the game.
+     *
+     * @param room the room to add
+     */
+    public void addRoom(Room room) {
+        room.setGame(this);
+        this.rooms.add(room);
+    }
+
+    /**
+     * Sets the current room.
+     *
+     * @param room the room to set as the current room
+     */
+    public void setCurrentRoom(Room room) {
+        room.setGame(this);
+        this.currentRoom = room;
+    }
+
+    /**
+     * Adds a previous passage to the stack of previous passages.
+     *
+     * @param previousPassage the passage to add
+     */
+    public void addPreviousPassage(Passage previousPassage) {
+        previousPassages.push(previousPassage);
+    }
+
+    /**
+     * Retrieves the last passage from the stack of previous passages.
+     *
+     * @return an Optional containing the last passage if it exists, or an empty Optional if the stack is empty
+     */
+    public Optional<Passage> getLastPassage() {
+        if (previousPassages.isEmpty()) {
+            return Optional.empty();
+        }
+
+        return Optional.ofNullable(previousPassages.peek());
+    }
+
+    /**
+     * Removes the last passage from the stack of previous passages.
+     *
+     * @throws InvalidGameState if the stack is empty
+     */
+    public void removeLastPassage() {
+        if (previousPassages.isEmpty()) {
+            throw new InvalidGameState("Attempted to remove last passage, but stack is empty");
+        }
+
+        previousPassages.pop();
+    }
+
+    /**
+     * Finishes the game by setting the running state to false.
+     */
+    public void finish() {
+        this.runnning = false;
+    }
+}

+ 71 - 0
src/main/java/eu/oschl/textadventure/Inventory.java

@@ -0,0 +1,71 @@
+package eu.oschl.textadventure;
+
+import eu.oschl.textadventure.objects.InventoryItem;
+import eu.oschl.textadventure.objects.Weapon;
+
+import java.util.ArrayList;
+import java.util.Optional;
+
+/**
+ * Represents the player's inventory in the game, which can hold a limited number of items and a weapon.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Inventory {
+    private static final int MAX_ITEMS = 5;
+
+    public final ArrayList<InventoryItem> items;
+    public Weapon weapon;
+
+    public Inventory() {
+        this.items = new ArrayList<>();
+        this.weapon = null;
+    }
+
+    public ArrayList<InventoryItem> getItems() {
+        return items;
+    }
+
+    public Optional<Weapon> getWeapon() {
+        return Optional.ofNullable(weapon);
+    }
+
+    /**
+     * Attempts to add an item to the inventory. If the inventory is full, the item cannot be added.
+     *
+     * @param item the item to add
+     * @return true if the item was successfully added, false if the inventory is full
+     */
+    public boolean addItem(InventoryItem item) {
+        if (items.size() >= MAX_ITEMS) {
+            return false;
+        }
+
+        items.add(item);
+        return true;
+    }
+
+    /**
+     * Attempts to remove an item from the inventory.
+     *
+     * @param item the item to remove
+     * @return true if the item was successfully removed, false if the item was not found in the inventory
+     */
+    public boolean removeItem(InventoryItem item) {
+        if (!items.contains(item)) {
+            return false;
+        }
+
+        items.remove(item);
+        return true;
+    }
+
+    /**
+     * Sets the weapon in the inventory. If a weapon is already set, it will be replaced.
+     *
+     * @param weapon the weapon to set
+     */
+    public void setWeapon(Weapon weapon) {
+        this.weapon = weapon;
+    }
+}

+ 82 - 0
src/main/java/eu/oschl/textadventure/entities/Enemy.java

@@ -0,0 +1,82 @@
+package eu.oschl.textadventure.entities;
+
+import java.util.Optional;
+
+/**
+ * Represents an enemy in the game.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Enemy extends Entity {
+    public final String killText;
+
+    public final int strength;
+    public final boolean finalBoss;
+    public boolean alive;
+
+    public Enemy(String name, String description, String killText, int strength) {
+        this(name, description, killText, strength, false);
+        alive = true;
+    }
+
+    public Enemy(String name, String description, String killText, int strength, boolean finalBoss) {
+        super(name, description);
+        this.killText = killText;
+        this.strength = strength;
+        this.finalBoss = finalBoss;
+        alive = true;
+    }
+
+    public Enemy(String name, String description, int strength) {
+        this(name, description, null, strength, false);
+        alive = true;
+    }
+
+    public Enemy(String name, String description, int strength, boolean finalBoss) {
+        this(name, description, null, strength, finalBoss);
+        alive = true;
+    }
+
+    public Optional<String> getKillText() {
+        return Optional.ofNullable(killText);
+    }
+
+    public int getStrength() {
+        return strength;
+    }
+
+    public boolean isFinalBoss() {
+        return finalBoss;
+    }
+
+    public boolean isAlive() {
+        return alive;
+    }
+
+    /**
+     * Kills the enemy if the player has enough strength to do so.
+     *
+     * @return true if the enemy was killed, false otherwise
+     */
+    public boolean kill() {
+        var weapon = game.getInventory().getWeapon();
+
+        if (weapon.isPresent()) {
+            if (strength > weapon.get().getDamage()) {
+                return false;
+            }
+        } else {
+            if (strength > 0) {
+                return false;
+            }
+        }
+
+        alive = false;
+
+        if (finalBoss) {
+            game.finish();
+        }
+
+        return true;
+    }
+}

+ 42 - 0
src/main/java/eu/oschl/textadventure/entities/Entity.java

@@ -0,0 +1,42 @@
+package eu.oschl.textadventure.entities;
+
+import eu.oschl.textadventure.Game;
+
+/**
+ * Represents a generic entity in the game.
+ * This class serves as a base for all entities, such as enemies.
+ *
+ * @author Ondřej Schlaichert
+ */
+public abstract class Entity {
+    protected Game game;
+
+    protected final String name;
+    protected final String description;
+
+    public Entity(String name, String description) {
+        this.name = name;
+        this.description = description;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    /**
+     * Sets the game instance. This is typically called during the game's setup phase.
+     *
+     * @param game the game instance to associate
+     */
+    public void setGame(Game game) {
+        if (game == null) {
+            return;
+        }
+
+        this.game = game;
+    }
+}

+ 24 - 0
src/main/java/eu/oschl/textadventure/exceptions/InvalidGameState.java

@@ -0,0 +1,24 @@
+package eu.oschl.textadventure.exceptions;
+
+/**
+ * Exception thrown when the game is in an invalid state.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class InvalidGameState extends RuntimeException {
+    public InvalidGameState() {
+        super();
+    }
+
+    public InvalidGameState(String message) {
+        super(message);
+    }
+
+    public InvalidGameState(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public InvalidGameState(Throwable cause) {
+        super(cause);
+    }
+}

+ 58 - 0
src/main/java/eu/oschl/textadventure/map/Blockage.java

@@ -0,0 +1,58 @@
+package eu.oschl.textadventure.map;
+
+import eu.oschl.textadventure.Game;
+
+/**
+ * Represents a blockage in the game that requires a certain number of interactions to pass.
+ * This could be a door, a puzzle, or any other obstacle that the player must overcome.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Blockage {
+    private Game game;
+
+    private final String description;
+    private int requiredInteractionsToPass;
+
+    public Blockage(String description, int requiredInteractionsToPass) {
+        this.description = description;
+        this.requiredInteractionsToPass = requiredInteractionsToPass;
+    }
+
+    /**
+     * Sets the game instance. This is typically called during the game's setup phase.
+     *
+     * @param game the game instance to associate
+     */
+    public void setGame(Game game) {
+        if (game == null) {
+            return;
+        }
+
+        this.game = game;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public int getRequiredInteractionsToPass() {
+        return requiredInteractionsToPass;
+    }
+
+    /**
+     * Decreases the number of required interactions to pass this blockage.
+     */
+    public void interact() {
+        this.requiredInteractionsToPass--;
+    }
+
+    /**
+     * Checks if the player can pass this blockage.
+     *
+     * @return true if the player can pass, false otherwise
+     */
+    public boolean canPass() {
+        return requiredInteractionsToPass <= 0;
+    }
+}

+ 162 - 0
src/main/java/eu/oschl/textadventure/map/Passage.java

@@ -0,0 +1,162 @@
+package eu.oschl.textadventure.map;
+
+import eu.oschl.textadventure.Game;
+import eu.oschl.textadventure.exceptions.InvalidGameState;
+
+import java.util.Optional;
+
+/**
+ * Represents a passage between two rooms in the game.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Passage {
+    private Game game;
+
+    private final String name;
+    private final String description;
+    private final boolean seeThrough;
+
+    private final Room[] rooms;
+    private Blockage blockage;
+
+    public Passage(String name, String description, boolean seeThrough) {
+        this.name = name;
+        this.description = description;
+        this.seeThrough = seeThrough;
+        this.rooms = new Room[2];
+    }
+
+    public Passage(String name, boolean seeThrough) {
+        this(name, null, seeThrough);
+    }
+
+    /**
+     * Sets the game instance. This is typically called during the game's setup phase.
+     *
+     * @param game the game instance to associate
+     */
+    public void setGame(Game game) {
+        if (game == null) {
+            return;
+        }
+
+        this.game = game;
+
+        if (blockage != null) {
+            blockage.setGame(game);
+        }
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Optional<String> getDescription() {
+        return Optional.ofNullable(description);
+    }
+
+    public boolean isSeeThrough() {
+        return seeThrough;
+    }
+
+    public Room[] getRooms() {
+        return rooms;
+    }
+
+    public Optional<Blockage> getBlockage() {
+        return Optional.ofNullable(blockage);
+    }
+
+    /**
+     * Adds a room to this passage. A passage can only have two rooms.
+     *
+     * @param room the room to add
+     * @throws InvalidGameState if the passage already has two rooms
+     */
+    public void addRoom(Room room) {
+        room.addPassage(this);
+        room.setGame(game);
+
+        for (Room r : rooms) {
+            if (r == room) {
+                return;
+            }
+        }
+
+        if (rooms[0] == null) {
+            rooms[0] = room;
+        } else if (rooms[1] == null) {
+            rooms[1] = room;
+        } else {
+            throw new InvalidGameState("Attempted to add third room to passage");
+        }
+    }
+
+    /**
+     * Attempts to pass through the passage to the other room.
+     *
+     * @param goingBack true if the player is going back through the passage they entered before, false otherwise
+     * @return true if the passage was successfully passed, false otherwise
+     */
+    public boolean passThrough(boolean goingBack) {
+        if (!canPass()) {
+            return false;
+        }
+
+        if (!goingBack) {
+            game.addPreviousPassage(this);
+        }
+
+        getOtherRoom(game.getCurrentRoom()).enter();
+
+        return true;
+    }
+
+    /**
+     * Sets the blockage for this passage.
+     *
+     * @param blockage the blockage to set
+     */
+    public void setBlockage(Blockage blockage) {
+        blockage.setGame(game);
+        this.blockage = blockage;
+    }
+
+    /**
+     * Checks if the player can pass through this passage.
+     *
+     * @return true if the player can pass, false otherwise
+     */
+    public boolean canPass() {
+        var room = this.game.getCurrentRoom();
+
+        if (!room.getPassages().contains(this)) {
+            return false;
+        }
+
+        var playerGoingBack = game.getLastPassage().isPresent() && game.getLastPassage().get() == this;
+        if (room.isBlockedByEnemy() && !playerGoingBack) {
+            return false;
+        }
+
+        return getBlockage().isEmpty() || getBlockage().get().canPass();
+    }
+
+    /**
+     * Gets the other room connected by this passage.
+     *
+     * @param room the current room
+     * @return the other room connected by this passage
+     * @throws InvalidGameState if the provided room is not one of the rooms connected by this passage
+     */
+    public Room getOtherRoom(Room room) {
+        if (rooms[0] == room) {
+            return rooms[1];
+        } else if (rooms[1] == room) {
+            return rooms[0];
+        } else {
+            throw new InvalidGameState("Neither room leading to accessed passage is the current room");
+        }
+    }
+}

+ 159 - 0
src/main/java/eu/oschl/textadventure/map/Room.java

@@ -0,0 +1,159 @@
+package eu.oschl.textadventure.map;
+
+import eu.oschl.textadventure.Game;
+import eu.oschl.textadventure.entities.Enemy;
+import eu.oschl.textadventure.objects.GameObject;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Optional;
+
+/**
+ * Represents a room in the game.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Room {
+    private Game game;
+
+    private final String name;
+    private final String description;
+    private final String enterText;
+
+    private final HashSet<Passage> passages;
+    private final ArrayList<GameObject> objects;
+    private Enemy enemy;
+
+    private boolean entered;
+
+    public Room(String name, String description, String enterText) {
+        this.name = name;
+        this.description = description;
+        this.enterText = enterText;
+        this.passages = new HashSet<>();
+        this.objects = new ArrayList<>();
+        this.entered = false;
+    }
+
+    public Room(String name, String description) {
+        this(name, description, null);
+    }
+
+    /**
+     * Sets the game instance. This is typically called during the game's setup phase.
+     *
+     * @param game the game instance to associate
+     */
+    public void setGame(Game game) {
+        if (game == null) {
+            return;
+        }
+
+        this.game = game;
+
+        for (Passage passage : passages) {
+            passage.setGame(game);
+        }
+
+        if (enemy != null) {
+            enemy.setGame(game);
+        }
+
+        for (GameObject object : objects) {
+            object.setGame(game);
+        }
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public Optional<String> getEnterText() {
+        return Optional.ofNullable(enterText);
+    }
+
+    public HashSet<Passage> getPassages() {
+        return passages;
+    }
+
+    public ArrayList<GameObject> getObjects() {
+        return objects;
+    }
+
+    public Optional<Enemy> getEnemy() {
+        return Optional.ofNullable(enemy);
+    }
+
+    public boolean wasEntered() {
+        return entered;
+    }
+
+    /**
+     * Checks if the room is blocked by an enemy.
+     * An enemy is considered to block the room if it is present and alive.
+     *
+     * @return true if the room is blocked by an enemy, false otherwise
+     */
+    public boolean isBlockedByEnemy() {
+        return getEnemy().isPresent() && getEnemy().get().isAlive();
+    }
+
+    /**
+     * Marks the room as entered and sets it as the current room in the game.
+     * This method should be called when the player enters the room.
+     *
+     * @return true if the room was successfully entered, false otherwise
+     */
+    public boolean enter() {
+        game.setCurrentRoom(this);
+        entered = true;
+
+        return true;
+    }
+
+    /**
+     * Adds a passage to this room. A room can have multiple passages.
+     *
+     * @param passage the passage to add
+     */
+    public void addPassage(Passage passage) {
+        passage.setGame(game);
+        passages.add(passage);
+    }
+
+    /**
+     * Sets an enemy for this room.
+     *
+     * @param enemy the enemy to set, or null to remove any existing enemy
+     */
+    public void setEnemy(Enemy enemy) {
+        if (enemy != null) {
+            enemy.setGame(game);
+        }
+
+        this.enemy = enemy;
+    }
+
+    /**
+     * Adds a game object to this room.
+     *
+     * @param object the object to add
+     */
+    public void addObject(GameObject object) {
+        object.setGame(game);
+        objects.add(object);
+    }
+
+    /**
+     * Removes a game object from this room.
+     *
+     * @param object the object to remove
+     */
+    public void removeObject(GameObject object) {
+        objects.remove(object);
+    }
+}

+ 48 - 0
src/main/java/eu/oschl/textadventure/objects/Button.java

@@ -0,0 +1,48 @@
+package eu.oschl.textadventure.objects;
+
+import eu.oschl.textadventure.map.Blockage;
+
+/**
+ * Represents a button in the game that can be pressed exactly once to interact with a blockage.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Button extends GameObject {
+    private final String pressText;
+    private final Blockage interactsWith;
+    private boolean pressed;
+
+    public Button(String name, String description, String pressText, Blockage interactsWith) {
+        super(name, description);
+        this.pressText = pressText;
+        this.interactsWith = interactsWith;
+        this.pressed = false;
+    }
+
+    public String getPressText() {
+        return pressText;
+    }
+
+    public Blockage getInteractsWith() {
+        return interactsWith;
+    }
+
+    public boolean isPressed() {
+        return pressed;
+    }
+
+    /**
+     * Attempts to press the button. If the button has already been pressed, it does nothing.
+     *
+     * @return true if the button was successfully pressed, false if it was already pressed
+     */
+    public boolean press() {
+        if (isPressed()) {
+            return false;
+        }
+
+        interactsWith.interact();
+        pressed = true;
+        return true;
+    }
+}

+ 42 - 0
src/main/java/eu/oschl/textadventure/objects/GameObject.java

@@ -0,0 +1,42 @@
+package eu.oschl.textadventure.objects;
+
+import eu.oschl.textadventure.Game;
+
+/**
+ * Represents a generic game object that can be part of the game world.
+ * This class serves as a base for all game objects, providing common properties and methods.
+ *
+ * @author Ondřej Schlaichert
+ */
+public abstract class GameObject {
+    protected Game game;
+
+    protected final String name;
+    protected final String description;
+
+    public GameObject(String name, String description) {
+        this.name = name;
+        this.description = description;
+    }
+
+    /**
+     * Sets the game instance. This is typically called during the game's setup phase.
+     *
+     * @param game the game instance to associate
+     */
+    public void setGame(Game game) {
+        if (game == null) {
+            return;
+        }
+
+        this.game = game;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+}

+ 71 - 0
src/main/java/eu/oschl/textadventure/objects/InventoryItem.java

@@ -0,0 +1,71 @@
+package eu.oschl.textadventure.objects;
+
+import eu.oschl.textadventure.map.Blockage;
+import eu.oschl.textadventure.map.Room;
+
+import java.util.Arrays;
+
+/**
+ * Represents an item in the player's inventory that can be picked up and used in specific rooms.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class InventoryItem extends PickableObject {
+    private final String useText;
+    private final Room[] canBeUsedIn;
+    private final Blockage interactsWith;
+
+    public InventoryItem(String name, String description, String useText, Room[] canBeUsedIn, Blockage interactsWith) {
+        super(name, description);
+        this.useText = useText;
+        this.canBeUsedIn = canBeUsedIn;
+        this.interactsWith = interactsWith;
+    }
+
+    public String getUseText() {
+        return useText;
+    }
+
+    public Room[] getCanBeUsedIn() {
+        return canBeUsedIn;
+    }
+
+    public Blockage getInteractsWith() {
+        return interactsWith;
+    }
+
+    /**
+     * Attempts to pick up the item. If the current room is blocked by an enemy, the item cannot be picked up.
+     *
+     * @return true if the item was successfully picked up, false otherwise
+     */
+    public boolean pickUp() {
+        if (game.getCurrentRoom().isBlockedByEnemy()) {
+            return false;
+        }
+
+        var result = game.getInventory().addItem(this);
+
+        if (result) {
+            game.getCurrentRoom().removeObject(this);
+        }
+
+        return result;
+    }
+
+    /**
+     * Attempts to use the item in the current room.
+     *
+     * @return true if the item was successfully used, false if it cannot be used in the current room
+     */
+    public boolean use() {
+        if (!Arrays.asList(canBeUsedIn).contains(game.getCurrentRoom())) {
+            return false;
+        }
+
+        interactsWith.interact();
+        game.getInventory().removeItem(this);
+
+        return true;
+    }
+}

+ 19 - 0
src/main/java/eu/oschl/textadventure/objects/PickableObject.java

@@ -0,0 +1,19 @@
+package eu.oschl.textadventure.objects;
+
+/**
+ * Represents an object in the game that can be picked up by the player.
+ * This class serves as a base for all pickable objects, providing common properties and methods.
+ *
+ * @author Ondřej Schlaichert
+ */
+public abstract class PickableObject extends GameObject {
+    public PickableObject(String name, String description) {
+        super(name, description);
+    }
+
+    /**
+     * Picks up the object and adds it to the player's inventory.
+     * @return true if the object was successfully picked up, false otherwise
+     */
+    public abstract boolean pickUp();
+}

+ 47 - 0
src/main/java/eu/oschl/textadventure/objects/Weapon.java

@@ -0,0 +1,47 @@
+package eu.oschl.textadventure.objects;
+
+/**
+ * Represents a weapon in the game that can be picked up and used to attack enemies.
+ *
+ * @author Ondřej Schlaichert
+ */
+public class Weapon extends PickableObject {
+    public final String attackText;
+    public final int damage;
+
+    public Weapon(String name, String description, String attackText, int damage) {
+        super(name, description);
+        this.attackText = attackText;
+        this.damage = damage;
+    }
+
+    public String getAttackText() {
+        return attackText;
+    }
+
+    public int getDamage() {
+        return damage;
+    }
+
+    /**
+     * Attempts to pick up the weapon. If the current room is blocked by an enemy or if the player already has a more
+     * powerful weapon, the weapon cannot be picked up.
+     *
+     * @return true if the weapon was successfully picked up, false otherwise
+     */
+    @Override
+    public boolean pickUp() {
+        if (game.getCurrentRoom().isBlockedByEnemy()) {
+            return false;
+        }
+
+        if (game.getInventory().getWeapon().isPresent() && game.getInventory().getWeapon().get().getDamage() > damage) {
+            return false;
+        }
+
+        game.getCurrentRoom().removeObject(this);
+        game.getInventory().setWeapon(this);
+
+        return true;
+    }
+}