David 1 месяц назад
Сommit
21ce4ef69c
94 измененных файлов с 6780 добавлено и 0 удалено
  1. 82 0
      dependency-reduced-pom.xml
  2. 316 0
      mvnw
  3. 188 0
      mvnw.cmd
  4. 83 0
      pom.xml
  5. 31 0
      src/main/java/cz/davza/studentadventure/Items/Bed.java
  6. 19 0
      src/main/java/cz/davza/studentadventure/Items/Book.java
  7. 116 0
      src/main/java/cz/davza/studentadventure/Items/Computer.java
  8. 35 0
      src/main/java/cz/davza/studentadventure/Items/EnergyDrink.java
  9. 51 0
      src/main/java/cz/davza/studentadventure/Items/Money.java
  10. 33 0
      src/main/java/cz/davza/studentadventure/Items/MouldyCheese.java
  11. 34 0
      src/main/java/cz/davza/studentadventure/Items/StickyNote.java
  12. 34 0
      src/main/java/cz/davza/studentadventure/annotations/CommandKeyword.java
  13. 71 0
      src/main/java/cz/davza/studentadventure/base/CommandBase.java
  14. 61 0
      src/main/java/cz/davza/studentadventure/base/ItemBase.java
  15. 103 0
      src/main/java/cz/davza/studentadventure/base/ItemCollectionBase.java
  16. 65 0
      src/main/java/cz/davza/studentadventure/base/ObservableBase.java
  17. 200 0
      src/main/java/cz/davza/studentadventure/base/RoomBase.java
  18. 32 0
      src/main/java/cz/davza/studentadventure/commands/AvailableCommandsCommand.java
  19. 51 0
      src/main/java/cz/davza/studentadventure/commands/BuyCommand.java
  20. 44 0
      src/main/java/cz/davza/studentadventure/commands/ConsumeCommand.java
  21. 26 0
      src/main/java/cz/davza/studentadventure/commands/DescribeCommand.java
  22. 70 0
      src/main/java/cz/davza/studentadventure/commands/DropCommand.java
  23. 54 0
      src/main/java/cz/davza/studentadventure/commands/HelpCommand.java
  24. 46 0
      src/main/java/cz/davza/studentadventure/commands/InteractCommand.java
  25. 30 0
      src/main/java/cz/davza/studentadventure/commands/InventoryCommand.java
  26. 64 0
      src/main/java/cz/davza/studentadventure/commands/PickupCommand.java
  27. 77 0
      src/main/java/cz/davza/studentadventure/commands/SleepCommand.java
  28. 23 0
      src/main/java/cz/davza/studentadventure/commands/StatsCommand.java
  29. 61 0
      src/main/java/cz/davza/studentadventure/commands/StealCommand.java
  30. 29 0
      src/main/java/cz/davza/studentadventure/commands/StudentIDCommand.java
  31. 106 0
      src/main/java/cz/davza/studentadventure/commands/StudyCommand.java
  32. 70 0
      src/main/java/cz/davza/studentadventure/commands/TakeExamCommand.java
  33. 29 0
      src/main/java/cz/davza/studentadventure/commands/TimeCommand.java
  34. 56 0
      src/main/java/cz/davza/studentadventure/commands/WalkCommand.java
  35. 53 0
      src/main/java/cz/davza/studentadventure/enums/CommandState.java
  36. 18 0
      src/main/java/cz/davza/studentadventure/enums/DeprivationState.java
  37. 55 0
      src/main/java/cz/davza/studentadventure/gameWrappers/TextGameWrapper.java
  38. 39 0
      src/main/java/cz/davza/studentadventure/interfaces/ICollectable.java
  39. 60 0
      src/main/java/cz/davza/studentadventure/interfaces/ICollection.java
  40. 32 0
      src/main/java/cz/davza/studentadventure/interfaces/ICommand.java
  41. 30 0
      src/main/java/cz/davza/studentadventure/interfaces/ICommandable.java
  42. 32 0
      src/main/java/cz/davza/studentadventure/interfaces/IConsumable.java
  43. 19 0
      src/main/java/cz/davza/studentadventure/interfaces/IGameWrapper.java
  44. 32 0
      src/main/java/cz/davza/studentadventure/interfaces/IInteractable.java
  45. 40 0
      src/main/java/cz/davza/studentadventure/interfaces/IItem.java
  46. 31 0
      src/main/java/cz/davza/studentadventure/interfaces/IObservable.java
  47. 19 0
      src/main/java/cz/davza/studentadventure/interfaces/IObserver.java
  48. 42 0
      src/main/java/cz/davza/studentadventure/interfaces/IOutputSink.java
  49. 19 0
      src/main/java/cz/davza/studentadventure/main/Main.java
  50. 99 0
      src/main/java/cz/davza/studentadventure/managers/CommandFactory.java
  51. 47 0
      src/main/java/cz/davza/studentadventure/managers/CommandManager.java
  52. 222 0
      src/main/java/cz/davza/studentadventure/objects/CommandResult.java
  53. 256 0
      src/main/java/cz/davza/studentadventure/objects/Game.java
  54. 58 0
      src/main/java/cz/davza/studentadventure/objects/GameData.java
  55. 125 0
      src/main/java/cz/davza/studentadventure/objects/GameMap.java
  56. 71 0
      src/main/java/cz/davza/studentadventure/objects/GameTime.java
  57. 74 0
      src/main/java/cz/davza/studentadventure/objects/ItemStack.java
  58. 127 0
      src/main/java/cz/davza/studentadventure/objects/Player.java
  59. 82 0
      src/main/java/cz/davza/studentadventure/objects/PlayerInventory.java
  60. 67 0
      src/main/java/cz/davza/studentadventure/objects/PlayerStat.java
  61. 24 0
      src/main/java/cz/davza/studentadventure/objects/RoomItems.java
  62. 56 0
      src/main/java/cz/davza/studentadventure/objects/Wallet.java
  63. 32 0
      src/main/java/cz/davza/studentadventure/outputsinks/TextOutputSink.java
  64. 33 0
      src/main/java/cz/davza/studentadventure/rooms/BedroomRoom.java
  65. 53 0
      src/main/java/cz/davza/studentadventure/rooms/ClassroomRoom.java
  66. 33 0
      src/main/java/cz/davza/studentadventure/rooms/ComputerLabRoom.java
  67. 66 0
      src/main/java/cz/davza/studentadventure/rooms/ExamRoomRoom.java
  68. 24 0
      src/main/java/cz/davza/studentadventure/rooms/HallwayRoom.java
  69. 23 0
      src/main/java/cz/davza/studentadventure/rooms/ShopRoom.java
  70. 29 0
      src/main/java/cz/davza/studentadventure/rooms/StudyRoomRoom.java
  71. 104 0
      src/main/resources/cz/davza/studentadventure/main/help.html
  72. 11 0
      src/main/resources/simplelogger.properties
  73. 52 0
      test/cz/davza/studentadventure/TestHelpers.java
  74. 85 0
      test/cz/davza/studentadventure/commands/BuyCommandTest.java
  75. 90 0
      test/cz/davza/studentadventure/commands/ConsumeCommandTest.java
  76. 93 0
      test/cz/davza/studentadventure/commands/DropCommandTest.java
  77. 60 0
      test/cz/davza/studentadventure/commands/InteractCommandTest.java
  78. 90 0
      test/cz/davza/studentadventure/commands/PickupCommandTest.java
  79. 124 0
      test/cz/davza/studentadventure/commands/SleepCommandTest.java
  80. 96 0
      test/cz/davza/studentadventure/commands/StealCommandTest.java
  81. 133 0
      test/cz/davza/studentadventure/commands/StudyCommandTest.java
  82. 98 0
      test/cz/davza/studentadventure/commands/TakeExamCommandTest.java
  83. 100 0
      test/cz/davza/studentadventure/commands/WalkCommandTest.java
  84. 147 0
      test/cz/davza/studentadventure/integration/GameIntegrationTest.java
  85. 103 0
      test/cz/davza/studentadventure/items/ComputerTest.java
  86. 84 0
      test/cz/davza/studentadventure/objects/GameTimeTest.java
  87. 170 0
      test/cz/davza/studentadventure/objects/ItemCollectionBaseTest.java
  88. 157 0
      test/cz/davza/studentadventure/objects/ItemCollectionTest.java
  89. 66 0
      test/cz/davza/studentadventure/objects/PlayerStatTest.java
  90. 101 0
      test/cz/davza/studentadventure/objects/PlayerTest.java
  91. 55 0
      test/cz/davza/studentadventure/objects/WalletTest.java
  92. 71 0
      test/cz/davza/studentadventure/rooms/ClassroomRoomTest.java
  93. 95 0
      test/cz/davza/studentadventure/rooms/ExamRoomRoomTest.java
  94. 83 0
      test/cz/davza/studentadventure/rooms/ExamRoomTest.java

+ 82 - 0
dependency-reduced-pom.xml

@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>cz.davza</groupId>
+  <artifactId>StudentAdventure</artifactId>
+  <name>StudentAdventure</name>
+  <version>1.1.0</version>
+  <build>
+    <testSourceDirectory>${project.basedir}/test</testSourceDirectory>
+    <plugins>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.13.0</version>
+        <configuration>
+          <source>21</source>
+          <target>21</target>
+        </configuration>
+      </plugin>
+      <plugin>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>3.6.0</version>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <transformers>
+                <transformer>
+                  <mainClass>cz.davza.studentadventure.main.Main</mainClass>
+                </transformer>
+              </transformers>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-api</artifactId>
+      <version>5.12.1</version>
+      <scope>test</scope>
+      <exclusions>
+        <exclusion>
+          <artifactId>opentest4j</artifactId>
+          <groupId>org.opentest4j</groupId>
+        </exclusion>
+        <exclusion>
+          <artifactId>junit-platform-commons</artifactId>
+          <groupId>org.junit.platform</groupId>
+        </exclusion>
+        <exclusion>
+          <artifactId>apiguardian-api</artifactId>
+          <groupId>org.apiguardian</groupId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-engine</artifactId>
+      <version>5.12.1</version>
+      <scope>test</scope>
+      <exclusions>
+        <exclusion>
+          <artifactId>junit-platform-engine</artifactId>
+          <groupId>org.junit.platform</groupId>
+        </exclusion>
+        <exclusion>
+          <artifactId>apiguardian-api</artifactId>
+          <groupId>org.apiguardian</groupId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+  </dependencies>
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <junit.version>5.12.1</junit.version>
+  </properties>
+</project>

+ 316 - 0
mvnw

@@ -0,0 +1,316 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+#   JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+#   M2_HOME - location of maven2's installed home dir
+#   MAVEN_OPTS - parameters passed to the Java VM when running Maven
+#     e.g. to debug Maven itself, use
+#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+  if [ -f /usr/local/etc/mavenrc ] ; then
+    . /usr/local/etc/mavenrc
+  fi
+
+  if [ -f /etc/mavenrc ] ; then
+    . /etc/mavenrc
+  fi
+
+  if [ -f "$HOME/.mavenrc" ] ; then
+    . "$HOME/.mavenrc"
+  fi
+
+fi
+
+# OS specific support.  $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+  CYGWIN*) cygwin=true ;;
+  MINGW*) mingw=true;;
+  Darwin*) darwin=true
+    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+    if [ -z "$JAVA_HOME" ]; then
+      if [ -x "/usr/libexec/java_home" ]; then
+        export JAVA_HOME="`/usr/libexec/java_home`"
+      else
+        export JAVA_HOME="/Library/Java/Home"
+      fi
+    fi
+    ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+  if [ -r /etc/gentoo-release ] ; then
+    JAVA_HOME=`java-config --jre-home`
+  fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+  ## resolve links - $0 may be a link to maven's home
+  PRG="$0"
+
+  # need this for relative symlinks
+  while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+      PRG="$link"
+    else
+      PRG="`dirname "$PRG"`/$link"
+    fi
+  done
+
+  saveddir=`pwd`
+
+  M2_HOME=`dirname "$PRG"`/..
+
+  # make it fully qualified
+  M2_HOME=`cd "$M2_HOME" && pwd`
+
+  cd "$saveddir"
+  # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --unix "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME="`(cd "$M2_HOME"; pwd)`"
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+  javaExecutable="`which javac`"
+  if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+    # readlink(1) is not available as standard on Solaris 10.
+    readLink=`which readlink`
+    if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+      if $darwin ; then
+        javaHome="`dirname \"$javaExecutable\"`"
+        javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+      else
+        javaExecutable="`readlink -f \"$javaExecutable\"`"
+      fi
+      javaHome="`dirname \"$javaExecutable\"`"
+      javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+      JAVA_HOME="$javaHome"
+      export JAVA_HOME
+    fi
+  fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+  if [ -n "$JAVA_HOME"  ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+      # IBM's JDK on AIX uses strange locations for the executables
+      JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+    fi
+  else
+    JAVACMD="`\\unset -f command; \\command -v java`"
+  fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+  echo "Error: JAVA_HOME is not defined correctly." >&2
+  echo "  We cannot execute $JAVACMD" >&2
+  exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+  echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+  if [ -z "$1" ]
+  then
+    echo "Path not specified to find_maven_basedir"
+    return 1
+  fi
+
+  basedir="$1"
+  wdir="$1"
+  while [ "$wdir" != '/' ] ; do
+    if [ -d "$wdir"/.mvn ] ; then
+      basedir=$wdir
+      break
+    fi
+    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+    if [ -d "${wdir}" ]; then
+      wdir=`cd "$wdir/.."; pwd`
+    fi
+    # end of workaround
+  done
+  echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+  if [ -f "$1" ]; then
+    echo "$(tr -s '\n' ' ' < "$1")"
+  fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+  exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Found .mvn/wrapper/maven-wrapper.jar"
+    fi
+else
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+    fi
+    if [ -n "$MVNW_REPOURL" ]; then
+      jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+    else
+      jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+    fi
+    while IFS="=" read key value; do
+      case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+      esac
+    done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Downloading from: $jarUrl"
+    fi
+    wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+    if $cygwin; then
+      wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+    fi
+
+    if command -v wget > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found wget ... using wget"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+        else
+            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+        fi
+    elif command -v curl > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found curl ... using curl"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            curl -o "$wrapperJarPath" "$jarUrl" -f
+        else
+            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+        fi
+
+    else
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Falling back to using Java to download"
+        fi
+        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        # For Cygwin, switch paths to Windows format before running javac
+        if $cygwin; then
+          javaClass=`cygpath --path --windows "$javaClass"`
+        fi
+        if [ -e "$javaClass" ]; then
+            if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Compiling MavenWrapperDownloader.java ..."
+                fi
+                # Compiling the Java class
+                ("$JAVA_HOME/bin/javac" "$javaClass")
+            fi
+            if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                # Running the downloader
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Running MavenWrapperDownloader.java ..."
+                fi
+                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+            fi
+        fi
+    fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+  echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --path --windows "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+  [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+    MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+  $MAVEN_OPTS \
+  $MAVEN_DEBUG_OPTS \
+  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+  "-Dmaven.home=${M2_HOME}" \
+  "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

+ 188 - 0
mvnw.cmd

@@ -0,0 +1,188 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM     e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+    IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Found %WRAPPER_JAR%
+    )
+) else (
+    if not "%MVNW_REPOURL%" == "" (
+        SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+    )
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Couldn't find %WRAPPER_JAR%, downloading it ...
+        echo Downloading from: %DOWNLOAD_URL%
+    )
+
+    powershell -Command "&{"^
+		"$webclient = new-object System.Net.WebClient;"^
+		"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+		"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+		"}"^
+		"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+		"}"
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Finished downloading %WRAPPER_JAR%
+    )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+  %JVM_CONFIG_MAVEN_PROPS% ^
+  %MAVEN_OPTS% ^
+  %MAVEN_DEBUG_OPTS% ^
+  -classpath %WRAPPER_JAR% ^
+  "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%

+ 83 - 0
pom.xml

@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>cz.davza</groupId>
+    <artifactId>StudentAdventure</artifactId>
+    <version>1.1.0</version>
+    <name>StudentAdventure</name>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <junit.version>5.12.1</junit.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.reflections</groupId>
+            <artifactId>reflections</artifactId>
+            <version>0.10.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>2.0.12</version>
+<!--            <scope>test</scope>-->
+        </dependency>
+<!--        Adding the api package to rewrite what reflections package uses and upgrade to newest version-->
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>2.0.12</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <testSourceDirectory>${project.basedir}/test</testSourceDirectory>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.13.0</version>
+                <configuration>
+                    <source>21</source>
+                    <target>21</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>3.6.0</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <transformers>
+                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                                    <mainClass>cz.davza.studentadventure.main.Main</mainClass>
+                                </transformer>
+                            </transformers>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 31 - 0
src/main/java/cz/davza/studentadventure/Items/Bed.java

@@ -0,0 +1,31 @@
+package cz.davza.studentadventure.Items;
+
+import cz.davza.studentadventure.base.ItemBase;
+import cz.davza.studentadventure.commands.SleepCommand;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.interfaces.IInteractable;
+/**
+ * An interactable bed in the player's bedroom.
+ *
+ * <p>Interacting with the bed is equivalent to using the {@code sleep} command
+ * and will fully restore the player's stats and advance time to the next morning.</p>
+ *
+ * @see cz.davza.studentadventure.commands.SleepCommand
+ */
+public class Bed extends ItemBase implements IInteractable {
+    /** Creates a new Bed instance. */
+    public Bed() {
+        super("Bed", "A single bed with crumpled sheets. Very inviting.", "bed", 1);
+    }
+    /**
+     * {@inheritDoc}
+     * Delegates directly to {@link SleepCommand#Execute(GameData)}.
+     */
+    @Override
+    public CommandResult Interact(GameData context) {
+
+        return new SleepCommand().Execute(context);
+
+    }
+}

+ 19 - 0
src/main/java/cz/davza/studentadventure/Items/Book.java

@@ -0,0 +1,19 @@
+package cz.davza.studentadventure.Items;
+
+import cz.davza.studentadventure.base.ItemBase;
+import cz.davza.studentadventure.interfaces.ICollectable;
+/**
+ * A programming textbook that can be picked up and carried by the player.
+ *
+ * <p>Required to enter the {@link cz.davza.studentadventure.rooms.ClassroomRoom}.
+ * Stacks up to 4 per inventory slot.</p>
+ *
+ * @see cz.davza.studentadventure.rooms.ClassroomRoom#onEnter
+ */
+public class Book extends ItemBase implements ICollectable {
+    /** Creates a new Book instance. */
+    public Book() {
+        super("Book", "A hefty programming textbook.", "book", 3);
+    }
+
+}

+ 116 - 0
src/main/java/cz/davza/studentadventure/Items/Computer.java

@@ -0,0 +1,116 @@
+package cz.davza.studentadventure.Items;
+
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.base.ItemBase;
+import cz.davza.studentadventure.interfaces.ICommandable;
+import cz.davza.studentadventure.interfaces.IInteractable;
+import cz.davza.studentadventure.managers.CommandManager;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+
+import java.util.List;
+/**
+ * A password-protected university workstation that hosts its own sub-command loop.
+ *
+ * <p>Interacting with the computer for the first time triggers a password prompt.
+ * Once the correct password is entered the computer remains unlocked for the rest
+ * of the session. Inside the active session the player can:</p>
+ * <ul>
+ *   <li>{@code study <hours>} — study using the computer.</li>
+ *   <li>{@code studentid} — look up their student ID.</li>
+ *   <li>{@code exit} — step away and return to normal room commands.</li>
+ * </ul>
+ *
+ * <p>The computer implements {@link ICommandable} so the game loop can route
+ * subsequent player input directly to {@link #handleCommand} until the player
+ * types {@code exit}.</p>
+ *
+ * @see cz.davza.studentadventure.rooms.ComputerLabRoom
+ * @see IInteractable
+ * @see ICommandable
+ */
+public class Computer extends ItemBase implements IInteractable, ICommandable {
+
+    private enum ComputerState { PASSWORD, ACTIVE }
+    private ComputerState computerState = ComputerState.PASSWORD;
+    private final List<String> allowedCommands = List.of("study", "exit", "studentid");
+    private final String password;
+    /**
+     * Creates a computer locked with the given password.
+     *
+     * @param password the password the player must enter to unlock the workstation
+     */
+    public Computer(String password) {
+        super("Computer", "A university workstation.", "computer", 1);
+        this.password = password;
+    }
+    /**
+     * {@inheritDoc}
+     *
+     * <p>If the computer is locked, returns an {@link CommandResult#actionPending} result
+     * with a password prompt. If already unlocked, returns an {@code actionPending}
+     * result with a welcome message, routing further input to this computer.</p>
+     */
+    @Override
+    public CommandResult Interact(GameData context) {
+        if (computerState == ComputerState.PASSWORD) {
+            return CommandResult.actionPending(this, "The computer is locked.\nEnter password (or 'exit' to walk away):");
+        }
+        computerState = ComputerState.ACTIVE;
+        return CommandResult.actionPending(this, "You sit down at the computer. (write 'exit' to walk away)");
+    }
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Routes input to either the password handler or the active session handler
+     * depending on the current {@link ComputerState}.</p>
+     */
+    @Override
+    public CommandResult handleCommand(String input, GameData context) {
+        return switch (computerState) {
+            case PASSWORD -> handlePassword(input);
+            case ACTIVE -> handleActive(input, context);
+        };
+    }
+    /**
+     * Handles input while the computer is in the password-prompt state.
+     *
+     * <p>Typing {@code exit} cancels and returns control to the room.
+     * The correct password unlocks the computer and transitions to the active state.
+     * Any other input returns a failure with a retry prompt.</p>
+     *
+     * @param input the raw input string from the player
+     * @return the result of the password attempt
+     */
+    private CommandResult handlePassword(String input) {
+        if (input.equalsIgnoreCase("exit")) {
+            return CommandResult.actionResumed("You step away from the computer.");
+        }
+        if (input.equals(password)) {
+            computerState = ComputerState.ACTIVE;
+            return CommandResult.success("Password accepted. The computer unlocks.\nYou sit down at the computer. (write 'exit' to walk away)");
+        }
+        return CommandResult.failed("Incorrect password. Try again.");
+    }
+    /**
+     * Handles input while the computer is unlocked and in use.
+     *
+     * <p>Typing {@code exit} ends the session and returns control to the room.
+     * Other input is parsed against the computer's restricted command list
+     * ({@code study}, {@code studentid}). Unrecognised commands return a failure.</p>
+     *
+     * @param input   the raw input string from the player
+     * @param context the current game state passed to commands that need it
+     * @return the result of the command or a failure if the input is unrecognised
+     */
+    private CommandResult handleActive(String input, GameData context) {
+        if (input.equalsIgnoreCase("exit")) {
+            return CommandResult.actionResumed("You step away from the computer.");
+        }
+        CommandBase command = CommandManager.parseCommand(input, allowedCommands);
+        if (command == null) {
+            return CommandResult.failed("You can't do that here.");
+        }
+        return command.Execute(context);
+    }
+}

+ 35 - 0
src/main/java/cz/davza/studentadventure/Items/EnergyDrink.java

@@ -0,0 +1,35 @@
+package cz.davza.studentadventure.Items;
+
+import cz.davza.studentadventure.base.ItemBase;
+import cz.davza.studentadventure.interfaces.IConsumable;
+import cz.davza.studentadventure.objects.Player;
+/**
+ * A canned energy drink sold at the campus shop.
+ *
+ * <p>Consuming one restores {@value #ENERGY_RESTORE} energy to the player.
+ * Costs {@value #COST} coins and stacks up to 5 per inventory slot.</p>
+ *
+ * @see cz.davza.studentadventure.commands.BuyCommand
+ * @see cz.davza.studentadventure.commands.ConsumeCommand
+ */
+public class EnergyDrink extends ItemBase implements IConsumable {
+    /** The purchase price of one energy drink in coins. */
+    public static final int COST           = 20;
+    /** The amount of energy restored when consumed. */
+    public static final int ENERGY_RESTORE = 40;
+    /** Creates a new EnergyDrink instance. */
+    public EnergyDrink() {
+        super("Energy Drink", "A cold can of something that smells like chemicals.",
+                "energy_drink", 2);
+    }
+    /** {@inheritDoc} Restores {@value #ENERGY_RESTORE} energy. */
+    @Override
+    public void consume(Player player) {
+        player.getEnergy().increase(ENERGY_RESTORE);
+    }
+    /** {@inheritDoc} */
+    @Override
+    public String getConsumeMessage() {
+        return "You crack open the energy drink and skull it. Energy +" + ENERGY_RESTORE + ".";
+    }
+}

+ 51 - 0
src/main/java/cz/davza/studentadventure/Items/Money.java

@@ -0,0 +1,51 @@
+package cz.davza.studentadventure.Items;
+
+import cz.davza.studentadventure.base.ItemBase;
+import cz.davza.studentadventure.interfaces.ICollectable;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+/**
+ * Loose coins found on the ground.
+ *
+ * <p>Unlike most collectables, picking up {@code Money} does not add it to the
+ * inventory. Instead, {@link #onPickup} directly adds the coin value to the player's
+ * balance and prints a confirmation message.</p>
+ *
+ * <p>Can be constructed with a fixed amount or a random amount within a range.</p>
+ */
+public class Money extends ItemBase implements ICollectable {
+    private final int amount;
+
+    /**
+     * Creates a money item with a fixed coin value.
+     *
+     * @param amount the exact number of coins this item represents
+     */
+    public Money(int amount) {
+        super("Money", amount + " coins on the ground.", "money", 1);
+        this.amount = amount;
+    }
+
+    /**
+     * Creates a money item with a randomly determined coin value within the given range.
+     *
+     * @param min the minimum number of coins (inclusive)
+     * @param max the maximum number of coins (inclusive)
+     */
+    public Money(int min, int max) {
+        super("Money", "Some loose change on the ground.", "money", 1);
+        this.amount = min + (int)(Math.random() * (max - min + 1));
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Overridden to add coins directly to the player's balance rather than
+     * occupying an inventory slot. Always returns {@code 0} (never fails).</p>
+     */
+    @Override
+    public CommandResult onPickup(GameData context, int amount){
+        context.getPlayer().getWallet().earnMoney(this.amount);
+        return CommandResult.success("Picked up " + this.amount + " " + this.getCode() + ".\n");
+    }
+}

+ 33 - 0
src/main/java/cz/davza/studentadventure/Items/MouldyCheese.java

@@ -0,0 +1,33 @@
+package cz.davza.studentadventure.Items;
+
+import cz.davza.studentadventure.base.ItemBase;
+import cz.davza.studentadventure.interfaces.IConsumable;
+import cz.davza.studentadventure.objects.Player;
+/**
+ * A forgotten block of mouldy cheese.
+ *
+ * <p>Consuming it is inadvisable — it drains {@value #ENERGY_COST} energy from
+ * the player. Stacks up to 5 per slot.</p>
+ */
+public class MouldyCheese extends ItemBase implements IConsumable {
+    public static final int COST           = 20;
+    /** The energy cost inflicted on the player when consumed. */
+    public static final int ENERGY_COST = 15;
+    /** Creates a new MouldyCheese instance. */
+    public MouldyCheese() {
+        super("Mouldy Cheese", "A block of mouldy cheese someone forgot to stick in the fridge.",
+                "mouldy_cheese", 5);
+    }
+    /** {@inheritDoc} Drains {@value #ENERGY_COST} energy. */
+
+    @Override
+    public void consume(Player player) {
+        player.getEnergy().decrease(ENERGY_COST);
+    }
+    /** {@inheritDoc} */
+
+    @Override
+    public String getConsumeMessage() {
+        return "You bite into the block of cheese and start feeling unwell. Energy -" + ENERGY_COST + ".";
+    }
+}

+ 34 - 0
src/main/java/cz/davza/studentadventure/Items/StickyNote.java

@@ -0,0 +1,34 @@
+package cz.davza.studentadventure.Items;
+
+import cz.davza.studentadventure.base.ItemBase;
+import cz.davza.studentadventure.interfaces.IInteractable;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+/**
+ * A sticky note left in a room, containing a readable message.
+ *
+ * <p>Interacting with the note prints its contents. Notes are not collectable
+ * and stay in the room permanently. Max stack of 1 (each note is unique).</p>
+ *
+ * <p>Used throughout the world to hint at passwords, lore, and jokes.</p>
+ */
+public class StickyNote extends ItemBase implements IInteractable {
+    private final String note;
+    /**
+     * Creates a sticky note with the given message text.
+     *
+     * @param note the text written on the note
+     */
+    public StickyNote(String note) {
+        super("Sticky Note", "A sticky note with scribbled text. You may be able to just read it", "sticky_note", 1);
+        this.note = note;
+    }
+    /**
+     * {@inheritDoc}
+     * Prints the note's message to {@code System.out}.
+     */
+    @Override
+    public CommandResult Interact(GameData context) {
+        return CommandResult.success("You lean in to read the note. \n It reads: \n " + note + "\n");
+    }
+}

+ 34 - 0
src/main/java/cz/davza/studentadventure/annotations/CommandKeyword.java

@@ -0,0 +1,34 @@
+package cz.davza.studentadventure.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+/**
+ * Marks a class as a player command and associates it with a keyword string.
+ *
+ * <p>The {@link cz.davza.studentadventure.managers.CommandFactory} scans for all classes
+ * annotated with {@code @CommandKeyword} at runtime and registers them in its
+ * command registry. When a player types the matching keyword, the annotated class
+ * is instantiated and executed.</p>
+ *
+ * <p>Usage example:</p>
+ * <pre>{@code
+ * @CommandKeyword("walk")
+ * public class WalkCommand extends CommandBase { ... }
+ * }</pre>
+ *
+ * @see cz.davza.studentadventure.managers.CommandFactory
+ * @see cz.davza.studentadventure.base.CommandBase
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface CommandKeyword {
+    /**
+     * The keyword the player must type to trigger this command.
+     * This is matched case-insensitively by the {@link cz.davza.studentadventure.managers.CommandManager}.
+     *
+     * @return the command keyword string (e.g. {@code "walk"}, {@code "study"})
+     */
+    String value();
+}

+ 71 - 0
src/main/java/cz/davza/studentadventure/base/CommandBase.java

@@ -0,0 +1,71 @@
+package cz.davza.studentadventure.base;
+
+import cz.davza.studentadventure.interfaces.ICommand;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import java.util.List;
+
+// DU Z 12. CVIČENÍ - Dědičnost - Základní třída pro veškeré příkazy které z této dědí a přepisují funkci Execute
+
+
+
+/**
+ * Abstract base class for all player commands.
+ *
+ * <p>Provides shared parameter handling that all commands rely on. When the
+ * {@link cz.davza.studentadventure.managers.CommandFactory} instantiates a command, it
+ * calls {@link #setParams(List)} with the arguments the player typed
+ * after the keyword.</p>
+ *
+ * <p>Concrete subclasses must be annotated with
+ * {@link cz.davza.studentadventure.annotations.CommandKeyword} so the factory can
+ * discover and register them. They must implement {@link #Execute(GameData)} to
+ * define their behaviour.</p>
+ *
+ * <p>Example — a command typed as {@code "pickup book 2"} will have params
+ * {@code ["book", "2"]} when {@code Execute} is called.</p>
+ *
+ * @see cz.davza.studentadventure.interfaces.ICommand
+ * @see cz.davza.studentadventure.managers.CommandFactory
+ */
+public abstract class CommandBase implements ICommand {
+    private List<String> params;
+    /**
+     * Sets the parameter list parsed from the player's input.
+     * Called by {@link cz.davza.studentadventure.managers.CommandFactory} before {@link #Execute}.
+     *
+     * @param params the list of argument strings following the command keyword
+     */
+    public void setParams(List<String> params) {
+        this.params = params;
+    }
+    /**
+     * Returns the parameter list for this command invocation.
+     *
+     * @return the parameter list, or {@code null} if params have not been set
+     */
+    public List<String> getParams() {
+        return params;
+    }
+
+    /**
+     * Checks whether at least {@code minimum} parameters were supplied.
+     *
+     * <p>Commands should call this at the start of {@link #Execute} to guard
+     * against missing arguments, returning {@link CommandState#INVALID_PARAMS}
+     * if the check fails.</p>
+     *
+     * @param minimum the minimum number of parameters required
+     * @return {@code true} if the param list is non-null and has at least {@code minimum} entries
+     */
+    protected boolean hasParams(int minimum) {
+        return params != null && params.size() >= minimum;
+    }
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public abstract CommandResult Execute(GameData context);
+
+}

+ 61 - 0
src/main/java/cz/davza/studentadventure/base/ItemBase.java

@@ -0,0 +1,61 @@
+package cz.davza.studentadventure.base;
+
+import cz.davza.studentadventure.interfaces.IItem;
+
+// DU Z 12. CVIČENÍ - Dědičnost - Základní třída pro veškeré věci (Items), které ve hře figurují
+
+
+/**
+ * Abstract base class for all items in the game.
+ *
+ * <p>{@code ItemBase} provides the core identity fields every item needs —
+ * a display name, a description, a unique code, and a max stack size.
+ * Concrete item classes extend this and additionally implement one or more
+ * item behaviour interfaces ({@link cz.davza.studentadventure.interfaces.ICollectable},
+ * {@link cz.davza.studentadventure.interfaces.IConsumable},
+ * {@link cz.davza.studentadventure.interfaces.IInteractable}).</p>
+ *
+ * <p>The {@code code} field (e.g. {@code "book"}, {@code "energy_drink"}) is the
+ * primary key used by commands and room lookups to identify items, so it must
+ * be unique per item type.</p>
+ *
+ * @see cz.davza.studentadventure.interfaces.IItem
+ */
+public abstract class ItemBase implements IItem {
+    private final String name;
+    private final String description;
+    private final String code;
+    private final int bulk;
+    /**
+     * Constructs a new item with the given identity properties.
+     *
+     * @param name        the human-readable display name (e.g. {@code "Energy Drink"})
+     * @param description a short description shown when the player examines the item
+     * @param code        the unique identifier used in commands (e.g. {@code "energy_drink"})
+     * @param bulk   the maximum number of this item that can occupy a single inventory slot
+     */
+    public ItemBase(String name, String description, String code, int bulk) {
+        this.name = name;
+        this.description = description;
+        this.code = code;
+        this.bulk = bulk;
+    }
+    /** {@inheritDoc} */
+    @Override
+    public String getCode() { return code; }
+    /** {@inheritDoc} */
+    @Override
+    public int getBulk() { return bulk; }
+    /**
+     * Returns the human-readable display name of this item.
+     *
+     * @return the item's name
+     */
+    public String getName() { return name; }
+    /**
+     * Returns a short description of this item.
+     *
+     * @return the item description
+     */
+    public String getDescription() { return description; }
+}

+ 103 - 0
src/main/java/cz/davza/studentadventure/base/ItemCollectionBase.java

@@ -0,0 +1,103 @@
+package cz.davza.studentadventure.base;
+
+import cz.davza.studentadventure.interfaces.ICollection;
+import cz.davza.studentadventure.interfaces.IItem;
+import cz.davza.studentadventure.objects.ItemStack;
+
+import java.util.*;
+
+/**
+ * Abstract base implementation of a fixed-size, slot-based item collection.
+ *
+ * <p>Internally backed by a {@link java.util.HashMap} keyed on item code. Items are stacked
+ * automatically — {@link #Add} first fills existing stacks of the same item code
+ * before opening new slots. Any items that exceed the collection's capacity are
+ * returned as a leftover count.</p>
+ *
+ * <p>Concrete subclasses ({@link cz.davza.studentadventure.objects.PlayerInventory} and
+ * {@link cz.davza.studentadventure.objects.RoomItems}) simply call {@code super(size)}
+ * and inherit all behaviour.</p>
+ *
+ * @param <T> the item type this collection holds, constrained to {@link IItem}
+ *
+ * @see cz.davza.studentadventure.objects.PlayerInventory
+ * @see cz.davza.studentadventure.objects.RoomItems
+ * @see ItemStack
+ */
+public abstract class  ItemCollectionBase<T extends IItem> implements ICollection<T> {
+    private final Map<String,ItemStack> items;
+    private final int capacity;
+    private int usedCapacity;
+    /**
+     * Constructs a new collection with the given capacity
+     *
+     * @param capacity the fixed capacity of this collection (bulk it can hold)
+     */
+    public ItemCollectionBase(int capacity) {
+        this.items = new HashMap<String, ItemStack>();
+        this.capacity = capacity;
+    }
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Adds items to relevant ItemStack.</p>
+     *
+     * @return the number of items that could not
+     * be added due to capacity being full.
+     */
+    @Override
+    public int Add(IItem item, int count){
+        int canInsert = Math.min(count, this.GetRemainingCapacity()/item.getBulk());
+        if (canInsert <= 0){return count;}
+        if (items.containsKey(item.getCode())){
+            items.get(item.getCode()).Add(canInsert);
+        } else{
+            items.put(item.getCode(),new ItemStack(item, canInsert));
+        }
+        // add remaining possible amount to stack
+        usedCapacity += canInsert * item.getBulk();
+        return count - canInsert;
+    }
+    /**
+     * {@inheritDoc}
+     *
+     * <p>If {@code count} meets or exceeds the stack size, the slot is set to
+     * {@code null}. If the item is not present the call is silently ignored.</p>
+     */
+    @Override
+    public void Remove(String code, int count) {
+        ItemStack stack = items.getOrDefault(code, null);
+        if (stack == null) return;
+        int amountToRemove = Math.min(count, stack.getAmount());
+        stack.Remove(amountToRemove);
+        this.usedCapacity -= amountToRemove*stack.getItem().getBulk();
+        if (stack.getAmount() <= 0){ items.remove(code);}
+    }
+    public ItemStack GetByItemCode(String code) {
+        return items.get(code);
+    }
+    /** {@inheritDoc} */
+    @Override
+    public int GetCapacity() {
+        return this.capacity;
+    }
+    @Override
+    public int GetRemainingCapacity(){
+        return this.capacity - this.usedCapacity;
+    }
+    /** {@inheritDoc} */
+    @Override
+    public void Empty() {
+        items.clear();
+        usedCapacity = 0;
+    }
+
+    /**
+     * Returns the Stacks of Items that the collection holds
+     * @return Collection of ItemStacks the collection holds
+     */
+    public Collection<ItemStack> GetItems(){
+        return items.values();
+    }
+//
+}

+ 65 - 0
src/main/java/cz/davza/studentadventure/base/ObservableBase.java

@@ -0,0 +1,65 @@
+package cz.davza.studentadventure.base;
+
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.interfaces.IObservable;
+import cz.davza.studentadventure.interfaces.IObserver;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+// DU Z 12. CVIČENÍ - Návrhový vzor Observer, který se zejména využívá pro sledování ukončení hry
+
+/**
+ * Base implementation of the Observer pattern for game state events.
+ *
+ * <p>Maintains a map of {@link CommandState} keys to sets of registered
+ * {@link IObserver} listeners. Any class that needs to broadcast state-change
+ * events (e.g. {@link cz.davza.studentadventure.objects.Game},
+ * {@link cz.davza.studentadventure.objects.GameMap}) can extend this class and
+ * call {@link #notifyObservers(CommandState)} when the relevant state occurs.</p>
+ *
+ * <p>Multiple observers can be registered for the same state. Observers are stored
+ * in a {@link HashSet}, so duplicate registrations of the same instance are ignored.</p>
+ *
+ * @see IObservable
+ * @see IObserver
+ */
+public class ObservableBase implements IObservable {
+
+    private final Map<CommandState, Set<IObserver>> observers = new HashMap<>();
+    /**
+     * Registers an observer to be notified when the given state is fired.
+     *
+     * <p>If no observer set exists for {@code registeringState} yet, one is
+     * created automatically. Registering the same observer instance twice for
+     * the same state has no effect (set semantics).</p>
+     *
+     * @param registeringState the event state this observer wants to listen for
+     * @param newObserver      the observer to register
+     */
+    @Override
+    public void addObserver(CommandState registeringState,IObserver newObserver) {
+        if  (!observers.containsKey(registeringState)) observers.put(registeringState, new HashSet<>());
+        observers.get(registeringState).add(newObserver);
+    }
+
+    /**
+     * Notifies all observers registered for the given state.
+     *
+     * <p>If no observers are registered for {@code notifiedState}, the call is
+     * silently ignored. Each registered observer's {@link IObserver#update()} method
+     * is called in an unspecified order.</p>
+     *
+     * @param notifiedState the state event that has occurred
+     */
+    @Override
+    public void notifyObservers(CommandState notifiedState) {
+    if   (!observers.containsKey(notifiedState)) return;
+
+    for (IObserver observer : observers.get(notifiedState)) {
+        observer.update();
+    }
+    }
+}

+ 200 - 0
src/main/java/cz/davza/studentadventure/base/RoomBase.java

@@ -0,0 +1,200 @@
+package cz.davza.studentadventure.base;
+
+import cz.davza.studentadventure.interfaces.ICollectable;
+import cz.davza.studentadventure.interfaces.IInteractable;
+import cz.davza.studentadventure.interfaces.IItem;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.ItemStack;
+import cz.davza.studentadventure.objects.RoomItems;
+
+import java.util.*;
+
+/**
+ * Abstract base class for all rooms in the game world.
+ *
+ * <p>A room holds a set of items, a map of named exits to neighbouring rooms,
+ * and a list of commands that are valid while the player is present.
+ *
+ * <p>Subclasses configure the room by adding to {@link #allowedCommands} in
+ * their constructor and can override {@link #onEnter(GameData)} to enforce
+ * entry conditions (e.g. requiring the player to carry a book before entering
+ * a classroom).</p>
+ *
+ * @see cz.davza.studentadventure.rooms.BedroomRoom
+ * @see cz.davza.studentadventure.rooms.ClassroomRoom
+ */
+public abstract class RoomBase {
+    private final String name;
+    private final String exitCode;
+    private final String description;
+    private final ItemCollectionBase<IItem> items;
+    private final Map<String, RoomBase> exits = new HashMap<>();
+
+    /**
+     * Commands permitted in this room in addition to the global commands.
+     * Subclasses add entries (e.g. {@code "study"}, {@code "sleep"}) in their constructor.
+     */
+    protected final List<String> allowedCommands = new ArrayList<>();
+
+
+
+    /**
+     * Constructs a new room with the given name, description and item capacity.
+     *
+     * <p>All rooms automatically allow {@code "pickup"}, {@code "interact"}, and
+     * {@code "walk"} by default.</p>
+     *
+     * @param name         the display name of the room (e.g. {@code "Bedroom"})
+     * @param description  the text shown when the player enters or looks around
+     * @param itemCapacity the maximum number of item slots in this room
+     */
+    public RoomBase(String name,String exitCode, String description, int itemCapacity) {
+        this.name = name;
+        this.exitCode = exitCode;
+        this.description = description;
+        this.items = new RoomItems(itemCapacity);
+
+        // All rooms support these by default
+        allowedCommands.add("pickup");
+        allowedCommands.add("interact");
+        allowedCommands.add("walk");
+        allowedCommands.add("drop");
+    }
+
+
+    /**
+     * Registers a one-way exit from this room to another.
+     *
+     * @param room the room this exit leads to
+     */
+    public void addExit(RoomBase room) {
+        exits.put(room.getExitCode(), room);
+    }
+    /**
+     * Returns the room connected by the given exit key, or {@code null} if no
+     * such exit exists.
+     *
+     * @param key the direction keyword (case-insensitive)
+     * @return the connected room, or {@code null}
+     */
+    public RoomBase getExit(String key) {
+        return exits.get(key.toLowerCase());
+    }
+    /**
+     * Returns an unmodifiable view of all exits registered on this room.
+     *
+     * @return a map of exit keywords to destination rooms
+     */
+    public Map<String, RoomBase> getExits() {
+        return exits;
+    }
+
+    /**
+     * Searches this room's items for an {@link IInteractable} with the given code.
+     *
+     * @param code the item code to look for
+     * @return the matching {@link ItemBase} (cast-safe as {@link IInteractable}),
+     *         or {@code null} if not found
+     */
+    public ItemStack getInteractable(String code) {
+        ItemStack stack = items.GetByItemCode(code);
+        if (stack != null && stack.getItem() instanceof IInteractable) { return stack;}
+        return null;
+    }
+    /**
+     * Searches this room's items for an {@link ICollectable} with the given code.
+     *
+     * @param code the item code to look for
+     * @return the matching item as {@link ICollectable}, or {@code null} if not found
+     */
+    public ItemStack getCollectable(String code) {
+        ItemStack stack = items.GetByItemCode(code);
+        if (stack != null && stack.getItem() instanceof ICollectable) { return stack;}
+        return null;
+    }
+
+    /**
+     * Prints the room's name, description, available exits, and visible items
+     * to {@code System.out}.
+     */
+    public String describe() {
+        StringBuilder description = new StringBuilder();
+        description.append("===").append(name).append(" ===\n");
+        description.append(getDescription()).append("\n");
+        description.append(describeExits());
+        description.append(describeItems());
+        return description.toString();
+    }
+
+    private String describeExits() {
+        if (exits.isEmpty()) {
+            return "There are no exits.\n";
+        }
+        return "Exits: " + String.join(", ", exits.keySet()) + "\n";
+    }
+
+    private String describeItems() {
+        StringBuilder output = new StringBuilder();
+        Collection<ItemStack> roomItems = items.GetItems();
+        if (roomItems.isEmpty()) {
+            output.append("There are no items in this room.\n");
+        }else{
+            for(ItemStack item : roomItems){
+                output.append(item.toString()).append("\n");
+            }
+        }
+        return output.toString();
+    }
+    /**
+     * Removes up to {@code count} of the item with the given code from this room.
+     * If the item is not found the call is silently ignored.
+     *
+     * @param code  the item code to remove
+     * @param count the number of items to remove
+     */
+    public void removeItem(String code, int count) {
+        items.Remove(code, count);
+    }
+    /**
+     * Returns the item collection for this room.
+     *
+     * @return this room's {@link RoomItems}
+     */
+    public ItemCollectionBase<IItem> getItems() { return items; }
+    /**
+     * Returns the display name of this room.
+     *
+     * @return the room name
+     */
+    public String getName() { return name; }
+    /**
+     * Returns the description of this room.
+     *
+     * @return the room description
+     */
+    public String getDescription() { return description; }
+    /**
+     *
+     * <p>Subclasses can override this to enforce entry conditions. To block entry,
+     * call {@code context.getGame().setNextRoom(null)} inside the override.
+     * The default implementation does nothing.</p>
+     *
+     * @param context the current game state
+     * @return CommandResult result of the Walk command
+     */
+    public CommandResult onEnter(GameData context){
+        return CommandResult.success();
+    }
+    public List<String> getAllowedCommands() {
+        return allowedCommands;
+    }
+
+    public String getExitCode() {
+        return exitCode;
+    }
+    @Override
+    public String toString() {
+        return exitCode;
+    }
+}

+ 32 - 0
src/main/java/cz/davza/studentadventure/commands/AvailableCommandsCommand.java

@@ -0,0 +1,32 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+/**
+ * Lists all commands currently available to the player in their location.
+ *
+ * <p>Usage: {@code availablecommands}</p>
+ *
+ * <p>The list is derived from the current room's allowed commands merged with the
+ * global commands that are always available. Useful when the player is unsure
+ * what actions are valid in an unfamiliar room.</p>
+ */
+@CommandKeyword("availablecommands")
+public class AvailableCommandsCommand extends CommandBase {
+
+    /**
+     * {@inheritDoc}
+     * Outputs a bullet-point list of every command keyword the player can currently use.
+     */
+    @Override
+    public CommandResult Execute(GameData context) {
+        StringBuilder output = new StringBuilder();
+        output.append("Commands you can run here are:\n");
+        for (String command:context.getAllowedCommands()){
+            output.append("-").append(command).append("\n");
+        }
+        return CommandResult.success(output.toString());
+    }
+}

+ 51 - 0
src/main/java/cz/davza/studentadventure/commands/BuyCommand.java

@@ -0,0 +1,51 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.Player;
+import cz.davza.studentadventure.Items.EnergyDrink;
+/**
+ * Purchases an item from the campus shop and adds it to the player's inventory.
+ *
+ * <p>Usage: {@code buy <itemCode>} (e.g. {@code buy energy_drink})</p>
+ *
+ * <p>Currently only {@code energy_drink} is available. The command checks the
+ * player's balance and inventory space before completing the transaction.</p>
+ */
+@CommandKeyword("buy")
+public class BuyCommand extends CommandBase {
+    /** {@inheritDoc} Requires one parameter: the item code to purchase. */
+    @Override
+    public CommandResult Execute(GameData context) {
+        if (!hasParams(1)) return CommandResult.invalidParams();
+
+        String itemCode = getParams().get(0).toLowerCase();
+        Player player   = context.getPlayer();
+
+        switch (itemCode) {
+            case "energy_drink":
+                return buyEnergyDrink(player);
+            default:
+                return CommandResult.failed("The shop doesn't sell that.\n");
+        }
+    }
+
+    private CommandResult buyEnergyDrink(Player player) {
+        if (player.getWallet().getMoney() < EnergyDrink.COST) {
+            return CommandResult.failed("You can't afford that. You need "
+                    + EnergyDrink.COST + " coins but only have "
+                    + player.getWallet().getMoney() + ".\n");
+        }
+
+        int leftOver = player.getInventory().Add(new EnergyDrink(), 1);
+        if (leftOver > 0) {
+            return CommandResult.inventoryFull("Your inventory is full, no room for the drink.");
+        }
+
+        player.getWallet().spendMoney(EnergyDrink.COST);
+        return CommandResult.success("You buy an energy drink for " + EnergyDrink.COST
+                + " coins. Money remaining: " + player.getWallet().getMoney() + " coins.\n");
+    }
+}

+ 44 - 0
src/main/java/cz/davza/studentadventure/commands/ConsumeCommand.java

@@ -0,0 +1,44 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.interfaces.IConsumable;
+import cz.davza.studentadventure.objects.ItemStack;
+import cz.davza.studentadventure.objects.Player;
+/**
+ * Consumes an item from the player's inventory, applying its effect.
+ *
+ * <p>Usage: {@code consume <itemcode>} (e.g. {@code consume 2})</p>
+ *
+ * <p>The item at the given slot must implement {@link IConsumable}. One instance
+ * is removed from the slot after consumption. Player stats are printed after the
+ * effect is applied.</p>
+ */
+@CommandKeyword("consume")
+public class ConsumeCommand extends CommandBase {
+    /** {@inheritDoc} Requires one parameter: the items codename. */
+    @Override
+    public CommandResult Execute(GameData context) {
+        if (!hasParams(1)) return CommandResult.invalidParams();
+        String itemCode = getParams().get(0);
+        Player player   = context.getPlayer();
+        ItemStack stack = player.getInventory().GetByItemCode(itemCode);
+
+        if (stack == null) {
+            return CommandResult.itemNotFound("You do not have a(n) " + itemCode + ".\n");
+        }
+
+        if (!(stack.getItem() instanceof IConsumable)) {
+            return CommandResult.failed("You can't consume that.\n");
+        }
+
+        IConsumable consumable = (IConsumable) stack.getItem();
+        consumable.consume(player);
+        player.getInventory().Remove(itemCode, 1);
+
+        player.printStats();
+        return CommandResult.success(consumable.getConsumeMessage());
+    }
+}

+ 26 - 0
src/main/java/cz/davza/studentadventure/commands/DescribeCommand.java

@@ -0,0 +1,26 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+/**
+ * Describes the player's current room — its name, exits, and visible items.
+ *
+ * <p>Usage: {@code describe}</p>
+ *
+ * <p>Delegates to {@link cz.davza.studentadventure.base.RoomBase#describe()}.
+ * Useful after moving to a new room or when the player wants a reminder of
+ * what is around them. No parameters are required.</p>
+ */
+@CommandKeyword("describe")
+public class DescribeCommand extends CommandBase {
+    /**
+     * {@inheritDoc}
+     * Returns the current room's full description string.
+     */
+    @Override
+    public CommandResult Execute(GameData context){
+        return CommandResult.success(context.getMap().getCurrentRoom().describe());
+    }
+}

+ 70 - 0
src/main/java/cz/davza/studentadventure/commands/DropCommand.java

@@ -0,0 +1,70 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.interfaces.IItem;
+import cz.davza.studentadventure.objects.ItemStack;
+import cz.davza.studentadventure.objects.Player;
+/**
+ * Drops an item from the inventory into the current room.
+ *
+ * <p>Usage: {@code drop <itemcode> <amount>} (e.g. {@code drop 0})</p>
+ *
+ * <p>Only the number of items that actually fit in the room are removed from
+ * the inventory. Any items the room could not accommodate remain in the slot.</p>
+ */
+@CommandKeyword("drop")
+public class DropCommand extends CommandBase {
+    /** {@inheritDoc} Requires one parameter: the item code to drop, and an optional quantity (defaults to 1). */
+    @Override
+    public CommandResult Execute(GameData context) {
+        if (!hasParams(1)) return CommandResult.invalidParams();
+        String itemCode = getParams().get(0);
+        if (itemCode == null || itemCode.trim().isEmpty()) {
+            return CommandResult.invalidParams();
+
+        }
+        int dropAmount;
+        String output;
+        Player player   = context.getPlayer();
+        ItemStack stack = player.getInventory().GetByItemCode(itemCode);
+
+        if (getParams().size() == 2) {
+            try{
+                dropAmount = Integer.parseInt(getParams().get(1));
+            } catch  (NumberFormatException e){
+                return CommandResult.invalidParams();
+            }
+        }else{
+            dropAmount = 1;
+        }
+
+
+        if (stack == null) {
+            return CommandResult.itemNotFound("You do not seem to have a(n) " + itemCode + ".\n");
+        }
+
+        IItem item  = stack.getItem();
+        dropAmount = Math.min(stack.getAmount(), dropAmount);
+        // Add to room first
+        int roomLeftCapacity = context.getMap().getCurrentRoom().getItems().GetRemainingCapacity();
+        int leftOverCapacity = roomLeftCapacity/item.getBulk() - dropAmount;
+        if (leftOverCapacity >= 0) {
+            player.getInventory().Remove(itemCode, dropAmount);
+            context.getMap().getCurrentRoom().getItems().Add(item, dropAmount);
+            output = "You drop " + dropAmount + " " + itemCode + ".\n";
+
+        }else{
+            int droppedAmount = dropAmount - Math.abs(leftOverCapacity);
+            // not everything could be dropped (dropAmount * bulk > available capacity
+            // absolute value of leftOverCapacity = the amount we couldn't drop (as long as leftOverCapacity < 0
+            context.getMap().getCurrentRoom().getItems().Add(item, droppedAmount);
+            player.getInventory().Remove(itemCode, droppedAmount);
+            output = "You drop "+ droppedAmount + " of " + itemCode
+                    + " but the room is cluttered so you cannot drop more.\n";
+        }
+        return CommandResult.success(output);
+    }
+}

+ 54 - 0
src/main/java/cz/davza/studentadventure/commands/HelpCommand.java

@@ -0,0 +1,54 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+/**
+ * Displays the full command reference for the game.
+ *
+ * <p>Usage: {@code help}</p>
+ *
+ * <p>Prints a static list of every command keyword and a short description of
+ * what it does. Available in all rooms. No parameters are required.</p>
+ */
+@CommandKeyword("help")
+public class HelpCommand extends CommandBase {
+
+    /**
+     * {@inheritDoc}
+     * Returns the full command reference as a formatted string.
+     */
+    @Override
+    public CommandResult Execute(GameData context) {
+
+        return CommandResult.success(printHelp());
+    }
+    /**
+     * Builds and returns the command reference text.
+     *
+     * @return a multi-line string listing all available commands and their usage
+     */
+    private String printHelp() {
+        return """
+            === COMMANDS ===
+            walk <exit>        - Move to another room
+            pickup <item> <n>  - Pick up n of an item
+            drop <slot>        - Drop item from inventory slot
+            interact <item>    - Interact with an item
+            study <hours>      - Study for n hours
+            sleep              - Sleep (bedroom and classroom only)
+            steal              - Steal money (study room only)
+            buy <item>         - Buy an item (shop only)
+            consume <slot>     - Consume an item from inventory
+            takeexam           - Take the final exam (exam room only)
+            inventory          - Show your inventory
+            stats              - Show your stats
+            time               - Show current game time
+            describe           - Describe the current scene
+            quit               - Give up
+            studentId          - Tells you your student ID (computer only)
+            availablecommands  - Tells you your currently available commands
+            """;
+    }
+}

+ 46 - 0
src/main/java/cz/davza/studentadventure/commands/InteractCommand.java

@@ -0,0 +1,46 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.base.RoomBase;
+import cz.davza.studentadventure.interfaces.IInteractable;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.ItemStack;
+
+/**
+ * Lets the player interact with an interactable item in the current room.
+ *
+ * <p>Usage: {@code interact <itemCode>} (e.g. {@code interact computer})</p>
+ *
+ * <p>Locates the item via {@link RoomBase#getInteractable(String)} and hands
+ * control to {@link IInteractable#Interact(GameData)}. If the code matches a
+ * collectable instead, a contextual "can't interact" message is shown.</p>
+ */
+@CommandKeyword("interact")
+public class InteractCommand extends CommandBase {
+    /** {@inheritDoc} Requires one parameter: the item code to interact with. */
+    @Override
+    public CommandResult Execute(GameData context) {
+        if (!hasParams(1)) return CommandResult.invalidParams();
+
+        String itemCode = getParams().getFirst();
+        RoomBase room = context.getMap().getCurrentRoom();
+
+        // Check if item exists at all first
+        ItemStack item = room.getInteractable(itemCode);
+        if (item == null) {
+            if (room.getCollectable(itemCode) != null) {
+                return CommandResult.itemNotInteractable("You can't interact with that.\n");
+            }
+            return CommandResult.itemNotInteractable("There is no " + itemCode + " here.\n");
+        }
+
+        // Hand control over to the item's own event loop
+
+        return ((IInteractable) item.getItem()).Interact(context);
+
+    }
+
+
+}

+ 30 - 0
src/main/java/cz/davza/studentadventure/commands/InventoryCommand.java

@@ -0,0 +1,30 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.ItemStack;
+/**
+ * Displays the contents of the player's inventory.
+ *
+ * <p>Usage: {@code inventory}</p>
+ *
+ * <p>Lists all occupied slots with their index, item code, and quantity.
+ * Empty slots are omitted. No parameters are required.</p>
+ */
+@CommandKeyword("inventory")
+public class InventoryCommand extends CommandBase {
+    /** {@inheritDoc} */
+    @Override
+    public CommandResult Execute(GameData context) {
+        // No params needed, inventory is always accessible
+        var inventory = context.getPlayer().getInventory();
+        StringBuilder output = new StringBuilder();
+        output.append("=== Inventory ===\n");
+        for (ItemStack inventoryStack : inventory.GetItems()) {
+            output.append(inventoryStack.toString()).append("\n");
+        }
+        return CommandResult.success(output.toString());
+    }
+}

+ 64 - 0
src/main/java/cz/davza/studentadventure/commands/PickupCommand.java

@@ -0,0 +1,64 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.base.RoomBase;
+import cz.davza.studentadventure.interfaces.ICollectable;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.ItemStack;
+
+/**
+ * Picks up a quantity of a collectable item from the current room.
+ *
+ * <p>Usage: {@code pickup <itemCode> <amount>} (e.g. {@code pickup book 2})</p>
+ *
+ * <p></p>
+ */
+@CommandKeyword("pickup")
+
+    public class PickupCommand extends CommandBase {
+    /** {@inheritDoc} Requires two parameters: item code and quantity. */
+        @Override
+        public CommandResult Execute(GameData context) {
+            if (!hasParams(1)) return CommandResult.invalidParams();
+
+            String itemCode = getParams().get(0);
+            int amount;
+            if (getParams().size() >= 2) {
+            try {
+                amount = Integer.parseInt(getParams().get(1));
+            } catch (NumberFormatException e) {
+                return CommandResult.invalidParams();
+            }
+            }
+            else {
+                amount = 1;
+            }
+
+            RoomBase room = context.getMap().getCurrentRoom();
+
+            // Check item exists in room
+            ItemStack item = room.getItems().GetByItemCode(itemCode);
+            if (item == null) return CommandResult.itemNotFound("There is no " + itemCode + " here.\n");
+            if (!(item.getItem() instanceof ICollectable)) return CommandResult.itemNotCollectable("You can't pick that up.\n");
+
+            CommandResult onPickupResult = ((ICollectable) item.getItem()).onPickup(context, amount);
+            if (onPickupResult != null) return onPickupResult;
+            // Try adding to inventory
+            int leftOver = context.getPlayer().getInventory().Add(item.getItem(), amount);
+            // Remove only what was actually picked up from the room
+            int pickedUp = amount - leftOver;
+            if (pickedUp > 0) {
+                room.removeItem(itemCode, pickedUp);
+            }
+
+            if (leftOver > 0) {
+                return CommandResult.inventoryFull("Picked up " + pickedUp + " " + itemCode
+                        + ". Inventory full, " + leftOver + " left on the ground.\n");
+            }
+
+            return CommandResult.success("Picked up " + amount + " " + itemCode + ".\n");
+        }
+    }
+

+ 77 - 0
src/main/java/cz/davza/studentadventure/commands/SleepCommand.java

@@ -0,0 +1,77 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.base.RoomBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.rooms.BedroomRoom;
+import cz.davza.studentadventure.rooms.ClassroomRoom;
+import cz.davza.studentadventure.objects.Player;
+import java.util.Random;
+/**
+ * Lets the player sleep, restoring energy and reducing stress.
+ *
+ * <p>Behaviour differs by location:</p>
+ * <ul>
+ *   <li><b>Bedroom</b> — always succeeds; fully restores stats and advances to next morning.</li>
+ *   <li><b>Classroom</b> — has a {@value #CATCH_CHANCE} chance of being caught, which ends
+ *       the game. On success, partially restores stats and advances time by 2 hours.</li>
+ *   <li><b>Anywhere else</b> — fails with a message.</li>
+ * </ul>
+ */
+@CommandKeyword("sleep")
+public class SleepCommand extends CommandBase {
+
+    private static final double CATCH_CHANCE = 0.30;
+    private final Random random = new Random();
+    /** {@inheritDoc} */
+    @Override
+    public CommandResult Execute(GameData context) {
+        RoomBase room = context.getMap().getCurrentRoom();
+        CommandResult output;
+        if (room instanceof BedroomRoom) {
+            output = sleepInBed(context.getPlayer());
+            context.getPlayer().resetHoursAwake();
+            context.getTime().advanceToNextMorning();
+            return output;
+        } else if (room instanceof ClassroomRoom) {
+            output = sleepInClass(context.getPlayer());
+            context.getPlayer().addHoursAwake(-2);
+            context.getTime().advanceHours(2);
+            return output;
+        }
+
+        return CommandResult.failed("You can't sleep here.\n");
+    }
+
+    private CommandResult sleepInBed(Player player) {
+        restoreStats(player);
+        return CommandResult.success("You crawl into bed and sleep...\n" +
+                "You wake up feeling refreshed.\n" +
+                player.printStats());
+    }
+
+    private CommandResult sleepInClass(Player player) {
+        if (random.nextDouble() < CATCH_CHANCE) {
+            return CommandResult.gameOver(
+                    "You try to sneak in a nap during class...\n" +
+                            "The professor slams a book on your desk.\n" +
+                            "\"Get out of my classroom and don't come back!\"\n" +
+                            "\nGAME OVER - You were caught sleeping in class and kicked out of school.");
+        }
+
+        restoreStats(player);
+        return CommandResult.success(
+                "You try to sneak in a nap during class...\n" +
+                        "You manage to sleep without being noticed.\n" +
+                        "You wake up just in time. Lucky.\n" +
+                        player.printStats());
+    }
+
+    private void restoreStats(Player player) {
+        player.getEnergy().increase(100);
+        player.getStress().decrease(100);
+    }
+}

+ 23 - 0
src/main/java/cz/davza/studentadventure/commands/StatsCommand.java

@@ -0,0 +1,23 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.enums.CommandState;
+/**
+ * Prints the player's current stats to the console.
+ *
+ * <p>Usage: {@code stats}</p>
+ *
+ * <p>Delegates to {@link cz.davza.studentadventure.objects.Player#printStats()}.
+ * No parameters are required.</p>
+ */
+@CommandKeyword("stats")
+public class StatsCommand extends CommandBase {
+    /** {@inheritDoc} */
+    @Override
+    public CommandResult Execute(GameData context) {
+        return CommandResult.success(context.getPlayer().printStats());
+    }
+}

+ 61 - 0
src/main/java/cz/davza/studentadventure/commands/StealCommand.java

@@ -0,0 +1,61 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.Player;
+import java.util.Random;
+/**
+ * Attempts to steal coins from an unattended bag in the study room.
+ *
+ * <p>Usage: {@code steal}</p>
+ *
+ * <p>There is a base {@value #CATCH_CHANCE_NORMAL} chance of being caught. If the
+ * player is in a <em>desperate</em> state (high stress and low energy simultaneously)
+ * the catch chance is reduced to {@value #CATCH_CHANCE_DESPERATE} — desperation makes
+ * the player bolder but also less careful. Getting caught ends the game.</p>
+ *
+ * <p>On success, a random amount between {@value #MIN_STEAL} and {@value #MAX_STEAL}
+ * coins is added to the player's balance.</p>
+ */
+@CommandKeyword("steal")
+public class StealCommand extends CommandBase {
+
+    private static final double CATCH_CHANCE_NORMAL     = 0.40;
+    private static final double CATCH_CHANCE_DESPERATE  = 0.15;
+    private static final int    DESPERATE_STRESS_THRESHOLD = 70;
+    private static final int    DESPERATE_ENERGY_THRESHOLD = 30;
+    private static final int    MIN_STEAL = 10;
+    private static final int    MAX_STEAL = 40;
+
+    private final Random random = new Random();
+    /** {@inheritDoc} */
+    @Override
+    public CommandResult Execute(GameData context) {
+        Player player = context.getPlayer();
+
+        boolean desperate = player.getStress().getValue() > DESPERATE_STRESS_THRESHOLD
+                && player.getEnergy().getValue() < DESPERATE_ENERGY_THRESHOLD;
+
+        double catchChance = desperate ? CATCH_CHANCE_DESPERATE : CATCH_CHANCE_NORMAL;
+        String attempt = desperate
+                ? "Hands shaking, you eye the unattended bag next to you..."
+                : "You glance around and reach for someone's bag...";
+
+        if (random.nextDouble() < catchChance) {
+            return CommandResult.gameOver(
+                    attempt + "\n" +
+                            "Someone shouts \"Hey! Thief!\"\n" +
+                            "Security escorts you out of the building.\n" +
+                            "\nGAME OVER - You were caught stealing.");
+        }
+
+        int stolen = MIN_STEAL + random.nextInt(MAX_STEAL - MIN_STEAL + 1);
+        player.getWallet().earnMoney(stolen);
+        return CommandResult.success(
+                attempt + "\n" +
+                        "You pocket " + stolen + " coins without anyone noticing.\n" +
+                        "Money: " + player.getWallet().getMoney() + " coins");
+    }
+}

+ 29 - 0
src/main/java/cz/davza/studentadventure/commands/StudentIDCommand.java

@@ -0,0 +1,29 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+/**
+ * Displays the player's unique student ID.
+ *
+ * <p>Usage: {@code studentId}</p>
+ *
+ * <p>The student ID is required to enter the exam room. The player can look it up
+ * here while at a computer terminal, then remember (or note) it for later.
+ * No parameters are required.</p>
+ *
+ * @see cz.davza.studentadventure.rooms.ExamRoomRoom
+ */
+@CommandKeyword("studentId")
+public class StudentIDCommand extends CommandBase {
+
+    /**
+     * {@inheritDoc}
+     * Returns the player's student ID string.
+     */
+    @Override
+    public CommandResult Execute(GameData context) {
+        return CommandResult.success("Your student ID is: " + context.getPlayer().getId()+"\n");
+    }
+}

+ 106 - 0
src/main/java/cz/davza/studentadventure/commands/StudyCommand.java

@@ -0,0 +1,106 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.Player;
+/**
+ * Allows the player to study for a given number of hours, gaining intelligence
+ * at the cost of energy and stress.
+ *
+ * <p>Usage: {@code study <hours>} (e.g. {@code study 3})</p>
+ *
+ * <p>Each hour of study consumes energy and generates stress. Both costs scale
+ * exponentially with study duration (via a fatigue multiplier) and are further
+ * increased by the player's sleep-deprivation multiplier. Studying while
+ * exhausted (energy ≤ {@code EXHAUSTION_THRESHOLD}) incurs an additional stress penalty.</p>
+ *
+ * <p>The loop terminates early if the player runs out of energy mid-session.</p>
+ */
+@CommandKeyword("study")
+public class StudyCommand extends CommandBase {
+
+    private static final int    INTELLIGENCE_GAIN    = 5;
+    private static final int    ENERGY_COST          = 10;
+    private static final int    STRESS_GAIN          = 5;
+    private static final int    STRESS_GAIN_TIRED    = 10;
+    private static final int    EXHAUSTION_THRESHOLD = 20;
+    private static final double FATIGUE_RATE         = 1.3;
+    /** {@inheritDoc} Requires one parameter: number of hours to study. */
+    @Override
+    public CommandResult Execute(GameData context) {
+        if (!hasParams(1)) return CommandResult.invalidParams();
+
+        int hours;
+        try {
+            hours = Integer.parseInt(getParams().get(0));
+            if (hours <= 0) return CommandResult.invalidParams();
+        } catch (NumberFormatException e) {
+            return CommandResult.invalidParams();
+        }
+
+        Player player = context.getPlayer();
+
+        if (player.getEnergy().getValue() == 0) {
+            return CommandResult.failed("You are too exhausted to study. Get some sleep.");
+        }
+
+        StringBuilder output = new StringBuilder();
+        int hoursCompleted = 0;
+
+        for (int i = 0; i < hours; i++) {
+            boolean exhausted      = player.getEnergy().getValue() <= EXHAUSTION_THRESHOLD;
+            double fatigueMult     = Math.pow(FATIGUE_RATE, i);
+            double deprivationMult = context.getPlayer().getDeprivationMultiplier();
+            double totalMult       = fatigueMult * deprivationMult;
+
+            int energyCost = (int) Math.round(ENERGY_COST * totalMult);
+            int stressCost = (int) Math.round(
+                    (exhausted ? STRESS_GAIN_TIRED : STRESS_GAIN) * totalMult
+            );
+
+            if (player.getEnergy().getValue() < energyCost) {
+                output.append("You run out of energy after ")
+                        .append(hoursCompleted)
+                        .append(" hour(s) and have to stop.\n");
+                break;
+            }
+
+
+
+            player.getIntelligence().increase(INTELLIGENCE_GAIN);
+            player.getEnergy().decrease(energyCost);
+            player.getStress().increase(stressCost);
+
+            hoursCompleted++;
+            player.addHoursAwake(1);
+            context.getTime().advanceHours(1);
+
+            if (exhausted) {
+                output.append("Hour ").append(hoursCompleted)
+                        .append(": Exhausted, studying is costing you more. ")
+                        .append("(energy -").append(energyCost)
+                        .append(", stress +").append(stressCost).append(")\n");
+            } else {
+                output.append("Hour ").append(hoursCompleted)
+                        .append(": Productive session. ")
+                        .append("(energy -").append(energyCost)
+                        .append(", stress +").append(stressCost).append(")\n");
+            }
+        }
+
+        if (hoursCompleted == hours) {
+            output.append("You finish studying. ").append(buildStatSummary(player));
+        }
+
+        return CommandResult.success(output.toString());
+    }
+
+    private String buildStatSummary(Player player) {
+        return "Stats → "
+                + player.getIntelligence()
+                + " | " + player.getEnergy()
+                + " | " + player.getStress();
+    }
+}

+ 70 - 0
src/main/java/cz/davza/studentadventure/commands/TakeExamCommand.java

@@ -0,0 +1,70 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.Player;
+/**
+ * Triggers the final exam sequence — the win/lose condition of the game.
+ *
+ * <p>Usage: {@code takeexam}</p>
+ *
+ * <p>The player's effective exam score is calculated as:
+ * <pre>effectiveScore = intelligence - (stress / {@value #STRESS_DIVISOR})</pre>
+ * A score of at least {@value #PASS_THRESHOLD} is required to pass. The result
+ * tier (Pass, Merit, Distinction) is based on the effective score.</p>
+ *
+ * <p>Returns {@link CommandState#GAME_WIN} on success or
+ * {@link CommandState#GAME_OVER} on failure.</p>
+ */
+@CommandKeyword("takeexam")
+public class TakeExamCommand extends CommandBase {
+
+    private static final int PASS_THRESHOLD    = 50;
+    private static final int STRESS_DIVISOR    = 10;
+    /** {@inheritDoc} */
+    @Override
+    public CommandResult Execute(GameData context) {
+        Player player = context.getPlayer();
+
+        int intelligence   = player.getIntelligence().getValue();
+        int stressPenalty  = player.getStress().getValue() / STRESS_DIVISOR;
+        int effectiveScore = intelligence - stressPenalty;
+
+        String header = "\n=== FINAL EXAM ===\n" +
+                "You turn over the paper and begin...\n\n" +
+                "Intelligence:    " + intelligence + "\n" +
+                "Stress penalty:  -" + stressPenalty + "\n" +
+                "Effective score: " + effectiveScore + "/" + PASS_THRESHOLD + " needed\n\n";
+
+        if (effectiveScore >= PASS_THRESHOLD) {
+            return pass(effectiveScore, header);
+        } else {
+            return fail(header);
+        }
+    }
+
+    private CommandResult pass(int score, String header) {
+        String result;
+        if (score >= 80) {
+            result = "RESULT: DISTINCTION\nThe professor nods approvingly as you hand in your paper.";
+        } else if (score >= 65) {
+            result = "RESULT: PASS WITH MERIT\nA solid performance. All that studying paid off.";
+        } else {
+            result = "RESULT: PASS\nIt was close, but you scraped through.";
+        }
+        return CommandResult.gameWin(header +
+                "You move through the questions with confidence.\n" +
+                "Two hours later you put your pen down.\n\n" +
+                result + "\n\nCONGRATULATIONS - You passed the exam!");
+    }
+
+    private CommandResult fail(String header) {
+        return CommandResult.gameOver(header +
+                "You stare at the paper. The words blur together.\n" +
+                "You can't remember half of what you studied.\n\n" +
+                "RESULT: FAIL\n\nGAME OVER - You failed the exam.");
+    }
+}

+ 29 - 0
src/main/java/cz/davza/studentadventure/commands/TimeCommand.java

@@ -0,0 +1,29 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+/**
+ * Prints the current in-game time and day.
+ *
+ * <p>Usage: {@code time}</p>
+ *
+ * <p>Delegates to {@link cz.davza.studentadventure.objects.GameTime#toString()},
+ * which formats the time as {@code "Day D, HH:00"}. Useful for tracking how
+ * close the exam deadline is. No parameters are required.</p>
+ *
+ * @see cz.davza.studentadventure.objects.GameTime
+ */
+@CommandKeyword("time")
+public class TimeCommand extends CommandBase {
+
+    /**
+     * {@inheritDoc}
+     * Returns the current game time as a formatted string.
+     */
+    @Override
+    public CommandResult Execute(GameData context) {
+        return CommandResult.success("Current time:" + context.getTime()+"\n");
+    }
+}

+ 56 - 0
src/main/java/cz/davza/studentadventure/commands/WalkCommand.java

@@ -0,0 +1,56 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.base.RoomBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.enums.CommandState;
+/**
+ * Moves the player to a neighbouring room via a named exit.
+ *
+ * <p>Usage: {@code walk <exit>} (e.g. {@code walk hallway})</p>
+ *
+ * <p>Before committing the move, {@link RoomBase#onEnter(GameData)} is called on
+ * the destination room so entry conditions (such as requiring a textbook) can cancel
+ * the walk. Time is only advanced <em>after</em> the entry check passes.</p>
+ *
+ * <p>Returns {@link CommandState#ROOM_CHANGED} on success so the game loop knows to announce room change.</p>
+ */
+@CommandKeyword("walk")
+public class WalkCommand extends CommandBase {
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Requires one parameter: the exit direction string.</p>
+     */
+    @Override
+    public CommandResult Execute(GameData context) {
+        if (!hasParams(1)) return CommandResult.invalidParams();
+
+        String direction = getParams().getFirst();
+        RoomBase nextRoom = context.getMap().getCurrentRoom().getExit(direction);
+
+        if (nextRoom == null) {
+            return CommandResult.failed("There is no exit that way.");
+        }
+
+        CommandResult entryResult = nextRoom.onEnter(context);
+
+        if (entryResult.getResultState() == CommandState.FAILED) {
+            return entryResult; // passes the onEnter failure message back up
+        }
+
+        if (entryResult.getResultState() == CommandState.ACTION_PENDING) {
+            context.getMap().setNextRoom(nextRoom); // hold the pending room change
+            return entryResult; // passes the handler and message back up to Game
+        }
+
+        // normal entry
+        context.getMap().setNextRoom(nextRoom);
+        context.getPlayer().addHoursAwake(2);
+        context.getTime().advanceHours(2);
+        return CommandResult.roomChanged(entryResult.getResultMessage());
+    }
+
+}

+ 53 - 0
src/main/java/cz/davza/studentadventure/enums/CommandState.java

@@ -0,0 +1,53 @@
+package cz.davza.studentadventure.enums;
+
+import cz.davza.studentadventure.objects.Game;
+
+
+// DU Z 12. CVIČENÍ - Návrhový vzor Enum - výčet možných výsledů příkazu
+
+
+/**
+ * Represents the outcome of a command execution.
+ *
+ * <p>Returned by every {@link cz.davza.studentadventure.interfaces.ICommand#Execute} call.
+ * The game loop in {@link Game} switches on this value to
+ * determine how to react — e.g. committing a room change, ending the game, or
+ * displaying a generic error.</p>
+ */
+public enum CommandState {
+    /** The command completed successfully with no side effects requiring special handling. */
+
+    SUCCESS,
+    /** The command succeeded but with a non-fatal issue the player should be aware of. */
+
+    WARNING,
+    /** The command could not be completed (reason already printed to the player). */
+
+    FAILED,
+    /** The command received the wrong number or type of parameters. */
+
+    INVALID_PARAMS,
+    /** The player's inventory was full and could not accept the item. */
+
+    INVENTORY_FULL,
+    /** The specified item was not found in the current context. */
+
+    ITEM_NOT_FOUND,
+    /**
+     * The player successfully walked to a new room.
+     * The game loop commits the room change.
+     */
+    ROOM_CHANGED,
+    /** The item exists but cannot be picked up (e.g. it is interactable-only). */
+    ITEM_NOT_COLLECTABLE,
+    /** The item exists but cannot be interacted with (e.g. it is collectable-only). */
+    ITEM_NOT_INTERACTABLE,
+    /** A fatal event occurred — the game loop should end in defeat. */
+    GAME_OVER,
+    ACTION_PENDING,
+    ACTION_RESUMED,
+    INVENTORY_CHANGE,
+    /** The player won — the game loop should end in victory. */
+    GAME_WIN,
+    GAME_END
+}

+ 18 - 0
src/main/java/cz/davza/studentadventure/enums/DeprivationState.java

@@ -0,0 +1,18 @@
+package cz.davza.studentadventure.enums;
+/**
+ * Represents the player's current sleep-deprivation level.
+ *
+ * <p>Determined by the number of consecutive hours the player has been awake.
+ * Used by {@link cz.davza.studentadventure.objects.Player#getDeprivationMultiplier()}
+ * to scale the cost of tiring activities.</p>
+ *
+ * @see cz.davza.studentadventure.objects.Player#getDeprivationState()
+ */
+public enum DeprivationState {
+    /** The player is well-rested. No penalty multiplier applied. */
+    NORMAL,
+    /** The player is getting tired (14+ hours awake). Mild penalty multiplier applied. */
+    TIRED,
+    /** The player is severely sleep-deprived (20+ hours awake). Escalating penalty applied. */
+    DEPRIVED
+}

+ 55 - 0
src/main/java/cz/davza/studentadventure/gameWrappers/TextGameWrapper.java

@@ -0,0 +1,55 @@
+package cz.davza.studentadventure.gameWrappers;
+
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.interfaces.IGameWrapper;
+import cz.davza.studentadventure.objects.Game;
+
+import java.util.Scanner;
+/**
+ * Text-mode game wrapper — reads player input from {@code System.in} and
+ * drives the main game loop.
+ *
+ * <p>Registers a {@link CommandState#GAME_END} observer on the {@link Game} so the
+ * loop exits cleanly when any end condition (win, lose, or quit) fires, without
+ * the wrapper needing to inspect individual command results.</p>
+ *
+ * @see IGameWrapper
+ * @see Game
+ */
+public class TextGameWrapper implements IGameWrapper {
+    private final Game game;
+    private boolean gameRunning;
+    private final Scanner input;
+    /**
+     * Creates a text wrapper for the given game instance.
+     *
+     * @param game the game to drive
+     */
+    public TextGameWrapper(Game game) {
+        this.game = game;
+        input = new Scanner(System.in);
+    }
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Registers a {@link CommandState#GAME_END} listener that sets
+     * {@code gameRunning} to {@code false}, then enters the read-eval-print loop.
+     * Blank lines are silently skipped. The loop exits when the game signals
+     * {@code GAME_END}.</p>
+     */
+    @Override
+    public void runGame() {
+        this.gameRunning = true;
+        game.addObserver(CommandState.GAME_END, ()->{this.gameRunning = false;});
+        game.printIntro();
+        while(this.gameRunning) {
+            System.out.print(">");
+            String input = this.input.nextLine().trim();
+            if (input.isEmpty()) continue;
+            game.sendCommand(input);
+        }
+        game.printOutro();
+
+
+    }
+}

+ 39 - 0
src/main/java/cz/davza/studentadventure/interfaces/ICollectable.java

@@ -0,0 +1,39 @@
+package cz.davza.studentadventure.interfaces;
+
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+/**
+ * Marks an item as something the player can pick up from a room.
+ *
+ * <p>Items implementing this interface will be found by
+ * {@link cz.davza.studentadventure.base.RoomBase#getCollectable(String)} and can be
+ * acted on by the {@code pickup} command.</p>
+ *
+ * <p>The default {@link #Pickup} implementation adds the item directly to the
+ * provided collection. Classes can override this to produce custom pickup behaviour —
+ * for example, {@link cz.davza.studentadventure.Items.Money} overrides it to add coins
+ * directly to the player's wallet instead of occupying an inventory slot.</p>
+ *
+ * @see cz.davza.studentadventure.commands.PickupCommand
+ * @see cz.davza.studentadventure.Items.Money
+ */
+public interface ICollectable {
+
+    /**
+     * Handles the logic for picking up this item.
+     *
+     * <p>The default implementation attempts to add one instance of this item to
+     * {@code itemCollection}, printing a message if the inventory is full.</p>
+     *
+     * <p>Override this method to implement special pickup behaviour, such as
+     * adding a resource directly to the player rather than to the inventory.</p>
+     *
+     * @param itemCollection the collection to add the item into (typically the player's inventory)
+     * @param context        the current game state, providing access to the player and other data
+     * @return the number of items that could not be picked up (0 if pickup was fully successful)
+     */
+    default CommandResult onPickup(GameData context, int amount){
+        return null;
+
+    }
+}

+ 60 - 0
src/main/java/cz/davza/studentadventure/interfaces/ICollection.java

@@ -0,0 +1,60 @@
+package cz.davza.studentadventure.interfaces;
+import cz.davza.studentadventure.objects.ItemStack;
+/**
+ * Contract for a fixed-size, slot-based item collection.
+ *
+ * <p>Implementations manage a collection of {@link ItemStack} entries, keyed by item code.
+ * Items are stacked automatically up to their {@link IItem#getBulk()} limit,
+ * and any items that cannot fit are returned as a leftover count from {@link #Add}.</p>
+ *
+ * <p>Used by both {@link cz.davza.studentadventure.objects.PlayerInventory} and
+ * {@link cz.davza.studentadventure.objects.RoomItems}.</p>
+ *
+ * @param <T> the type of item this collection holds, must extend {@link IItem}
+ *
+ * @see cz.davza.studentadventure.base.ItemCollectionBase
+ * @see ItemStack
+ */
+public interface ICollection<T> {
+    /**
+     * Adds the specified quantity of an item to the collection.
+     *
+     * <p>The method first attempts to top up any existing stacks of the same item
+     * code, then fills empty slots with new stacks. Items that cannot fit due to
+     * capacity limits are returned as a remainder.</p>
+     *
+     * @param item  the item to add
+     * @param count the number of items to add
+     * @return the number of items that could not be added (0 if all were added)
+     */
+    int Add(T item, int count);
+    /**
+     * Removes a specified number of items from the slot at the given index.
+     *
+     * <p>If {@code count} is greater than or equal to the stack size, the slot
+     * is cleared entirely. If the index is out of bounds or the slot is empty,
+     * the call is silently ignored.</p>
+     *
+     * @param code the items code
+     * @param count the number of items to remove
+     */
+    void Remove(String code, int count);
+
+
+    /**
+     * Returns the overall capacity the collection can hold
+     *
+     * @return Collections bulk capacity
+     */
+    int GetCapacity();
+    /**
+     * Empties collection of all items and resets used capacity
+     */
+    void Empty();
+    /**
+     * Returns the remaining capacity of the collection
+     *
+     * @return Collections remaining bulk capacity (how much can still be added)
+     */
+    int GetRemainingCapacity();
+}

+ 32 - 0
src/main/java/cz/davza/studentadventure/interfaces/ICommand.java

@@ -0,0 +1,32 @@
+package cz.davza.studentadventure.interfaces;
+
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+/**
+ * Contract for all executable player commands.
+ *
+ * <p>Every command the player can type must implement this interface.
+ * Commands are discovered at runtime via the {@link cz.davza.studentadventure.annotations.CommandKeyword}
+ * annotation and instantiated by the {@link cz.davza.studentadventure.managers.CommandFactory}.</p>
+ *
+ * <p>The {@link GameData} context passed to {@link #Execute(GameData)} provides access
+ * to everything a command might need — the player, the current room, game time, etc.</p>
+ *
+ * @see cz.davza.studentadventure.base.CommandBase
+ * @see cz.davza.studentadventure.managers.CommandFactory
+ * @see cz.davza.studentadventure.managers.CommandManager
+ */
+public interface ICommand {
+    /**
+     * Executes this command using the provided game context.
+     *
+     * <p>Implementations are responsible for printing any relevant feedback to
+     * {@code System.out} and returning an appropriate {@link CommandState} to
+     * allow the game loop to react (e.g. changing rooms, ending the game).</p>
+     *
+     * @param gameData the current game state, including player, room, and time
+     * @return a {@link CommandState} describing the outcome of the command
+     */
+    public CommandResult Execute(GameData gameData);
+}

+ 30 - 0
src/main/java/cz/davza/studentadventure/interfaces/ICommandable.java

@@ -0,0 +1,30 @@
+package cz.davza.studentadventure.interfaces;
+
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+/**
+ * Contract for objects that can receive and handle raw player input.
+ *
+ * <p>Implemented by {@link cz.davza.studentadventure.objects.Game} (the main dispatcher)
+ * and by items or rooms that manage their own sub-interaction loops
+ * (e.g. {@link cz.davza.studentadventure.Items.Computer},
+ * {@link cz.davza.studentadventure.rooms.ExamRoomRoom}).</p>
+ *
+ * <p>When a command returns {@link cz.davza.studentadventure.enums.CommandState#ACTION_PENDING},
+ * the embedded {@code ICommandable} becomes the active handler for subsequent
+ * player input until it returns
+ * {@link cz.davza.studentadventure.enums.CommandState#ACTION_RESUMED}.</p>
+ *
+ * @see cz.davza.studentadventure.objects.Game
+ * @see cz.davza.studentadventure.Items.Computer
+ */
+public interface ICommandable {
+    /**
+     * Handles a single line of raw player input.
+     *
+     * @param command the text typed by the player (already trimmed)
+     * @param context the current game state
+     * @return a {@link CommandResult} describing the outcome
+     */
+    CommandResult handleCommand(String command, GameData context);
+}

+ 32 - 0
src/main/java/cz/davza/studentadventure/interfaces/IConsumable.java

@@ -0,0 +1,32 @@
+package cz.davza.studentadventure.interfaces;
+
+import cz.davza.studentadventure.objects.Player;
+/**
+ * Marks an item as consumable by the player.
+ *
+ * <p>Items implementing this interface can be used via the {@code consume} command.
+ * On consumption, {@link #consume(Player)} is called to apply the item's effect,
+ * and the item is then removed from the inventory slot.</p>
+ *
+ * <p>Examples include {@link cz.davza.studentadventure.Items.EnergyDrink} (restores energy)
+ * and {@link cz.davza.studentadventure.Items.MouldyCheese} (reduces energy).</p>
+ *
+ * @see cz.davza.studentadventure.commands.ConsumeCommand
+ */
+public interface IConsumable {
+    /**
+     * Applies this item's effect to the given player.
+     *
+     * <p>This is called before the item is removed from the inventory, so
+     * implementations can safely read or modify player stats here.</p>
+     *
+     * @param player the player consuming this item
+     */
+    void consume(Player player);
+    /**
+     * Returns the message displayed to the player after consuming this item.
+     *
+     * @return a non-null feedback string (e.g. {@code "You skull the energy drink. Energy +40."})
+     */
+    String getConsumeMessage();
+}

+ 19 - 0
src/main/java/cz/davza/studentadventure/interfaces/IGameWrapper.java

@@ -0,0 +1,19 @@
+package cz.davza.studentadventure.interfaces;
+
+/**
+ * Contract for classes that wrap the game loop for a specific UI mode.
+ *
+ * <p>A wrapper owns the read-eval-print loop for one display medium. The text wrapper
+ * ({@link cz.davza.studentadventure.gameWrappers.TextGameWrapper}) reads from
+ * {@code System.in}; a GUI wrapper would read from JavaFX events instead.
+ * Both delegate actual command processing to {@link cz.davza.studentadventure.objects.Game}.</p>
+ *
+ * @see cz.davza.studentadventure.gameWrappers.TextGameWrapper
+ */
+public interface IGameWrapper {
+    /**
+     * Starts the game loop and blocks until the session ends.
+     * Prints the intro, processes player commands in a loop, then prints the outro.
+     */
+    void runGame();
+}

+ 32 - 0
src/main/java/cz/davza/studentadventure/interfaces/IInteractable.java

@@ -0,0 +1,32 @@
+package cz.davza.studentadventure.interfaces;
+
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+/**
+ * Marks an item as interactable by the player.
+ *
+ * <p>Items implementing this interface are found by
+ * {@link cz.davza.studentadventure.base.RoomBase#getInteractable(String)} and can be
+ * used via the {@code interact} command. Unlike consumables, interactable items
+ * remain in the room after use and may contain their own inner event loop
+ * (e.g. {@link cz.davza.studentadventure.Items.Computer}).</p>
+ *
+ * @see cz.davza.studentadventure.commands.InteractCommand
+ * @see cz.davza.studentadventure.Items.Bed
+ * @see cz.davza.studentadventure.Items.Computer
+ * @see cz.davza.studentadventure.Items.StickyNote
+ */
+public interface IInteractable {
+    /**
+     * Handles player interaction with this item.
+     *
+     * <p>Implementations should print feedback directly to {@code System.out} where
+     * needed and return any final message string — or an empty string if the output
+     * was already printed internally. The returned string is printed by
+     * {@link cz.davza.studentadventure.commands.InteractCommand} only if non-empty.</p>
+     *
+     * @param context the current game state
+     * @return a result message, or an empty string if no further output is needed
+     */
+    public CommandResult Interact(GameData context);
+}

+ 40 - 0
src/main/java/cz/davza/studentadventure/interfaces/IItem.java

@@ -0,0 +1,40 @@
+package cz.davza.studentadventure.interfaces;
+
+/**
+ * Base contract for all items in the game world.
+ *
+ * <p>Every item — whether collectable, consumable, or interactable — must implement
+ * this interface. It provides the minimum identity information needed by the inventory
+ * and room item systems to store and retrieve items.</p>
+ *
+ * <p>Items are distinguished from one another by their {@link #getCode()} value, which
+ * is used for lookups throughout the codebase (e.g. {@code "book"}, {@code "energy_drink"}).</p>
+ *
+ * @see cz.davza.studentadventure.base.ItemBase
+ * @see ICollectable
+ * @see IConsumable
+ * @see IInteractable
+ */
+public interface IItem {
+    /**
+     * Returns the unique identifier code for this item type.
+     *
+     * <p>Codes are lowercase strings used by commands and room lookups
+     * (e.g. {@code "book"}, {@code "energy_drink"}, {@code "mouldy_cheese"}).
+     * Two item instances with the same code are treated as the same item type
+     * by the stacking system.</p>
+     *
+     * @return the item's unique code string
+     */
+    String getCode();
+    /**
+     * Returns the maximum number of this item that can be held in a single
+     * {@link cz.davza.studentadventure.objects.ItemStack}.
+     *
+     * <p>Items with a max stack of 1 occupy their own slot per instance.
+     * Higher values allow multiple items to share a slot up to the given limit.</p>
+     *
+     * @return the maximum stack size, always greater than zero
+     */
+    int getBulk();
+}

+ 31 - 0
src/main/java/cz/davza/studentadventure/interfaces/IObservable.java

@@ -0,0 +1,31 @@
+package cz.davza.studentadventure.interfaces;
+
+import cz.davza.studentadventure.enums.CommandState;
+/**
+ * Contract for objects that broadcast state-change events to registered listeners.
+ *
+ * <p>Part of the Observer pattern. Classes that produce events (e.g.
+ * {@link cz.davza.studentadventure.objects.Game},
+ * {@link cz.davza.studentadventure.objects.PlayerInventory}) implement this interface.
+ * Interested parties register via {@link #addObserver} and receive callbacks
+ * via {@link IObserver#update()} when the relevant {@link CommandState} fires.</p>
+ *
+ * @see IObserver
+ * @see cz.davza.studentadventure.base.ObservableBase
+ */
+public interface IObservable {
+    /**
+     * Registers {@code o} to be notified whenever {@code registeringState} is fired.
+     *
+     * @param registeringState the event the observer wants to listen for
+     * @param o                the observer to register
+     */
+    void addObserver(CommandState registeringState, IObserver o);
+    /**
+     * Notifies all observers registered for {@code notifiedState}.
+     * If no observers are registered for the state, the call is silently ignored.
+     *
+     * @param notifiedState the event that has occurred
+     */
+    void notifyObservers(CommandState notifiedState);
+}

+ 19 - 0
src/main/java/cz/davza/studentadventure/interfaces/IObserver.java

@@ -0,0 +1,19 @@
+package cz.davza.studentadventure.interfaces;
+/**
+ * Callback interface for the Observer pattern.
+ *
+ * <p>Implement this interface and register with an {@link IObservable} to receive
+ * notifications when a particular {@link cz.davza.studentadventure.enums.CommandState}
+ * event fires. The {@link #update()} method carries no parameters — the observer
+ * is expected to know the context from the object it registered on, or to query
+ * shared state when called.</p>
+ *
+ * @see IObservable
+ * @see cz.davza.studentadventure.base.ObservableBase
+ */
+public interface IObserver {
+    /**
+     * Called by the observed object when the event this observer is registered for occurs.
+     */
+    void update();
+}

+ 42 - 0
src/main/java/cz/davza/studentadventure/interfaces/IOutputSink.java

@@ -0,0 +1,42 @@
+package cz.davza.studentadventure.interfaces;
+/**
+ * Abstraction over the destination for all game text output.
+ *
+ * <p>Decouples the game logic from the display medium. The text-mode implementation
+ * ({@link cz.davza.studentadventure.outputsinks.TextOutputSink}) buffers output and
+ *
+ * <p>All game output should go through this interface rather than calling
+ * {@code System.out} directly, so the game remains display-agnostic.</p>
+ *
+ * @see cz.davza.studentadventure.outputsinks.TextOutputSink
+ */
+public interface IOutputSink {
+    /**
+     * Appends {@code text} to the internal buffer without a trailing newline.
+     *
+     * @param text the text to buffer
+     */
+    void buffer(String text);
+    /**
+     * Flushes the internal buffer to the underlying output medium and clears it.
+     * Call this after a complete logical unit of output (e.g. after processing
+     * each command) to ensure text appears promptly.
+     */
+    void flush();
+    /**
+     * Convenience method — delegates to {@link #buffer(String)}.
+     *
+     * @param text the text to append
+     */
+    default void append(String text) {
+        buffer(text);
+    }
+    /**
+     * Buffers {@code text} followed by a newline character.
+     *
+     * @param text the line of text to write
+     */
+    default void writeLine(String text) {
+        buffer(text + "\n");
+    }
+}

+ 19 - 0
src/main/java/cz/davza/studentadventure/main/Main.java

@@ -0,0 +1,19 @@
+package cz.davza.studentadventure.main;
+
+import cz.davza.studentadventure.gameWrappers.TextGameWrapper;
+import cz.davza.studentadventure.interfaces.IGameWrapper;
+import cz.davza.studentadventure.objects.Game;
+import cz.davza.studentadventure.outputsinks.TextOutputSink;
+
+public class Main {
+    /**
+     * Application entry point.
+     *
+     * @param args command-line arguments; pass {@code "text"} to start in text mode
+     */
+    public static void main(String[] args) {
+        Game game = new Game(new TextOutputSink());
+        IGameWrapper gameWrapper = new TextGameWrapper(game);
+        gameWrapper.runGame();
+    }
+}

+ 99 - 0
src/main/java/cz/davza/studentadventure/managers/CommandFactory.java

@@ -0,0 +1,99 @@
+package cz.davza.studentadventure.managers;
+
+import cz.davza.studentadventure.annotations.CommandKeyword;
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.commands.*;
+import cz.davza.studentadventure.interfaces.ICommand;
+import org.reflections.Reflections;
+
+
+import java.util.*;
+
+
+/**
+ * Discovers and instantiates {@link CommandBase} subclasses at runtime.
+ *
+ * <p>On first use, {@code CommandFactory} scans the {@code cz.davza.studentadventure.commands}
+ * package for compiled {@code .class} files annotated with
+ * {@link cz.davza.studentadventure.annotations.CommandKeyword}. Each discovered class is
+ * registered in an internal map keyed by its keyword string.</p>
+ *
+ * <p>Subsequent calls to {@link #createCommand} instantiate the registered class
+ * via its no-arg constructor, inject the provided params, and return the result.
+ * The scan only runs once per factory instance (guarded by {@link #loaded}).</p>
+ *
+ * <p><strong>Note:</strong> {@code registry} and {@code loaded} are instance fields.
+ * For efficiency in long-running sessions consider making them {@code static} so the
+ * scan is shared across all factory instances.</p>
+ *
+ * @see CommandManager
+ * @see cz.davza.studentadventure.annotations.CommandKeyword
+ */
+public class CommandFactory {
+    private static final String COMMANDS_PACKAGE = "cz.davza.studentadventure.commands";
+    private static final Map<String, Class<? extends CommandBase>> REGISTRY = new HashMap<>();
+    private static boolean loaded = false;
+    /**
+     * Scans the commands package and populates the registry.
+     * Does nothing if the registry has already been loaded.
+     *
+     * <p>Any class in the package that is annotated with
+     * {@link cz.davza.studentadventure.annotations.CommandKeyword} is registered under
+     * its keyword value. Errors are printed to {@code System.err} and the scan
+     * is considered complete (commands found before the error remain registered).</p>
+     */
+    static {
+        try {
+            Reflections reflections = new Reflections(COMMANDS_PACKAGE);
+            Set<Class<?>> annotatedClasses = reflections.getTypesAnnotatedWith(CommandKeyword.class);
+
+            for (Class<?> clazz : annotatedClasses) {
+                // Defensive design: ensure the annotated class actually extends CommandBase
+                if (CommandBase.class.isAssignableFrom(clazz)) {
+                    CommandKeyword annotation = clazz.getAnnotation(CommandKeyword.class);
+                    String keyword = annotation.value().toLowerCase().trim();
+
+                    Class<? extends CommandBase> commandClass = (Class<? extends CommandBase>) clazz;
+
+                    REGISTRY.put(keyword, commandClass);
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Instantiates the command class registered under {@code keyword} and
+     * sets its parameter list.
+     *
+     * @param keyword the command keyword (case-insensitive)
+     * @param params  the tokenised argument list to pass to the command
+     * @return a configured {@link CommandBase} ready for execution,
+     *         or {@code null} if no command is registered under {@code keyword}
+     */
+    public CommandBase createCommand(String keyword, List<String> params) {
+        Class<?> clazz = REGISTRY.get(keyword.toLowerCase());
+        if (clazz == null) return null;
+
+        try {
+            CommandBase command = (CommandBase) clazz
+                    .getDeclaredConstructor()
+                    .newInstance();
+            command.setParams(params);
+            return command;
+        } catch (Exception e) {
+            System.err.println("Failed to instantiate command: " + keyword);
+            return null;
+        }
+    }
+    /**
+     * Returns whether a command is registered under the given keyword.
+     *
+     * @param keyword the keyword to check (case-insensitive)
+     * @return {@code true} if a command class is registered for this keyword
+     */
+    public boolean isRegistered(String keyword) {
+        return REGISTRY.containsKey(keyword.toLowerCase());
+    }
+}

+ 47 - 0
src/main/java/cz/davza/studentadventure/managers/CommandManager.java

@@ -0,0 +1,47 @@
+package cz.davza.studentadventure.managers;
+
+import cz.davza.studentadventure.base.CommandBase;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+/**
+ * Parses raw player input into an executable {@link CommandBase}.
+ *
+ * <p>Input is tokenised by whitespace; the first token is treated as the command
+ * keyword and the remainder become the parameter list. The keyword is checked
+ * against the provided {@code allowedCommands} list before a command is created —
+ * this allows rooms to restrict which commands are valid in their context.</p>
+ *
+ * <p>Actual command instantiation is delegated to a {@link CommandFactory}.</p>
+ *
+ * @see CommandFactory
+ */
+public class CommandManager {
+    private static final CommandFactory factory = new CommandFactory();
+    /**
+     * Parses {@code userInput} and returns a ready-to-execute command if the keyword
+     * is in {@code allowedCommands}, otherwise returns {@code null}.
+     *
+     * <p>The returned command already has its parameter list set via
+     * {@link CommandBase#setParams(List)}.</p>
+     *
+     * @param userInput       the raw string typed by the player
+     * @param allowedCommands the set of command keywords valid in the current context
+     * @return a configured {@link CommandBase}, or {@code null} if the input was blank
+     *         or the keyword was not allowed
+     */
+    public static CommandBase parseCommand(String userInput, List<String> allowedCommands) {
+        if (userInput == null || userInput.trim().isEmpty()) return null;
+
+        String[] parts = userInput.trim().toLowerCase().split("\\s+");
+        String keyword = parts[0];
+
+        if (!allowedCommands.contains(keyword)) return null;
+
+        List<String> params = new ArrayList<>(
+                Arrays.asList(parts).subList(1, parts.length)
+        );
+
+        return factory.createCommand(keyword, params);
+    }
+}

+ 222 - 0
src/main/java/cz/davza/studentadventure/objects/CommandResult.java

@@ -0,0 +1,222 @@
+package cz.davza.studentadventure.objects;
+
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.interfaces.ICommandable;
+/**
+ * Immutable value object returned by every command execution.
+ *
+ * <p>Carries three pieces of information:</p>
+ * <ul>
+ *   <li>A {@link CommandState} that tells the game loop how to react.</li>
+ *   <li>A message string to display to the player (may be empty).</li>
+ *   <li>An optional {@link ICommandable} handler for multi-turn interactions
+ *       (e.g. the Computer password prompt). When non-null, the game loop
+ *       redirects subsequent player input to this handler instead of the
+ *       main command dispatcher.</li>
+ * </ul>
+ *
+ * <p>Instances are created exclusively through the static factory methods
+ * (e.g. {@link #success(String)}, {@link #failed(String)}) — the constructor
+ * is private to enforce this.</p>
+ *
+ * @see CommandState
+ * @see cz.davza.studentadventure.interfaces.ICommand
+ */
+
+public class CommandResult {
+    private final CommandState resultState;
+    private final String resultMessage;
+    private final ICommandable newCommandHandler;
+
+    private CommandResult(CommandState resultState, String resultMessage, ICommandable newCommandHandler){
+        this.resultState = resultState;
+        this.resultMessage = resultMessage;
+        this.newCommandHandler = newCommandHandler;
+
+    }
+    // ── Handler switching ─────────────────────────────────────────────
+
+    /**
+     * Creates a result that suspends normal command processing and routes
+     * subsequent input to {@code nextHandler} until it returns
+     * {@link #actionResumed(String)}.
+     *
+     * @param nextHandler the handler to receive the next player input
+     * @param output      the message shown when the pending state begins
+     * @return a new {@link CommandState#ACTION_PENDING} result
+     */
+    public static CommandResult actionPending(ICommandable nextHandler, String output) {
+        return new CommandResult(CommandState.ACTION_PENDING, output, nextHandler);
+    }
+    /**
+     * Creates a result that ends a pending sub-interaction and returns
+     * control to the main game loop.
+     *
+     * @param output the message shown when control is returned
+     * @return a new {@link CommandState#ACTION_RESUMED} result
+     */
+    public static CommandResult actionResumed(String output) {
+        return new CommandResult(CommandState.ACTION_RESUMED, output, null);
+    }
+    // ── General purpose ──────────────────────────────────────────────
+    /**
+     * Creates a successful result with a player-facing message.
+     *
+     * @param output the message to display
+     * @return a new {@link CommandState#SUCCESS} result
+     */
+    public static CommandResult success(String output) {
+        return new CommandResult(CommandState.SUCCESS, output, null);
+    }
+    /**
+     * Creates a successful result with no message.
+     *
+     * @return a new {@link CommandState#SUCCESS} result with an empty message
+     */
+    public static CommandResult success() {
+        return new CommandResult(CommandState.SUCCESS, "", null);
+    }
+    /**
+     * Creates a warning result — the command ran but something was noteworthy.
+     *
+     * @param output the warning message to display
+     * @return a new {@link CommandState#WARNING} result
+     */
+    public static CommandResult warning(String output) {
+        return new CommandResult(CommandState.WARNING, output, null);
+    }
+    /**
+     * Creates a failure result — the command could not be completed.
+     *
+     * @param output the reason shown to the player
+     * @return a new {@link CommandState#FAILED} result
+     */
+    public static CommandResult failed(String output) {
+        return new CommandResult(CommandState.FAILED, output, null);
+    }
+    /**
+     * Creates an invalid-parameters result with no message.
+     * The game loop replaces the empty message with a generic hint.
+     *
+     * @return a new {@link CommandState#INVALID_PARAMS} result
+     */
+    public static CommandResult invalidParams() {
+        return new CommandResult(CommandState.INVALID_PARAMS, "", null);
+    }
+
+    // ── Item specific ─────────────────────────────────────────────────
+    /**
+     * Creates a result indicating the player's inventory is full.
+     *
+     * @param output the message describing what could not be added
+     * @return a new {@link CommandState#INVENTORY_FULL} result
+     */
+    public static CommandResult inventoryFull(String output) {
+        return new CommandResult(CommandState.INVENTORY_FULL, output, null);
+    }
+    /**
+     * Creates a result indicating the target item was not found.
+     *
+     * @param output the message shown to the player
+     * @return a new {@link CommandState#ITEM_NOT_FOUND} result
+     */
+    public static CommandResult itemNotFound(String output) {
+        return new CommandResult(CommandState.ITEM_NOT_FOUND, output, null);
+    }
+    /**
+     * Creates a result indicating the item cannot be picked up.
+     *
+     * @param output the message shown to the player
+     * @return a new {@link CommandState#ITEM_NOT_COLLECTABLE} result
+     */
+    public static CommandResult itemNotCollectable(String output) {
+        return new CommandResult(CommandState.ITEM_NOT_COLLECTABLE, output, null);
+    }
+    /**
+     * Creates a result indicating the item cannot be interacted with.
+     *
+     * @param output the message shown to the player
+     * @return a new {@link CommandState#ITEM_NOT_INTERACTABLE} result
+     */
+    public static CommandResult itemNotInteractable(String output) {
+        return new CommandResult(CommandState.ITEM_NOT_INTERACTABLE, output, null);
+    }
+
+    // ── Navigation ────────────────────────────────────────────────────
+    /**
+     * Creates a result indicating the player successfully moved to a new room.
+     * The game loop will commit the pending room change and describe the new room.
+     *
+     * @param output the message shown after the move (may be empty)
+     * @return a new {@link CommandState#ROOM_CHANGED} result
+     */
+    public static CommandResult roomChanged(String output) {
+        return new CommandResult(CommandState.ROOM_CHANGED, output, null);
+    }
+
+    // ── Handler switching ─────────────────────────────────────────────
+    /**
+     * Creates a result that changes the active command handler without changing state.
+     *
+     * @param nextHandler the new handler to use for subsequent input
+     * @param output      the message shown when the switch occurs
+     * @return a new {@link CommandState#SUCCESS} result with a handler attached
+     */
+    public static CommandResult changeHandler(ICommandable nextHandler, String output) {
+        return new CommandResult(CommandState.SUCCESS, output, nextHandler);
+    }
+
+    // ── Game ending ───────────────────────────────────────────────────
+    /**
+     * Creates a game-over result ending the session in defeat.
+     *
+     * @param output the final message shown to the player
+     * @return a new {@link CommandState#GAME_OVER} result
+     */
+
+    public static CommandResult gameOver(String output) {
+        return new CommandResult(CommandState.GAME_OVER, output, null);
+    }
+    /**
+     * Creates a game-win result ending the session in victory.
+     *
+     * @param output the final message shown to the player
+     * @return a new {@link CommandState#GAME_WIN} result
+     */
+    public static CommandResult gameWin(String output) {
+        return new CommandResult(CommandState.GAME_WIN, output, null);
+    }
+    /**
+     * Creates a generic game-end result (used for graceful shutdown, e.g. quit).
+     *
+     * @param output the final message shown to the player
+     * @return a new {@link CommandState#GAME_END} result
+     */
+    public static CommandResult gameEnd(String output){
+        return new CommandResult(CommandState.GAME_END,  output, null);
+    }
+    /**
+     * Returns the outcome state of this result, used by the game loop to branch.
+     *
+     * @return the {@link CommandState}
+     */
+    public CommandState getResultState() {
+        return resultState;
+    }
+    /**
+     * Returns the message to display to the player.
+     *
+     * @return the result message; never {@code null}, may be empty
+     */
+    public String getResultMessage() {
+        return resultMessage;
+    }
+    /**
+     * Returns the optional next command handler for multi-turn interactions.
+     *
+     * @return the next {@link ICommandable}, or {@code null} for single-turn results
+     */
+    public ICommandable getNewCommandHandler() {
+        return newCommandHandler;
+    }
+}

+ 256 - 0
src/main/java/cz/davza/studentadventure/objects/Game.java

@@ -0,0 +1,256 @@
+package cz.davza.studentadventure.objects;
+
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.base.ObservableBase;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.interfaces.ICommandable;
+import cz.davza.studentadventure.interfaces.IOutputSink;
+import cz.davza.studentadventure.managers.CommandManager;
+/**
+ * Central game controller — the single entry point for all player input.
+ *
+ * <p>Extends {@link ObservableBase} so that wrappers (text, GUI) can register
+ * listeners for lifecycle events such as {@link CommandState#GAME_END}. Implements
+ * {@link ICommandable} so the same {@link #handleCommand} interface is shared with
+ * sub-handlers like {@link cz.davza.studentadventure.Items.Computer}.</p>
+ *
+ * <h2>Command routing</h2>
+ * <p>Each call to {@link #sendCommand(String)} is first checked for a quit request,
+ * then forwarded to {@link #currentCommandHandler}. During normal play that handler
+ * is {@code this} (the Game). When a command returns
+ * {@link CommandState#ACTION_PENDING}, the embedded {@link ICommandable} becomes
+ * the active handler until it returns {@link CommandState#ACTION_RESUMED}.</p>
+ *
+ * <h2>Game-over conditions</h2>
+ * <ul>
+ *   <li>Player stress reaches 100.</li>
+ *   <li>The exam deadline passes without taking the exam.</li>
+ *   <li>Certain commands return {@link CommandState#GAME_OVER} directly.</li>
+ * </ul>
+ *
+ * @see GameData
+ * @see CommandManager
+ * @see cz.davza.studentadventure.interfaces.IOutputSink
+ */
+public class Game extends ObservableBase implements ICommandable {
+    private static final int    INVENTORY_SIZE  = 20;
+    private boolean gameOver = false;
+    private GameData context;
+    private final IOutputSink output;
+    private ICommandable currentCommandHandler;
+
+    /**
+     * Creates a new game instance, initialises all game state, and sets up the world map.
+     *
+     * @param output the sink that receives all text to display (text console or GUI)
+     */
+
+    public Game(IOutputSink output)
+    {
+        this.currentCommandHandler = this;
+        this.output = output;
+        setupContext();
+    }
+    /**
+     * Initialises the player, map, and time, then wires them together into a {@link GameData}.
+     */
+    private void setupContext() {
+        Player player = new Player("Student", INVENTORY_SIZE);
+        GameMap map  = new GameMap();
+        GameTime  time = new GameTime();
+        context = new GameData(player, map,time);
+    }
+
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Parses {@code input} against the currently allowed commands and executes the
+     * matching {@link CommandBase}. Returns a failure result if the command is unknown
+     * or not allowed in the current room.</p>
+     */
+    @Override
+    public CommandResult handleCommand(String input, GameData context) {
+        CommandBase command = CommandManager.parseCommand(input, context.getAllowedCommands());
+        if (command == null) {
+            return CommandResult.failed("You can't do that here.");
+        }
+        return command.Execute(context);
+    }
+
+    /**
+     * Processes a single line of player input and returns the resulting {@link CommandResult}.
+     *
+     * <p>This is the primary method called by game wrappers. It handles:</p>
+     * <ul>
+     *   <li>Rejecting input after the game has ended.</li>
+     *   <li>The {@code quit} command (immediately ends the session).</li>
+     *   <li>Routing to the active {@link ICommandable} handler.</li>
+     *   <li>State transitions: room changes, handler switches, game-over/win checks.</li>
+     *   <li>Time-based deadline enforcement.</li>
+     *   <li>Stress-based game-over enforcement.</li>
+     *   <li>Sleep-deprivation warnings.</li>
+     * </ul>
+     *
+     * <p>All output is written to the {@link IOutputSink} provided at construction.</p>
+     *
+     * @param input the raw string typed by the player
+     * @return the final {@link CommandResult} after all post-processing
+     */
+    public CommandResult sendCommand(String input){
+        if (gameOver){
+            output.writeLine("GAME OVER - The game is already over.");
+            return CommandResult.gameOver("The game is already over");
+        }
+        if (input.equalsIgnoreCase("quit")) {
+            output.writeLine("You give up and drop out.");
+            notifyObservers(CommandState.GAME_END);
+            gameOver = true;
+            return CommandResult.gameOver("You give up and drop out.");
+        }
+
+        // Delegate to current handler if not Game itself
+        CommandResult result = currentCommandHandler.handleCommand(input, context);
+
+        // Handle handler switching
+        switch (result.getResultState()) {
+            case ACTION_PENDING:
+                currentCommandHandler = result.getNewCommandHandler();
+                break;
+
+            case ACTION_RESUMED:
+                currentCommandHandler = this;
+                if ( context.getMap().hasPendingRoomChange()) {
+                    context.getMap().commitRoomChange();
+                    String roomDesc = context.getMap().getCurrentRoom().describe();
+                    context.updateAllowedCommands();
+                    result =  CommandResult.success(
+                            result.getResultMessage() + "\n" +
+                                    roomDesc + "\n" +
+                                    "Time: " + context.getTime());
+                }
+                break;
+
+            case ROOM_CHANGED:
+                if (context.getMap().hasPendingRoomChange()) {
+                    context.getMap().commitRoomChange();
+                    String roomDesc = context.getMap().getCurrentRoom().describe();
+                    context.updateAllowedCommands();
+                    result =  CommandResult.roomChanged(
+                            result.getResultMessage() + "\n" +
+                                    roomDesc + "\n" +
+                                    "Time: " + context.getTime());
+                }
+                break;
+
+            case GAME_WIN:
+                notifyObservers(CommandState.GAME_END);
+                gameOver = true;
+                result =  CommandResult.gameWin(
+                        result.getResultMessage() + "\n" +
+                                "Good job! You have successfully survived finals week!\n" );
+                break;
+
+
+            case GAME_OVER:
+                notifyObservers(CommandState.GAME_END);
+                gameOver = true;
+                result =  CommandResult.gameOver(
+                        result.getResultMessage() + "\n" );
+                break;
+
+            case INVALID_PARAMS:
+                result =  CommandResult.failed(
+                        result.getResultMessage().isEmpty()
+                                ? "That command wasn't quite right. Type 'help' for guidance."
+                                : result.getResultMessage());
+                break;
+
+            default:
+                break;
+        }
+        printDeprivationWarning();
+        // check if time is over
+        if(this.context.getTime().isPastDeadline()){
+            output.writeLine("\nThe school bell rings.");
+            output.writeLine("You look down at your watch and realise it's too late.");
+            output.writeLine("\nGAME OVER - Didn't take exam in time.");
+            gameOver = true;
+            notifyObservers(CommandState.GAME_END);
+            result =  CommandResult.gameOver("Didn't take exam in time.");
+        }
+
+
+        // Stress game over check
+        if (context.getPlayer().getStress().getValue() >= 100) {
+            output.writeLine("\nThe pressure becomes too much.");
+            output.writeLine("You have a breakdown.");
+            output.writeLine("\nGAME OVER - Stress reached 100.");
+            notifyObservers(CommandState.GAME_END);
+            gameOver = true;
+            result =  CommandResult.gameOver("Stress reached 100.");
+        }
+        output.writeLine(result.getResultMessage());
+        this.output.flush();
+        return result;
+    }
+
+    /**
+     * Prints a warning to the output sink if the player is tired or severely deprived.
+     * Called automatically at the end of every {@link #sendCommand} cycle.
+     */
+    private void printDeprivationWarning() {
+        switch (context.getPlayer().getDeprivationState()) {
+            case TIRED:
+                output.writeLine("⚠  You're getting tired. Consider sleeping soon.");
+                break;
+            case DEPRIVED:
+                output.writeLine(String.format(
+                        "⚠  You're exhausted. Everything costs more. (deprivation multiplier: %.1fx)",
+                        context.getPlayer().getDeprivationMultiplier()));
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Prints the opening title card and initial room description to the output sink.
+     * Called once by the game wrapper before the main loop begins.
+     */
+    public void printIntro() {
+        output.writeLine("""
+                ╔══════════════════════════════════════╗
+                ║    STUDENT ADVENTURE - Finals week   ║
+                ║      Can you survive exam week?      ║
+                ╚══════════════════════════════════════╝
+                
+                You wake up on Day 1. The exam is on Day 7 at 09:00.
+                You have 6 days to prepare. Good luck.
+                """);
+        output.writeLine(context.getMap().getCurrentRoom().describe());
+        output.writeLine("Time: " + context.getTime());
+        output.writeLine("Type 'help' for a list of commands.\n");
+        output.flush();
+    }
+    /**
+     * Prints a closing message after the game session has ended.
+     * Called once by the game wrapper after the main loop exits.
+     */
+    public void printOutro(){
+        output.writeLine("""
+               Thanks for playing STUDENT ADVENTURE - Finals week \n""");
+    }
+    /**
+     * Returns the current game context (player, map, time).
+     *
+     * @return the active {@link GameData}
+     */
+    public GameData getContext() {
+        return context;
+    }
+
+
+
+
+}

+ 58 - 0
src/main/java/cz/davza/studentadventure/objects/GameData.java

@@ -0,0 +1,58 @@
+package cz.davza.studentadventure.objects;
+
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Immutable context object passed to every command during execution.
+ *
+ * <p>{@code GameData} is the single access point that commands use to read and
+ * modify the game state. It holds references to the {@link Player}, the
+ * {@link GameMap}, the {@link Game} instance (for things
+ * like room transitions and global commands), and the {@link GameTime} clock.</p>
+ *
+ * <p>The object itself is created once in {@link Game#setupContext()} and reused throughout
+ * the session.
+ */
+public class GameData {
+    private final Player player;
+    private final GameMap map;
+    private final GameTime time;
+    private final String computerPassword = "java123";
+    private static final List<String> GLOBAL_COMMANDS = List.of("inventory", "stats", "consume", "describe", "time","help","availablecommands");
+    private final List<String> allowedCommands = new ArrayList<>();
+    /**
+     * Constructs a new game context.
+     *
+     * @param player      the player character
+     * @param map         the world map
+     * @param time        the game clock
+     */
+    public GameData(Player player, GameMap map, GameTime time) {
+        this.player = player;
+        this.map = map;
+        this.time = time;
+        map.setupMap(this);
+        updateAllowedCommands();
+
+    }
+    /** @return the game clock */
+    public GameTime getTime() { return time; }
+    /** @return the player character */
+    public Player getPlayer() { return player; }
+    /** @return the Games Map with information about where the player is and can go */
+    public  GameMap getMap() { return map; }
+    /** @return the computer password*/
+    public String getComputerPassword() {
+        return computerPassword;
+    }
+    public void updateAllowedCommands(){
+        allowedCommands.clear();
+        allowedCommands.addAll(this.map.getCurrentRoom().getAllowedCommands());
+        allowedCommands.addAll(GLOBAL_COMMANDS);
+    }
+    public List<String> getAllowedCommands() {
+        return allowedCommands;
+    }
+}

+ 125 - 0
src/main/java/cz/davza/studentadventure/objects/GameMap.java

@@ -0,0 +1,125 @@
+package cz.davza.studentadventure.objects;
+
+import cz.davza.studentadventure.base.ObservableBase;
+import cz.davza.studentadventure.base.RoomBase;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.rooms.*;
+
+import java.util.HashMap;
+import java.util.Map;
+/**
+ * Manages the world map — the set of rooms and transitions between them.
+ *
+ * <p>Extends {@link ObservableBase} so listeners can react to
+ * {@link CommandState#ROOM_CHANGED} events (e.g. a GUI updating its room display).</p>
+ *
+ * <h2>Room transitions</h2>
+ * <p>Room changes are two-phase to support entry conditions and sub-interactions:</p>
+ * <ol>
+ *   <li>{@link #setNextRoom(RoomBase)} — stages the destination (called by
+ *       {@link cz.davza.studentadventure.commands.WalkCommand}).</li>
+ *   <li>{@link #commitRoomChange()} — atomically swaps current → next and fires
+ *       the {@link CommandState#ROOM_CHANGED} notification (called by the game loop
+ *       in {@link Game#sendCommand(String)} once any entry checks have passed).</li>
+ * </ol>
+ *
+ * @see RoomBase
+ * @see Game
+ */
+public class GameMap extends ObservableBase {
+    private RoomBase currentRoom;
+    private RoomBase nextRoom;
+    private Map<String, RoomBase> rooms;
+
+    /**
+     * Constructs all rooms, wires up their exits, and sets the starting room.
+     * Called once from {@link GameData#GameData(Player, GameMap, GameTime)}.
+     *
+     * @param gameData the game context passed to each room constructor
+     *                 so rooms can access the computer password and other shared data
+     */
+    public void setupMap(GameData gameData) {
+        // initialize map
+        this.rooms  = new HashMap<>();
+        //        Base Rooms
+        RoomBase bedroom     = new BedroomRoom(gameData);
+        RoomBase hallway     = new HallwayRoom(gameData);
+        RoomBase studyRoom   = new StudyRoomRoom(gameData);
+        RoomBase computerLab = new ComputerLabRoom(gameData);
+        RoomBase classroom   = new ClassroomRoom(gameData);
+        RoomBase shop        = new ShopRoom(gameData);
+        RoomBase examRoom    = new ExamRoomRoom(gameData);
+
+        // wire up exits
+        bedroom.addExit(hallway);
+        hallway.addExit(bedroom);
+
+        hallway.addExit( studyRoom);
+        studyRoom.addExit( hallway);
+
+        studyRoom.addExit( computerLab);
+        computerLab.addExit( studyRoom);
+
+        computerLab.addExit(shop);
+        shop.addExit(computerLab);
+
+        hallway.addExit( classroom);
+        classroom.addExit( hallway);
+
+        classroom.addExit( examRoom);
+        examRoom.addExit( classroom);
+        // Add to list
+        rooms.put("bedroom", bedroom);
+        rooms.put("hallway", hallway);
+        rooms.put("studyroom", studyRoom);
+        rooms.put("computerlab", computerLab);
+        rooms.put("classroom", classroom);
+        rooms.put("shop", shop);
+        rooms.put("examroom", examRoom);
+
+        // Set starting room
+        this.currentRoom = bedroom;
+    }
+    /**
+     * Returns whether a room change is pending (i.e. {@link #setNextRoom} has been
+     * called but {@link #commitRoomChange} has not yet been called).
+     *
+     * @return {@code true} if a destination room is staged
+     */
+    public boolean hasPendingRoomChange() { return nextRoom != null; }
+    /**
+     * Stages the given room as the destination for the next room transition.
+     * Pass {@code null} to cancel a pending transition.
+     *
+     * @param room the destination room, or {@code null} to cancel
+     */
+    public void setNextRoom(RoomBase room) { this.nextRoom = room; }
+    /**
+     * Returns the staged destination room without committing the transition.
+     *
+     * @return the pending next room, or {@code null} if none is staged
+     */
+    public RoomBase getNextRoom()
+    {
+        return nextRoom;
+    }
+    /**
+     * Returns the room the player is currently in.
+     *
+     * @return the current {@link RoomBase}
+     */
+    public RoomBase getCurrentRoom() { return currentRoom; }
+    /**
+     * Commits the pending room transition: moves the player into the staged room,
+     * clears the pending destination, and notifies observers with
+     * {@link CommandState#ROOM_CHANGED}.
+     *
+     * <p>Should only be called when {@link #hasPendingRoomChange()} returns {@code true}.</p>
+     */
+    public void commitRoomChange() {
+        currentRoom = nextRoom;
+        nextRoom    = null;
+        notifyObservers(CommandState.ROOM_CHANGED);
+    }
+
+}

+ 71 - 0
src/main/java/cz/davza/studentadventure/objects/GameTime.java

@@ -0,0 +1,71 @@
+package cz.davza.studentadventure.objects;
+/**
+ * Tracks in-game time as a day/hour pair.
+ *
+ * <p>Time starts at Day {@value #START_DAY}, {@value #START_HOUR}:00 and the exam
+ * deadline is Day {@value #DEADLINE_DAY} at {@value #DEADLINE_HOUR}:00. Hours roll
+ * over into the next day automatically. Once {@link #isPastDeadline()} returns
+ * {@code true} the game loop ends in defeat.</p>
+ */
+public class GameTime {
+    private int day;
+    private int hour;
+
+    private static final int START_DAY  = 1;
+    private static final int START_HOUR = 8;
+    private static final int DEADLINE_DAY  = 7;
+    private static final int DEADLINE_HOUR = 9;
+    private static final int HOURS_IN_DAY  = 24;
+    /**
+     * Initialises the clock to Day {@value #START_DAY}, {@value #START_HOUR}:00.
+     */
+    public GameTime() {
+        this.day  = START_DAY;
+        this.hour = START_HOUR;
+    }
+    /**
+     * Advances the clock by the given number of hours, rolling over into the next
+     * day as necessary.
+     *
+     * @param hours the number of hours to advance (must be positive)
+     */
+    public void advanceHours(int hours) {
+        this.hour += hours;
+        while (this.hour >= HOURS_IN_DAY) {
+            this.hour -= HOURS_IN_DAY;
+            this.day++;
+        }
+    }
+    /**
+     * Jumps the clock forward to {@value #START_HOUR}:00 on the next day.
+     * Used when the player sleeps in bed.
+     */
+    public void advanceToNextMorning() {
+        this.day++;
+        this.hour = START_HOUR;
+    }
+    /**
+     * Returns whether the exam deadline has passed.
+     *
+     * @return {@code true} if the current time is on or after Day {@value #DEADLINE_DAY}
+     *         at {@value #DEADLINE_HOUR}:00
+     */
+    public boolean isPastDeadline() {
+        if (day > DEADLINE_DAY) return true;
+        if (day == DEADLINE_DAY && hour >= DEADLINE_HOUR) return true;
+        return false;
+    }
+    /** @return the current in-game day number */
+    public int getDay()  { return day; }
+    /** @return the current in-game hour (0–23) */
+    public int getHour() { return hour; }
+    /**
+     * Returns a human-readable time string in the format {@code "Day D, HH:00"}.
+     *
+     * @return formatted time string
+     */
+    @Override
+    public String toString() {
+        return String.format("Day %d, %02d:00", day, hour);
+    }
+}

+ 74 - 0
src/main/java/cz/davza/studentadventure/objects/ItemStack.java

@@ -0,0 +1,74 @@
+package cz.davza.studentadventure.objects;
+
+import cz.davza.studentadventure.interfaces.IItem;
+/**
+ * A counted slot holding one or more instances of the same {@link IItem}.
+ *
+ * <p>The maximum number of items that can be held in a single stack is determined
+ * by {@link IItem#getBulk()} on the contained item. The collection layer
+ * ({@link cz.davza.studentadventure.base.ItemCollectionBase}) is responsible for
+ * enforcing this limit; {@code ItemStack} itself does not clamp {@link #Add}.</p>
+ */
+public class ItemStack{
+    private final IItem item;
+    private int amount;
+    /**
+     * Creates a stack containing one instance of the given item.
+     *
+     * @param item the item type this stack holds
+     */
+    public ItemStack(IItem item){
+        this.item = item;
+        this.amount = 1;
+    }
+    /**
+     * Creates a stack containing the specified number of items.
+     *
+     * @param item   the item type this stack holds
+     * @param amount the initial quantity (must be ≥ 1)
+     * @throws IllegalArgumentException if {@code amount} is less than 1
+     */
+    public ItemStack(IItem item, int amount){
+        if (amount <= 0) throw new IllegalArgumentException("Cannot hold less than 1 item");
+        this.item = item;
+        this.amount = amount;
+
+    }
+    /**
+     * Increases the stack count by the given amount.
+     * @param amount the number of items to add
+     */
+    public void Add(int amount){
+        this.amount += amount;
+    }
+    /**
+     * Decreases the stack count by the given amount.
+     * No bounds checking is performed; callers should not reduce below zero.
+     *
+     * @param amount the number of items to remove
+     */
+    public void Remove(int amount){
+        this.amount -= amount;
+    }
+    /**
+     * Returns the item type held in this stack.
+     *
+     * @return the item
+     */
+    public IItem getItem(){
+        return this.item;
+    }
+    /**
+     * Returns the current quantity of items in this stack.
+     *
+     * @return the stack count
+     */
+    public int getAmount(){
+        return this.amount;
+    }
+
+    @Override
+    public String toString() {
+        return item.getCode() + " x " + amount;
+    }
+}

+ 127 - 0
src/main/java/cz/davza/studentadventure/objects/Player.java

@@ -0,0 +1,127 @@
+package cz.davza.studentadventure.objects;
+
+import cz.davza.studentadventure.enums.DeprivationState;
+
+import java.util.Random;
+
+/**
+ * Represents the player character and their current state.
+ *
+ * <p>Holds all mutable stats ({@link #getIntelligence()}, {@link #getEnergy()},
+ * {@link #getStress()}), the player's money, their inventory, and sleep-deprivation
+ * tracking via {@link #hoursAwake}.</p>
+ *
+ * <p>The deprivation system multiplies the cost of actions like studying once
+ * the player has been awake too long. See {@link #getDeprivationMultiplier()}.</p>
+ */
+public class Player {
+    private final String name;
+    private final PlayerInventory inventory;
+    private final Wallet wallet;
+    private final String id;
+    private final PlayerStat intelligence;
+    private final PlayerStat energy;
+    private final PlayerStat stress;
+
+    private static final int    TIRED_THRESHOLD    = 14;
+    private static final int    DEPRIVED_THRESHOLD = 20;
+    private static final double MAX_MULTIPLIER     = 3.0;
+    private static final int STARTING_MONEY = 50;
+
+    private int hoursAwake = 0;
+    /**
+     * Creates a new player with default starting stats and an empty inventory.
+     *
+     * @param name          the player's display name
+     * @param inventorySize the number of inventory slots available to the player
+     */
+    public Player(String name, int inventorySize) {
+        this.name = name;
+        this.inventory = new PlayerInventory(inventorySize);
+        this.id = name + new Random().nextInt(100);
+        this.intelligence = new PlayerStat("Intelligence", 5);
+        this.energy = new PlayerStat("Energy", 100);
+        this.stress = new PlayerStat("Stress", 0);
+        this.wallet = new Wallet(STARTING_MONEY);
+    }
+
+    /** @return the player's inventory */
+    public PlayerInventory getInventory() { return inventory; }
+
+    /** @return the intelligence stat */
+    public PlayerStat getIntelligence() { return intelligence; }
+    /** @return the energy stat */
+    public PlayerStat getEnergy() { return energy; }
+    /** @return the player's wallet */
+    public Wallet getWallet() { return wallet; }
+    /**
+     * @return the stress stat
+     */
+    public PlayerStat getStress() { return stress; }
+    /** @return the player's display name */
+    public String getName() { return name; }
+    /**
+     * Prints a formatted summary of the player's stats to {@code System.out}.
+     */
+    public String printStats() {
+        return "=== " + name + " ===\n" +
+                intelligence + "\n" +
+                energy + "\n" +
+                stress + "\n" +
+                "Money: " + this.wallet.getMoney() + " coins\n";
+    }
+    /**
+     * Increments the consecutive hours-awake counter.
+     * Passing a negative value can be used to reduce it (e.g. after sleeping in class).
+     *
+     * @param hours the number of hours to add (may be negative)
+     */
+    public void addHoursAwake(int hours) {
+        hoursAwake += hours;
+    }
+    /**
+     * Resets the hours-awake counter to zero. Called after a full night's sleep.
+     */
+    public void resetHoursAwake() {
+        hoursAwake = 0;
+    }
+    /**
+     * Returns the player's current sleep-deprivation state based on
+     * how many consecutive hours they have been awake.
+     *
+     * @return {@link DeprivationState#NORMAL}, {@link DeprivationState#TIRED},
+     *         or {@link DeprivationState#DEPRIVED}
+     */
+    public DeprivationState getDeprivationState() {
+        if (hoursAwake >= DEPRIVED_THRESHOLD) return DeprivationState.DEPRIVED;
+        if (hoursAwake >= TIRED_THRESHOLD)    return DeprivationState.TIRED;
+        return DeprivationState.NORMAL;
+    }
+    /**
+     * Returns a cost multiplier applied to tiring actions (such as studying)
+     * based on the current deprivation state.
+     *
+     * <ul>
+     *   <li>{@link DeprivationState#NORMAL}   → {@code 1.0×}</li>
+     *   <li>{@link DeprivationState#TIRED}     → {@code 1.3×}</li>
+     *   <li>{@link DeprivationState#DEPRIVED}  → {@code 1.3×} + {@code 0.2×} per extra hour,
+     *       capped at {@value #MAX_MULTIPLIER}×</li>
+     * </ul>
+     *
+     * @return the deprivation cost multiplier (always ≥ 1.0)
+     */
+    public double getDeprivationMultiplier() {
+        switch (getDeprivationState()) {
+            case TIRED:
+                return 1.3;
+            case DEPRIVED:
+                int hoursDeprived = hoursAwake - DEPRIVED_THRESHOLD;
+                return Math.min(1.3 + (hoursDeprived * 0.2), MAX_MULTIPLIER);
+            default:
+                return 1.0;
+        }
+    }
+    public String getId() {
+        return id;
+    }
+}

+ 82 - 0
src/main/java/cz/davza/studentadventure/objects/PlayerInventory.java

@@ -0,0 +1,82 @@
+package cz.davza.studentadventure.objects;
+
+import cz.davza.studentadventure.base.ItemCollectionBase;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.interfaces.IItem;
+import cz.davza.studentadventure.interfaces.IObservable;
+import cz.davza.studentadventure.interfaces.IObserver;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+/**
+ * The player's personal item inventory, with Observer support for change notifications.
+ *
+ * <p>Extends {@link ItemCollectionBase} with the full slot-based stacking logic,
+ * and additionally implements {@link IObservable} so that external listeners
+ * (such as a GUI panel) can react whenever the inventory contents change.</p>
+ *
+ * <p>Every call to {@link #Add} or {@link #Remove} automatically fires a
+ * {@link CommandState#INVENTORY_CHANGE} notification to all registered observers
+ * after delegating the operation to the superclass.</p>
+ *
+ * @see cz.davza.studentadventure.base.ItemCollectionBase
+ * @see IObservable
+ * @see IObserver
+ */
+public class PlayerInventory extends ItemCollectionBase<IItem> implements IObservable {
+    private final Map<CommandState, Set<IObserver>> observers = new HashMap<>();
+    /**
+     * Creates a new inventory with the given number of bulk-unit capacity.
+     *
+     * @param size the total bulk capacity of this inventory
+     */
+    public PlayerInventory(int size) {
+        super(size);
+    }
+    /**
+     * {@inheritDoc}
+     *
+     * <p>After the item is added (or partially added) by the superclass,
+     * all observers registered for {@link CommandState#INVENTORY_CHANGE} are notified.</p>
+     */
+    @Override
+    public int Add(IItem item, int count){
+        int result = super.Add(item, count);
+        notifyObservers(CommandState.INVENTORY_CHANGE);
+        return result;
+    }
+    /**
+     * {@inheritDoc}
+     *
+     * <p>After the item is removed by the superclass,
+     * all observers registered for {@link CommandState#INVENTORY_CHANGE} are notified.</p>
+     */
+    @Override
+    public void Remove(String itemCode, int count) {
+        super.Remove(itemCode, count);
+        notifyObservers(CommandState.INVENTORY_CHANGE);
+    }
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void addObserver(CommandState observedstate, IObserver o) {
+        if  (!observers.containsKey(observedstate)) observers.put(observedstate, new HashSet<>());
+        observers.get(observedstate).add(o);
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void notifyObservers(CommandState notifiedState) {
+        if   (!observers.containsKey(notifiedState)) return;
+
+        for (IObserver observer : observers.get(notifiedState)) {
+            observer.update();
+        }
+    }
+}

+ 67 - 0
src/main/java/cz/davza/studentadventure/objects/PlayerStat.java

@@ -0,0 +1,67 @@
+package cz.davza.studentadventure.objects;
+/**
+ * A single bounded numeric stat for the player (e.g. energy, stress, intelligence).
+ *
+ * <p>Values are always clamped to the range [{@value #MIN}, {@value #MAX}].
+ * Use {@link #increase(int)} and {@link #decrease(int)} to modify the value;
+ * both internally call the same clamp so negative amounts are handled safely
+ * by whichever direction is semantically appropriate.</p>
+ */
+public class PlayerStat {
+    private final String name;
+    private int value;
+    private static final int MIN = 0;
+    private static final int MAX = 100;
+    /**
+     * Creates a new stat with the given name and initial value.
+     * The initial value is clamped to [{@value #MIN}, {@value #MAX}].
+     *
+     * @param name         the stat's display name (e.g. {@code "Energy"})
+     * @param initialValue the starting value
+     */
+    public PlayerStat(String name, int initialValue) {
+        this.name = name;
+        this.value = clamp(initialValue);
+    }
+    /**
+     * Increases the stat by {@code amount}, clamped at {@value #MAX}.
+     *
+     * @param amount the amount to add (should be positive)
+     */
+    public void increase(int amount) {
+        this.value = clamp(this.value + amount);
+    }
+    /**
+     * Decreases the stat by {@code amount}, clamped at {@value #MIN}.
+     *
+     * @param amount the amount to subtract (should be positive)
+     */
+    public void decrease(int amount) {
+        this.value = clamp(this.value - amount);
+    }
+    /**
+     * Returns the current value of this stat.
+     *
+     * @return a value in the range [{@value #MIN}, {@value #MAX}]
+     */
+    public int getValue() { return value; }
+    /**
+     * Returns the display name of this stat.
+     *
+     * @return the stat name
+     */
+    public String getName() { return name; }
+
+    private int clamp(int val) {
+        return Math.max(MIN, Math.min(MAX, val));
+    }
+    /**
+     * Returns a human-readable representation in the form {@code "Name: value/100"}.
+     *
+     * @return formatted stat string
+     */
+    @Override
+    public String toString() {
+        return name + ": " + value + "/100";
+    }
+}

+ 24 - 0
src/main/java/cz/davza/studentadventure/objects/RoomItems.java

@@ -0,0 +1,24 @@
+package cz.davza.studentadventure.objects;
+
+import cz.davza.studentadventure.base.ItemCollectionBase;
+import cz.davza.studentadventure.interfaces.IItem;
+/**
+ * A fixed-size item collection representing the items present in a room.
+ *
+ * <p>Thin concrete subclass of {@link ItemCollectionBase} — all behaviour is
+ * inherited. The capacity passed to the constructor determines how many bulk
+ * units of items the room can hold at once.</p>
+ *
+ * @see ItemCollectionBase
+ * @see cz.davza.studentadventure.base.RoomBase
+ */
+public class RoomItems extends ItemCollectionBase<IItem> {
+    /**
+     * Creates a new room item collection with the given bulk capacity.
+     *
+     * @param size the total bulk capacity of this room's item storage
+     */
+    public RoomItems(int size) {
+        super(size);
+    }
+}

+ 56 - 0
src/main/java/cz/davza/studentadventure/objects/Wallet.java

@@ -0,0 +1,56 @@
+package cz.davza.studentadventure.objects;
+/**
+ * Holds and manages the player's coin balance.
+ *
+ * <p>Provides simple earn/spend operations. {@link #spendMoney(int)} performs a
+ * balance check and returns {@code false} without modifying the balance if the
+ * player cannot afford the amount — callers should check the return value before
+ * assuming the transaction succeeded.</p>
+ *
+ * @see cz.davza.studentadventure.objects.Player
+ */
+public class Wallet {
+    private int money;
+    /**
+     * Creates a wallet with a zero starting balance.
+     */
+    public Wallet(){}
+    /**
+     * Creates a wallet with the given starting balance.
+     *
+     * @param startingAmount the initial coin balance (must be ≥ 0)
+     */
+    public Wallet(int startingAmount){
+        this.money = startingAmount;
+    }
+    /**
+     * Returns the current coin balance.
+     *
+     * @return the number of coins held
+     */
+    public int getMoney() {
+        return money;
+    }
+    /**
+     * Adds coins to the balance (e.g. from picking up money or stealing).
+     *
+     * @param amount the number of coins to add (must be ≥ 0)
+     */
+    public void earnMoney(int amount){
+        this.money += amount;
+    }
+    /**
+     * Deducts coins from the balance if the player can afford it.
+     *
+     * @param amount the number of coins to deduct
+     * @return {@code true} if the transaction succeeded; {@code false} if the
+     *         balance was insufficient (balance is unchanged in that case)
+     */
+    public boolean spendMoney(int amount){
+        if (amount > this.money){
+            return false;
+        }
+        this.money -= amount;
+        return true;
+    }
+}

+ 32 - 0
src/main/java/cz/davza/studentadventure/outputsinks/TextOutputSink.java

@@ -0,0 +1,32 @@
+package cz.davza.studentadventure.outputsinks;
+
+import cz.davza.studentadventure.interfaces.IOutputSink;
+/**
+ * {@link IOutputSink} implementation that writes output to {@code System.out}.
+ *
+ * <p>Text is buffered internally and printed in a single {@link System#out} call
+ * on {@link #flush()}, keeping output atomic and avoiding interleaved partial lines.</p>
+ *
+ * @see IOutputSink
+ * @see GuiOutputSink
+ */
+public class TextOutputSink implements IOutputSink {
+    private final StringBuilder buffer = new StringBuilder();
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void buffer(String text) {
+        buffer.append(text);
+    }
+    /**
+     * {@inheritDoc}
+     * Prints the buffered content to {@code System.out} and clears the buffer.
+     */
+
+    @Override
+    public void flush() {
+        System.out.print(buffer.toString());
+        buffer.setLength(0);
+    }
+}

+ 33 - 0
src/main/java/cz/davza/studentadventure/rooms/BedroomRoom.java

@@ -0,0 +1,33 @@
+package cz.davza.studentadventure.rooms;
+
+import cz.davza.studentadventure.Items.Bed;
+import cz.davza.studentadventure.Items.Money;
+import cz.davza.studentadventure.base.RoomBase;
+import cz.davza.studentadventure.objects.GameData;
+
+/**
+ * The player's student bedroom — the starting room of the game.
+ *
+ * <p>Contains the player's bed, a sticky note hinting at the computer lab password,
+ * and a small amount of random loose change on the floor. The {@code sleep} command
+ * is available here, fully restoring stats and advancing to the next morning.</p>
+ *
+ * @see cz.davza.studentadventure.commands.SleepCommand
+ */
+public class BedroomRoom extends RoomBase {
+    /**
+     * Constructs the bedroom, placing a sticky note, bed, and random money.
+     *
+     *
+     */
+    public BedroomRoom(GameData gameContext) {
+        super("Bedroom",
+                "bedroom",
+                "Your student bedroom. Clothes on the floor, a messy study desk and an inviting bed.",
+                20);
+        allowedCommands.add("sleep");
+        allowedCommands.add("study");
+        getItems().Add(new Bed(), 1);
+        getItems().Add(new Money(10, 30), 1); // Random loose change
+    }
+}

+ 53 - 0
src/main/java/cz/davza/studentadventure/rooms/ClassroomRoom.java

@@ -0,0 +1,53 @@
+package cz.davza.studentadventure.rooms;
+
+import cz.davza.studentadventure.Items.StickyNote;
+import cz.davza.studentadventure.base.RoomBase;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.Player;
+/**
+ * A university lecture hall. Entry requires the player to be carrying a book.
+ *
+ * <p>If the player attempts to enter without a {@code book} in their inventory,
+ * the professor blocks them at the door and inflicts a stress penalty.
+ * Sleeping in class is allowed but carries a {@code 30%} chance of a game-over.</p>
+ *
+ * @see cz.davza.studentadventure.commands.SleepCommand
+ */
+public class ClassroomRoom extends RoomBase {
+    /** Creates the classroom room. */
+    public ClassroomRoom(GameData gameContext) {
+        super("Classroom",
+                "classroom",
+                "Rows of seats face a whiteboard. The professor drones on at the front. " +
+                        "Several students look just as tired as you.",
+                10);
+        getItems().Add(new StickyNote("Computer lab password: " + gameContext.getComputerPassword()), 1);
+        allowedCommands.add("sleep");
+    }
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Checks the player's inventory for a {@code book}. If none is found,
+     * prints a refusal message, applies a stress penalty, and cancels the room
+     * change via {@code context.getMap().setNextRoom(null)}.</p>
+     *
+     * @return
+     */
+    @Override
+    public CommandResult onEnter(GameData context) {
+        Player player = context.getPlayer();
+        boolean hasBook = player.getInventory().GetByItemCode("book") != null;
+
+        if (!hasBook) {
+            player.getStress().increase(15);
+            context.getMap().setNextRoom(null);
+            return CommandResult.failed(
+                    "The professor stops you at the door.\n" +
+                            "\"No textbook? Get out of my classroom!\"\n" +
+                            "Stress +15. " + player.getStress());
+        }
+
+        return CommandResult.success("");
+    }
+}

+ 33 - 0
src/main/java/cz/davza/studentadventure/rooms/ComputerLabRoom.java

@@ -0,0 +1,33 @@
+package cz.davza.studentadventure.rooms;
+
+import cz.davza.studentadventure.Items.Book;
+import cz.davza.studentadventure.base.RoomBase;
+import cz.davza.studentadventure.Items.Computer;
+import cz.davza.studentadventure.objects.GameData;
+
+/**
+ * A university computer lab containing password-protected workstations.
+ *
+ * <p>The {@code study} command is available here. Interacting with the
+ * {@link Computer} opens a sub-loop where the player can study using the
+ * machine after providing the correct password.</p>
+ *
+ * @see Computer
+ */
+public class ComputerLabRoom extends RoomBase {
+    /**
+     * Constructs the computer lab with a computer and a spare book.
+     *
+     * @param gameContext the game data for option extractions
+     */
+    public ComputerLabRoom(GameData gameContext) {
+        super("Computer Lab",
+                "computerlab",
+                "Rows of university computers hum quietly. " +
+                        "A sign on the wall reads: 'Authorised users only.'",
+                10);
+        allowedCommands.add("study");
+        getItems().Add(new Computer(gameContext.getComputerPassword()), 1);
+        getItems().Add(new Book(),1);
+    }
+}

+ 66 - 0
src/main/java/cz/davza/studentadventure/rooms/ExamRoomRoom.java

@@ -0,0 +1,66 @@
+package cz.davza.studentadventure.rooms;
+
+import cz.davza.studentadventure.base.RoomBase;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.interfaces.ICommandable;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+
+import java.util.Scanner;
+
+/**
+ * The final exam hall — the endgame room of the adventure.
+ *
+ * <p>Only the {@code takeexam} command is available here. Walking in and
+ * using it triggers the win/lose evaluation based on the player's accumulated
+ * intelligence and current stress level.</p>
+ *
+ * @see cz.davza.studentadventure.commands.TakeExamCommand
+ */
+public class ExamRoomRoom extends RoomBase implements ICommandable {
+    /** Creates the exam room. */
+    public ExamRoomRoom(GameData gameContext) {
+        super("Exam Room",
+                "examroom",
+                "A silent hall filled with desks. This is it. " +
+                        "The exam paper sits face down in front of you.",
+                2);
+        allowedCommands.add("takeexam");
+    }
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Checks the player's inventory for a {@code book}. If none is found,
+     * prints a refusal message, applies a stress penalty, and cancels the room
+     * change via {@code context.getMap().setNextRoom(null)}.</p>
+     *
+     * @return
+     */
+
+        @Override
+        public CommandResult onEnter(GameData context) {
+            return CommandResult.actionPending(this,
+                    "As you enter the exam room a teacher stops you.\n" +
+                            "'To take the test you must tell us your student ID'\n" +
+                            "Enter student ID (or 'leave' to walk away):");
+        }
+
+        @Override
+        public CommandResult handleCommand(String input, GameData context) {
+            String studentId = context.getPlayer().getId();
+
+            if (input.equalsIgnoreCase("leave")) {
+                context.getMap().setNextRoom(null);
+                return CommandResult.actionResumed(
+                        "Since you do not know your student ID you may not take the test.\n" +
+                                "You leave the exam room.");
+            }
+
+            if (input.equalsIgnoreCase(studentId)) {
+                return CommandResult.actionResumed("You confidently walk into the exam room.");
+            }
+
+            return CommandResult.failed("No I do not believe that is you.\nEnter student ID (or 'leave' to walk away):");
+        }
+
+}

+ 24 - 0
src/main/java/cz/davza/studentadventure/rooms/HallwayRoom.java

@@ -0,0 +1,24 @@
+package cz.davza.studentadventure.rooms;
+
+import cz.davza.studentadventure.Items.Money;
+import cz.davza.studentadventure.Items.StickyNote;
+import cz.davza.studentadventure.base.RoomBase;
+import cz.davza.studentadventure.objects.GameData;
+
+/**
+ * The central hallway connecting most rooms in the building.
+ *
+ * <p>Contains a sticky note and a small amount of loose change on the floor.
+ * Acts as the main hub for navigation.</p>
+ */
+public class HallwayRoom extends RoomBase {
+    /** Creates the hallway with its default items. */
+    public HallwayRoom(GameData gameContext) {
+        super("Hallway",
+                "hallway",
+                "A busy university hallway. Students rush past you in every direction.",
+                5);
+        getItems().Add(new StickyNote("Dr. Doofenshmirts stinks"),1);
+        getItems().Add(new Money(5),1);
+    }
+}

+ 23 - 0
src/main/java/cz/davza/studentadventure/rooms/ShopRoom.java

@@ -0,0 +1,23 @@
+package cz.davza.studentadventure.rooms;
+
+import cz.davza.studentadventure.base.RoomBase;
+import cz.davza.studentadventure.objects.GameData;
+
+/**
+ * The campus convenience shop.
+ *
+ * <p>The {@code buy} command is available here. Currently sells energy drinks.</p>
+ *
+ * @see cz.davza.studentadventure.commands.BuyCommand
+ */
+public class ShopRoom extends RoomBase {
+    /** Creates the shop room. */
+    public ShopRoom(GameData gameContext) {
+        super("Campus Shop",
+                "shop",
+                "A small convenience shop. A fridge hums in the corner, " +
+                        "stocked with energy drinks. The cashier looks bored.",
+                30);
+        allowedCommands.add("buy");
+    }
+}

+ 29 - 0
src/main/java/cz/davza/studentadventure/rooms/StudyRoomRoom.java

@@ -0,0 +1,29 @@
+package cz.davza.studentadventure.rooms;
+
+import cz.davza.studentadventure.Items.Book;
+import cz.davza.studentadventure.Items.StickyNote;
+import cz.davza.studentadventure.base.RoomBase;
+import cz.davza.studentadventure.objects.GameData;
+
+/**
+ * A quiet study area where students work undisturbed — and leave their bags unattended.
+ *
+ * <p>The {@code study} and {@code steal} commands are available here.
+ * Contains two books and a sticky note.</p>
+ *
+ * @see cz.davza.studentadventure.commands.StealCommand
+ */
+public class StudyRoomRoom extends RoomBase {
+    /** Creates the study room with default items. */
+    public StudyRoomRoom(GameData gameContext) {
+        super("Study Room",
+                "studyroom",
+                "A quiet study room. A few students sit with their heads buried in books. " +
+                        "Someone left their bag unattended on the chair next to you.",
+                10);
+        allowedCommands.add("study");
+        allowedCommands.add("steal");
+        getItems().Add(new Book(), 2);
+        getItems().Add(new StickyNote("Amanda Likes Brad ❤️"),1);
+    }
+}

+ 104 - 0
src/main/resources/cz/davza/studentadventure/main/help.html

@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" >
+    <title>Student Adventure — Help</title>
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            background-color: #1a1a2e;
+            color: #e0e0e0;
+            margin: 0;
+            padding: 24px;
+        }
+        h1 {
+            color: #a0c4ff;
+            font-size: 22px;
+            border-bottom: 2px solid #a0c4ff;
+            padding-bottom: 8px;
+            margin-bottom: 20px;
+        }
+        h2 {
+            color: #bdb2ff;
+            font-size: 16px;
+            margin-top: 24px;
+            margin-bottom: 8px;
+        }
+        table {
+            width: 100%;
+            border-collapse: collapse;
+            margin-bottom: 16px;
+        }
+        td {
+            padding: 8px 12px;
+            border-bottom: 1px solid #2e2e4e;
+            font-size: 14px;
+            vertical-align: top;
+        }
+        td:first-child {
+            font-family: monospace;
+            color: #a0c4ff;
+            white-space: nowrap;
+            width: 220px;
+        }
+        tr:hover td {
+            background-color: #2e2e4e;
+        }
+        .note {
+            font-size: 12px;
+            color: #888;
+            margin-top: 24px;
+            border-top: 1px solid #2e2e4e;
+            padding-top: 12px;
+        }
+    </style>
+</head>
+<body>
+
+<h1>Student Adventure — Help</h1>
+<p>You have until <strong>Day 7 at 09:00</strong> to prepare for and pass your final exam. Manage your energy, stress, and intelligence wisely.</p>
+
+<h2>Movement</h2>
+<table>
+    <tr><td>walk &lt;exit&gt;</td><td>Move to a connected room in the given direction.</td></tr>
+</table>
+
+<h2>Inventory</h2>
+<table>
+    <tr><td>pickup &lt;item&gt; &lt;n&gt;</td><td>Pick up n of an item from the current room.</td></tr>
+    <tr><td>drop &lt;item&gt; [n]</td><td>Drop n of the named item from your inventory (default 1). Example: drop book 2</td></tr>
+    <tr><td>consume &lt;item&gt;</td><td>Consume one of the named item from your inventory. Example: consume energy_drink</td></tr>
+    <tr><td>inventory</td><td>Show your current inventory.</td></tr>
+</table>
+
+<h2>Actions</h2>
+<table>
+    <tr><td>interact &lt;item&gt;</td><td>Interact with an item in the current room.</td></tr>
+    <tr><td>study &lt;hours&gt;</td><td>Study for the given number of hours. Available in bedroom, study room, and computer lab.</td></tr>
+    <tr><td>sleep</td><td>Sleep to restore energy and reduce stress. Available in bedroom and classroom.</td></tr>
+    <tr><td>steal</td><td>Attempt to steal money. Study room only. Risky.</td></tr>
+    <tr><td>buy &lt;item&gt;</td><td>Buy an item from the shop.</td></tr>
+    <tr><td>takeexam</td><td>Take the final exam. Exam room only.</td></tr>
+</table>
+
+<h2>Information</h2>
+<table>
+    <tr><td>stats</td><td>Show your current stats — intelligence, energy, stress, and money.</td></tr>
+    <tr><td>time</td><td>Show the current in-game time and day.</td></tr>
+    <tr><td>describe</td><td>Describe the current room and its contents.</td></tr>
+    <tr><td>availablecommands</td><td>List commands available in your current location.</td></tr>
+    <tr><td>studentId</td><td>Display your student ID. Computer lab only.</td></tr>
+</table>
+
+<h2>Game</h2>
+<table>
+    <tr><td>quit</td><td>Give up and end the game.</td></tr>
+</table>
+
+<div class="note">
+    Tip: Keep an eye on your energy — studying while exhausted costs significantly more and increases stress faster. Sleep before you burn out.
+</div>
+
+</body>
+</html>

+ 11 - 0
src/main/resources/simplelogger.properties

@@ -0,0 +1,11 @@
+# System-wide default log level (Options: trace, debug, info, warn, error, off)
+# Setting this to warn ensures you only see critical problems, not setup notices.
+org.slf4j.simpleLogger.defaultLogLevel=warn
+
+# Explicitly target and silence the reflections framework logs
+org.slf4j.simpleLogger.log.org.reflections=warn
+
+# Optional: Clear the logging layout so it doesn't print timestamps or thread names
+org.slf4j.simpleLogger.showDateTime=false
+org.slf4j.simpleLogger.showThreadName=false
+org.slf4j.simpleLogger.showLogName=false

+ 52 - 0
test/cz/davza/studentadventure/TestHelpers.java

@@ -0,0 +1,52 @@
+package cz.davza.studentadventure;
+
+import cz.davza.studentadventure.base.CommandBase;
+import cz.davza.studentadventure.interfaces.IOutputSink;
+import cz.davza.studentadventure.managers.CommandManager;
+import cz.davza.studentadventure.objects.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Shared test utilities.
+ */
+public class TestHelpers {
+
+    /** Captures all writeLine calls for assertion. */
+    public static class CapturingOutputSink implements IOutputSink {
+        private final StringBuilder buffer = new StringBuilder();
+        @Override public void writeLine(String line) { buffer.append(line).append("\n"); }
+        @Override public void buffer(String text) {}
+        @Override public void flush() {}
+        public String getOutput()              { return buffer.toString(); }
+        public void clear()                    { buffer.setLength(0); }
+        public boolean contains(String text)   { return buffer.toString().contains(text); }
+    }
+
+    /** Creates a fully wired GameData starting in the bedroom. */
+    public static GameData buildGameData() {
+        Player   player = new Player("TestStudent", 20);
+        GameMap  map    = new GameMap();
+        GameTime time   = new GameTime();
+        return new GameData(player, map, time);
+    }
+
+    /**
+     * Parses and executes a command string against the given context,
+     * bypassing the allowed-commands check so commands can be tested
+     * in isolation regardless of which room the player is in.
+     */
+    public static CommandResult execute(String input, GameData context) {
+        // Build an unrestricted allowed list containing everything
+        List<String> allAllowed = List.of(
+            "study","sleep","walk","pickup","drop","consume","interact",
+            "steal","buy","takeexam","inventory","stats","time","describe",
+            "help","availablecommands","studentid"
+        );
+        CommandBase cmd = CommandManager.parseCommand(input, allAllowed);
+        if (cmd == null) throw new IllegalArgumentException("Unknown command: " + input);
+        return cmd.Execute(context);
+    }
+}

+ 85 - 0
test/cz/davza/studentadventure/commands/BuyCommandTest.java

@@ -0,0 +1,85 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.Items.EnergyDrink;
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.Player;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static cz.davza.studentadventure.TestHelpers.execute;
+import static org.junit.jupiter.api.Assertions.*;
+
+class BuyCommandTest {
+
+    private GameData context;
+    private Player player;
+
+    @BeforeEach
+    void setUp() {
+        context = TestHelpers.buildGameData();
+        player  = context.getPlayer();
+    }
+
+    @Test
+    void buyEnergyDrinkDeductsCost() {
+        int before = player.getWallet().getMoney();
+        execute("buy energy_drink", context);
+        assertEquals(before - EnergyDrink.COST, player.getWallet().getMoney());
+    }
+
+    @Test
+    void buyEnergyDrinkAddsToInventory() {
+        execute("buy energy_drink", context);
+        assertNotNull(player.getInventory().GetByItemCode("energy_drink"));
+    }
+
+    @Test
+    void buyReturnsSuccessState() {
+        CommandResult result = execute("buy energy_drink", context);
+        assertEquals(CommandState.SUCCESS, result.getResultState());
+    }
+
+    @Test
+    void buyWithInsufficientFundsReturnsFailed() {
+        player.getWallet().spendMoney(player.getWallet().getMoney()); // drain wallet
+        CommandResult result = execute("buy energy_drink", context);
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+
+    @Test
+    void buyWithInsufficientFundsDoesNotAddItem() {
+        player.getWallet().spendMoney(player.getWallet().getMoney());
+        execute("buy energy_drink", context);
+        assertNull(player.getInventory().GetByItemCode("energy_drink"));
+    }
+
+    @Test
+    void buyUnknownItemReturnsFailed() {
+        CommandResult result = execute("buy sandwich", context);
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+
+    @Test
+    void buyWithNoParamsReturnsInvalidParams() {
+        CommandResult result = execute("buy", context);
+        assertEquals(CommandState.INVALID_PARAMS, result.getResultState());
+    }
+
+    @Test
+    void buyWhenInventoryFullReturnsInventoryFull() {
+        // Fill inventory: capacity 20, drink bulk 2 → 10 drinks = 20 bulk
+        player.getInventory().Add(new EnergyDrink(), 10);
+        CommandResult result = execute("buy energy_drink", context);
+        assertEquals(CommandState.INVENTORY_FULL, result.getResultState());
+    }
+
+    @Test
+    void buyDoesNotSpendMoneyWhenInventoryFull() {
+        player.getInventory().Add(new EnergyDrink(), 10);
+        int moneyBefore = player.getWallet().getMoney();
+        execute("buy energy_drink", context);
+        assertEquals(moneyBefore, player.getWallet().getMoney());
+    }
+}

+ 90 - 0
test/cz/davza/studentadventure/commands/ConsumeCommandTest.java

@@ -0,0 +1,90 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.Items.Book;
+import cz.davza.studentadventure.Items.EnergyDrink;
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.Player;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static cz.davza.studentadventure.TestHelpers.execute;
+import static org.junit.jupiter.api.Assertions.*;
+
+class ConsumeCommandTest {
+
+    private GameData context;
+    private Player player;
+
+    @BeforeEach
+    void setUp() {
+        context = TestHelpers.buildGameData();
+        player  = context.getPlayer();
+        player.getInventory().Add(new EnergyDrink(), 2);
+    }
+
+    @Test
+    void consumeAppliesItemEffect() {
+        player.getEnergy().decrease(50);
+        int before = player.getEnergy().getValue();
+        execute("consume energy_drink", context);
+        assertEquals(before + EnergyDrink.ENERGY_RESTORE, player.getEnergy().getValue());
+    }
+
+    @Test
+    void consumeRemovesOneFromStack() {
+        execute("consume energy_drink", context);
+        assertEquals(1, player.getInventory().GetByItemCode("energy_drink").getAmount());
+    }
+
+    @Test
+    void consumeLastItemClearsInventoryEntry() {
+        player.getInventory().Add(new EnergyDrink(), 1);
+        // inventory has 2 from setUp + 1 = 3 total
+        execute("consume energy_drink", context);
+        execute("consume energy_drink", context);
+        execute("consume energy_drink", context);
+        assertNull(player.getInventory().GetByItemCode("energy_drink"));
+    }
+
+    @Test
+    void consumeRestoresInventoryCapacity() {
+        int before = player.getInventory().GetRemainingCapacity();
+        execute("consume energy_drink", context);
+        assertEquals(before + new EnergyDrink().getBulk(),
+                player.getInventory().GetRemainingCapacity());
+    }
+
+    @Test
+    void consumeReturnsSuccessState() {
+        CommandResult result = execute("consume energy_drink", context);
+        assertEquals(CommandState.SUCCESS, result.getResultState());
+    }
+
+    @Test
+    void consumeItemNotInInventoryReturnsItemNotFound() {
+        CommandResult result = execute("consume mouldy_cheese", context);
+        assertEquals(CommandState.ITEM_NOT_FOUND, result.getResultState());
+    }
+
+    @Test
+    void consumeNonConsumableReturnsFailed() {
+        player.getInventory().Add(new Book(), 1);
+        CommandResult result = execute("consume book", context);
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+
+    @Test
+    void consumeWithNoParamsReturnsInvalidParams() {
+        CommandResult result = execute("consume", context);
+        assertEquals(CommandState.INVALID_PARAMS, result.getResultState());
+    }
+
+    @Test
+    void consumeEnergyCappsAt100() {
+        // energy already at 100 — drink shouldn't push above 100
+        execute("consume energy_drink", context);
+        assertEquals(100, player.getEnergy().getValue());
+    }
+}

+ 93 - 0
test/cz/davza/studentadventure/commands/DropCommandTest.java

@@ -0,0 +1,93 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.Items.Book;
+import cz.davza.studentadventure.Items.EnergyDrink;
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static cz.davza.studentadventure.TestHelpers.execute;
+import static org.junit.jupiter.api.Assertions.*;
+
+class DropCommandTest {
+
+    private GameData context;
+
+    @BeforeEach
+    void setUp() {
+        context = TestHelpers.buildGameData();
+        context.getPlayer().getInventory().Add(new Book(), 3);
+        context.getPlayer().getInventory().Add(new EnergyDrink(), 2);
+    }
+
+    @Test
+    void dropRemovesItemFromInventory() {
+        execute("drop book 1", context);
+        assertEquals(2, context.getPlayer().getInventory().GetByItemCode("book").getAmount());
+    }
+
+    @Test
+    void dropAddsItemToRoom() {
+        int before = context.getMap().getCurrentRoom().getItems().GetRemainingCapacity();
+        execute("drop book 1", context);
+        assertTrue(context.getMap().getCurrentRoom().getItems().GetRemainingCapacity() < before);
+    }
+
+    @Test
+    void dropEntireStackClearsInventoryEntry() {
+        execute("drop book 3", context);
+        assertNull(context.getPlayer().getInventory().GetByItemCode("book"));
+    }
+
+    @Test
+    void dropRestoresInventoryCapacity() {
+        int before = context.getPlayer().getInventory().GetRemainingCapacity();
+        execute("drop book 1", context);
+        assertEquals(before + new Book().getBulk(), context.getPlayer().getInventory().GetRemainingCapacity());
+    }
+
+    @Test
+    void dropReturnsSuccessState() {
+        CommandResult result = execute("drop book 1", context);
+        assertEquals(CommandState.SUCCESS, result.getResultState());
+    }
+
+    @Test
+    void dropItemNotInInventoryReturnsItemNotFound() {
+        CommandResult result = execute("drop mouldy_cheese 1", context);
+        assertEquals(CommandState.ITEM_NOT_FOUND, result.getResultState());
+    }
+
+    @Test
+    void dropWithNoParamsReturnsInvalidParams() {
+        CommandResult result = execute("drop", context);
+        assertEquals(CommandState.INVALID_PARAMS, result.getResultState());
+    }
+
+    @Test
+    void dropMoreThanStackDropsWholeStack() {
+        execute("drop book 100", context);
+        assertNull(context.getPlayer().getInventory().GetByItemCode("book"));
+    }
+
+    @Test
+    void dropWhenRoomFullDropsPartially() {
+        // Bedroom capacity is 20, already has Bed(1) and Money(1) = 2 used
+        // Fill the room to near capacity then try to drop more
+        context.getMap().getCurrentRoom().getItems().Add(new Book(), 5); // 5*3=15 + 2 existing = 17 used, 3 remaining
+        // room has 3 bulk left, book bulk=3 so exactly 1 more book fits
+        // drop 2 books — only 1 should go through
+        CommandResult result = execute("drop book 2", context);
+        assertEquals(CommandState.SUCCESS, result.getResultState());
+        assertTrue(result.getResultMessage().contains("cluttered"));
+    }
+
+    @Test
+    void dropWithNonNumberAmountDefaultsToOne() {
+        // "drop book abc" — second param parse fails → INVALID_PARAMS
+        CommandResult result = execute("drop book abc", context);
+        assertEquals(CommandState.INVALID_PARAMS, result.getResultState());
+    }
+}

+ 60 - 0
test/cz/davza/studentadventure/commands/InteractCommandTest.java

@@ -0,0 +1,60 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.Items.Book;
+import cz.davza.studentadventure.Items.Computer;
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static cz.davza.studentadventure.TestHelpers.execute;
+import static org.junit.jupiter.api.Assertions.*;
+
+class InteractCommandTest {
+
+    private GameData context;
+
+    @BeforeEach
+    void setUp() {
+        context = TestHelpers.buildGameData();
+        // Walk to computer lab: bedroom → hallway → studyroom → computerlab
+        context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("hallway"));
+        context.getMap().commitRoomChange();
+        context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("studyroom"));
+        context.getMap().commitRoomChange();
+        context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("computerlab"));
+        context.getMap().commitRoomChange();
+    }
+
+    @Test
+    void interactWithComputerReturnsActionPending() {
+        CommandResult result = execute("interact computer", context);
+        assertEquals(CommandState.ACTION_PENDING, result.getResultState());
+    }
+
+    @Test
+    void interactWithComputerSetsNewHandler() {
+        CommandResult result = execute("interact computer", context);
+        assertNotNull(result.getNewCommandHandler());
+    }
+
+    @Test
+    void interactWithNonExistentItemReturnsNotInteractable() {
+        CommandResult result = execute("interact ghost", context);
+        assertEquals(CommandState.ITEM_NOT_INTERACTABLE, result.getResultState());
+    }
+
+    @Test
+    void interactWithCollectableReturnsNotInteractable() {
+        // Book is in computer lab and is collectable, not interactable
+        CommandResult result = execute("interact book", context);
+        assertEquals(CommandState.ITEM_NOT_INTERACTABLE, result.getResultState());
+    }
+
+    @Test
+    void interactWithNoParamsReturnsInvalidParams() {
+        CommandResult result = execute("interact", context);
+        assertEquals(CommandState.INVALID_PARAMS, result.getResultState());
+    }
+}

+ 90 - 0
test/cz/davza/studentadventure/commands/PickupCommandTest.java

@@ -0,0 +1,90 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.Items.Book;
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static cz.davza.studentadventure.TestHelpers.execute;
+import static org.junit.jupiter.api.Assertions.*;
+
+class PickupCommandTest {
+
+    private GameData context;
+
+    @BeforeEach
+    void setUp() {
+        context = TestHelpers.buildGameData();
+        // Bedroom starts with a Bed and Money — add a Book for pickup tests
+        context.getMap().getCurrentRoom().getItems().Add(new Book(), 3);
+    }
+
+    @Test
+    void pickupItemAddsToInventory() {
+        execute("pickup book", context);
+        assertNotNull(context.getPlayer().getInventory().GetByItemCode("book"));
+    }
+
+    @Test
+    void pickupItemRemovesFromRoom() {
+        int before = context.getMap().getCurrentRoom().getItems().GetByItemCode("book").getAmount();
+        execute("pickup book", context);
+        int after = context.getMap().getCurrentRoom().getItems().GetByItemCode("book").getAmount();
+        assertEquals(before - 1, after);
+    }
+
+    @Test
+    void pickupSpecificAmountPicksCorrectNumber() {
+        execute("pickup book 2", context);
+        assertEquals(2, context.getPlayer().getInventory().GetByItemCode("book").getAmount());
+    }
+
+    @Test
+    void pickupReturnsSuccessState() {
+        CommandResult result = execute("pickup book", context);
+        assertEquals(CommandState.SUCCESS, result.getResultState());
+    }
+
+    @Test
+    void pickupNonExistentItemReturnsItemNotFound() {
+        CommandResult result = execute("pickup energy_drink", context);
+        assertEquals(CommandState.ITEM_NOT_FOUND, result.getResultState());
+    }
+
+    @Test
+    void pickupNonCollectableReturnsNotCollectable() {
+        // Bed is in bedroom and is not collectable
+        CommandResult result = execute("pickup bed", context);
+        assertEquals(CommandState.ITEM_NOT_COLLECTABLE, result.getResultState());
+    }
+
+    @Test
+    void pickupWithNoParamsReturnsInvalidParams() {
+        CommandResult result = execute("pickup", context);
+        assertEquals(CommandState.INVALID_PARAMS, result.getResultState());
+    }
+
+    @Test
+    void pickupWhenInventoryFullReturnsInventoryFull() {
+        // Fill inventory: capacity 20, book bulk 3 → 6 books fill 18, 7th won't all fit
+        context.getMap().getCurrentRoom().getItems().Add(new Book(), 10);
+        // Add 6 books to inventory (uses 18 capacity, 2 remaining — not enough for another book at bulk 3)
+        context.getPlayer().getInventory().Add(new Book(), 6);
+        CommandResult result = execute("pickup book 1", context);
+        assertEquals(CommandState.INVENTORY_FULL, result.getResultState());
+    }
+
+    @Test
+    void pickupWithNonNumberAmountReturnsInvalidParams() {
+        CommandResult result = execute("pickup book abc", context);
+        assertEquals(CommandState.INVALID_PARAMS, result.getResultState());
+    }
+
+    @Test
+    void pickupDefaultsToOneWhenNoAmountGiven() {
+        execute("pickup book", context);
+        assertEquals(1, context.getPlayer().getInventory().GetByItemCode("book").getAmount());
+    }
+}

+ 124 - 0
test/cz/davza/studentadventure/commands/SleepCommandTest.java

@@ -0,0 +1,124 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.Player;
+import cz.davza.studentadventure.rooms.BedroomRoom;
+import cz.davza.studentadventure.rooms.ClassroomRoom;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import java.util.ArrayList;
+import java.util.List;
+import static cz.davza.studentadventure.TestHelpers.execute;
+import static org.junit.jupiter.api.Assertions.*;
+
+class SleepCommandTest {
+
+    private GameData context;
+    private Player player;
+
+    @BeforeEach
+    void setUp() {
+        context = TestHelpers.buildGameData();
+        player  = context.getPlayer();
+        // Starting room is Bedroom — sleep is valid here immediately
+    }
+
+    @Test
+    void sleepInBedroomReturnsSuccess() {
+        CommandResult result = execute("sleep", context);
+        assertEquals(CommandState.SUCCESS, result.getResultState());
+    }
+
+    @Test
+    void sleepInBedroomRestoresEnergy() {
+        player.getEnergy().decrease(50);
+        execute("sleep", context);
+        assertEquals(100, player.getEnergy().getValue());
+    }
+
+    @Test
+    void sleepInBedroomReducesStress() {
+        player.getStress().increase(60);
+        execute("sleep", context);
+        assertEquals(0, player.getStress().getValue());
+    }
+
+    @Test
+    void sleepInBedroomResetsHoursAwake() {
+        player.addHoursAwake(20);
+        execute("sleep", context);
+        assertEquals(cz.davza.studentadventure.enums.DeprivationState.NORMAL,
+                player.getDeprivationState());
+    }
+
+    @Test
+    void sleepInBedroomAdvancesToNextMorning() {
+        execute("sleep", context);
+        assertEquals(2, context.getTime().getDay());
+        assertEquals(8, context.getTime().getHour());
+    }
+
+    @Test
+    void sleepOutsideBedroomAndClassroomFails() {
+        // Walk to hallway first
+        context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("hallway"));
+        context.getMap().commitRoomChange();
+        CommandResult result = execute("sleep", context);
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+
+    @Test
+    void sleepInClassroomAdvancesTimeBy2Hours() {
+        // Move player to classroom — need book to enter
+        context.getPlayer().getInventory().Add(new cz.davza.studentadventure.Items.Book(), 1);
+        context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("hallway"));
+        context.getMap().commitRoomChange();
+        context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("classroom"));
+        context.getMap().commitRoomChange();
+
+        int hourBefore = context.getTime().getHour();
+        // Run many times to guarantee we hit the success path at least once
+        // (30% catch chance means ~99.9% chance of success in 20 attempts)
+        boolean succeeded = false;
+        for (int i = 0; i < 20; i++) {
+            context = TestHelpers.buildGameData();
+            player  = context.getPlayer();
+            player.getInventory().Add(new cz.davza.studentadventure.Items.Book(), 1);
+            context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("hallway"));
+            context.getMap().commitRoomChange();
+            context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("classroom"));
+            context.getMap().commitRoomChange();
+            int before = context.getTime().getHour();
+            CommandResult result = execute("sleep", context);
+            if (result.getResultState() == CommandState.SUCCESS) {
+                assertEquals(before + 2, context.getTime().getHour());
+                succeeded = true;
+                break;
+            }
+        }
+        assertTrue(succeeded, "Expected at least one successful classroom sleep in 20 attempts");
+    }
+
+    @Test
+    void sleepInClassroomCanReturnGameOver() {
+        // Run many times to guarantee we hit the caught path
+        boolean caughtOnce = false;
+        for (int i = 0; i < 50; i++) {
+            GameData ctx = TestHelpers.buildGameData();
+            ctx.getPlayer().getInventory().Add(new cz.davza.studentadventure.Items.Book(), 1);
+            ctx.getMap().setNextRoom(ctx.getMap().getCurrentRoom().getExit("hallway"));
+            ctx.getMap().commitRoomChange();
+            ctx.getMap().setNextRoom(ctx.getMap().getCurrentRoom().getExit("classroom"));
+            ctx.getMap().commitRoomChange();
+            CommandResult result = execute("sleep", ctx);
+            if (result.getResultState() == CommandState.GAME_OVER) {
+                caughtOnce = true;
+                break;
+            }
+        }
+        assertTrue(caughtOnce, "Expected to be caught sleeping at least once in 50 attempts");
+    }
+}

+ 96 - 0
test/cz/davza/studentadventure/commands/StealCommandTest.java

@@ -0,0 +1,96 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.Player;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static cz.davza.studentadventure.TestHelpers.execute;
+import static org.junit.jupiter.api.Assertions.*;
+
+class StealCommandTest {
+
+    private GameData context;
+    private Player player;
+
+    @BeforeEach
+    void setUp() {
+        context = TestHelpers.buildGameData();
+        player  = context.getPlayer();
+    }
+
+    @Test
+    void stealCanSucceedAndAddMoney() {
+        boolean succeededOnce = false;
+        int startMoney = player.getWallet().getMoney();
+        for (int i = 0; i < 50; i++) {
+            context = TestHelpers.buildGameData();
+            player  = context.getPlayer();
+            CommandResult result = execute("steal", context);
+            if (result.getResultState() == CommandState.SUCCESS) {
+                assertTrue(player.getWallet().getMoney() > startMoney);
+                succeededOnce = true;
+                break;
+            }
+        }
+        assertTrue(succeededOnce, "Expected steal to succeed at least once in 50 attempts");
+    }
+
+    @Test
+    void stealCanReturnGameOver() {
+        boolean caughtOnce = false;
+        for (int i = 0; i < 50; i++) {
+            context = TestHelpers.buildGameData();
+            CommandResult result = execute("steal", context);
+            if (result.getResultState() == CommandState.GAME_OVER) {
+                caughtOnce = true;
+                break;
+            }
+        }
+        assertTrue(caughtOnce, "Expected to be caught stealing at least once in 50 attempts");
+    }
+
+    @Test
+    void stealInDesperateStateHasLowerCatchChance() {
+        // Desperate: stress > 70, energy < 30
+        // Run many attempts and count game overs vs successes in desperate vs normal
+        // Desperate should have fewer game overs (15% vs 40%)
+        int desperateGameOvers = 0;
+        int normalGameOvers = 0;
+        int trials = 200;
+
+        for (int i = 0; i < trials; i++) {
+            GameData ctx = TestHelpers.buildGameData();
+            ctx.getPlayer().getStress().increase(80);
+            ctx.getPlayer().getEnergy().decrease(80);
+            if (execute("steal", ctx).getResultState() == CommandState.GAME_OVER) desperateGameOvers++;
+        }
+        for (int i = 0; i < trials; i++) {
+            GameData ctx = TestHelpers.buildGameData();
+            if (execute("steal", ctx).getResultState() == CommandState.GAME_OVER) normalGameOvers++;
+        }
+
+        // With 200 trials, desperate (~15%) should have noticeably fewer than normal (~40%)
+        assertTrue(desperateGameOvers < normalGameOvers,
+                "Desperate steal should be caught less often. Desperate: " + desperateGameOvers
+                        + " Normal: " + normalGameOvers);
+    }
+
+    @Test
+    void stealMoneyAmountIsWithinExpectedRange() {
+        for (int i = 0; i < 50; i++) {
+            context = TestHelpers.buildGameData();
+            player  = context.getPlayer();
+            int before = player.getWallet().getMoney();
+            CommandResult result = execute("steal", context);
+            if (result.getResultState() == CommandState.SUCCESS) {
+                int gained = player.getWallet().getMoney() - before;
+                assertTrue(gained >= 10 && gained <= 40,
+                        "Expected stolen amount 10-40, got " + gained);
+                return;
+            }
+        }
+    }
+}

+ 133 - 0
test/cz/davza/studentadventure/commands/StudyCommandTest.java

@@ -0,0 +1,133 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.enums.DeprivationState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.Player;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static cz.davza.studentadventure.TestHelpers.execute;
+import static org.junit.jupiter.api.Assertions.*;
+
+class StudyCommandTest {
+
+    private GameData context;
+    private Player player;
+
+    @BeforeEach
+    void setUp() {
+        context = TestHelpers.buildGameData();
+        player  = context.getPlayer();
+    }
+
+    @Test
+    void studyOneHourIncreasesIntelligenceBy5() {
+        execute("study 1", context);
+        assertEquals(10, player.getIntelligence().getValue());
+    }
+
+    @Test
+    void studyOneHourCostsEnergy() {
+        int before = player.getEnergy().getValue();
+        execute("study 1", context);
+        assertTrue(player.getEnergy().getValue() < before);
+    }
+
+    @Test
+    void studyOneHourIncreasesStress() {
+        execute("study 1", context);
+        assertTrue(player.getStress().getValue() > 0);
+    }
+
+    @Test
+    void studyOneHourAdvancesTimeByOneHour() {
+        int before = context.getTime().getHour();
+        execute("study 1", context);
+        assertEquals(before + 1, context.getTime().getHour());
+    }
+
+    @Test
+    void studyMultipleHoursStacksGains() {
+        execute("study 3", context);
+        assertEquals(5 + 15, player.getIntelligence().getValue());
+    }
+
+    @Test
+    void studyMultipleHoursAdvancesTimeByThatMany() {
+        int before = context.getTime().getHour();
+        execute("study 3", context);
+        assertEquals(before + 3, context.getTime().getHour());
+    }
+
+    @Test
+    void studyWithNoParamsReturnsInvalidParams() {
+        CommandResult result = execute("study", context);
+        assertEquals(CommandState.INVALID_PARAMS, result.getResultState());
+    }
+
+    @Test
+    void studyWithZeroHoursReturnsInvalidParams() {
+        CommandResult result = execute("study 0", context);
+        assertEquals(CommandState.INVALID_PARAMS, result.getResultState());
+    }
+
+    @Test
+    void studyWithNegativeHoursReturnsInvalidParams() {
+        CommandResult result = execute("study -1", context);
+        assertEquals(CommandState.INVALID_PARAMS, result.getResultState());
+    }
+
+    @Test
+    void studyWithNonNumberReturnsInvalidParams() {
+        CommandResult result = execute("study abc", context);
+        assertEquals(CommandState.INVALID_PARAMS, result.getResultState());
+    }
+
+    @Test
+    void studyWhenEnergyZeroReturnsFailed() {
+        player.getEnergy().decrease(100);
+        CommandResult result = execute("study 1", context);
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+
+    @Test
+    void studyStopsEarlyWhenEnergyRunsOut() {
+        player.getEnergy().decrease(85); // 15 energy left, first hour costs ~10, second ~13 → stops at 1
+        CommandResult result = execute("study 2", context);
+        assertEquals(CommandState.SUCCESS, result.getResultState());
+        assertTrue(result.getResultMessage().contains("run out of energy"));
+    }
+
+    @Test
+    void studyAddsToHoursAwakeCounter() {
+        execute("study 3", context);
+        // 3 hours awake is still NORMAL (threshold is 14)
+        assertEquals(DeprivationState.NORMAL, player.getDeprivationState());
+    }
+
+    @Test
+    void studyReturnsSuccessState() {
+        CommandResult result = execute("study 1", context);
+        assertEquals(CommandState.SUCCESS, result.getResultState());
+    }
+
+    @Test
+    void studyCostsMoreEnergyWithDeprivation() {
+        // Push player to deprived state
+        player.addHoursAwake(25);
+        int energyBefore = player.getEnergy().getValue();
+        execute("study 1", context);
+        int energyCostDeprived = energyBefore - player.getEnergy().getValue();
+
+        // Reset and do the same at normal state
+        context = TestHelpers.buildGameData();
+        player  = context.getPlayer();
+        int energyBefore2 = player.getEnergy().getValue();
+        execute("study 1", context);
+        int energyCostNormal = energyBefore2 - player.getEnergy().getValue();
+
+        assertTrue(energyCostDeprived > energyCostNormal);
+    }
+}

+ 98 - 0
test/cz/davza/studentadventure/commands/TakeExamCommandTest.java

@@ -0,0 +1,98 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import cz.davza.studentadventure.objects.Player;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static cz.davza.studentadventure.TestHelpers.execute;
+import static org.junit.jupiter.api.Assertions.*;
+
+class TakeExamCommandTest {
+
+    private GameData context;
+    private Player player;
+
+    @BeforeEach
+    void setUp() {
+        context = TestHelpers.buildGameData();
+        player  = context.getPlayer();
+    }
+
+    @Test
+    void examPassesWhenIntelligenceHighEnough() {
+        // Need effectiveScore >= 50. intelligence - stress/10 >= 50
+        // Set intelligence to 50, stress to 0 → score = 50
+        player.getIntelligence().increase(100); // clamps to 100
+        CommandResult result = execute("takeexam", context);
+        assertEquals(CommandState.GAME_WIN, result.getResultState());
+    }
+
+    @Test
+    void examFailsWhenIntelligenceTooLow() {
+        // Default intelligence = 5, stress = 0 → score = 5, need 50 → fail
+        CommandResult result = execute("takeexam", context);
+        assertEquals(CommandState.GAME_OVER, result.getResultState());
+    }
+
+    @Test
+    void examStressPenaltyReducesScore() {
+        // intelligence 60, stress 100 → penalty 10 → score 50 → just passes
+        player.getIntelligence().increase(100); // 100
+        player.getStress().increase(100);       // 100 → penalty 10
+        // effectiveScore = 100 - 10 = 90 → passes
+        CommandResult result = execute("takeexam", context);
+        assertEquals(CommandState.GAME_WIN, result.getResultState());
+    }
+
+    @Test
+    void examFailsWhenStressPenaltyPullsScoreBelow50() {
+        // intelligence = 55, stress = 100 → penalty 10 → score 45 → fail
+        player.getIntelligence().increase(50); // starts at 5, becomes 55
+        player.getStress().increase(100);
+        CommandResult result = execute("takeexam", context);
+        assertEquals(CommandState.GAME_OVER, result.getResultState());
+    }
+
+    @Test
+    void examPassMessageContainsDistinctionWhenScoreAbove80() {
+        player.getIntelligence().increase(100); // 100
+        // stress = 0 → score = 100 → distinction
+        CommandResult result = execute("takeexam", context);
+        assertTrue(result.getResultMessage().contains("DISTINCTION"));
+    }
+
+    @Test
+    void examPassMessageContainsMeritWhenScoreBetween65And80() {
+        // Need score 65-79: intelligence 70, stress 0 → score 70
+        player.getIntelligence().increase(65); // 5 + 65 = 70
+        CommandResult result = execute("takeexam", context);
+        assertTrue(result.getResultMessage().contains("MERIT"));
+    }
+
+    @Test
+    void examPassMessageContainsPassWhenScoreBetween50And64() {
+        // Need score 50-64: intelligence 55, stress 0 → score 55
+        player.getIntelligence().increase(50); // 5 + 50 = 55
+        CommandResult result = execute("takeexam", context);
+        assertTrue(result.getResultMessage().contains("RESULT: PASS\n"));
+    }
+
+    @Test
+    void examScoreExactlyAtThresholdPasses() {
+        // intelligence = 50, stress 0 → score exactly 50 → pass
+        player.getIntelligence().increase(45); // 5 + 45 = 50
+        CommandResult result = execute("takeexam", context);
+        assertEquals(CommandState.GAME_WIN, result.getResultState());
+    }
+
+    @Test
+    void examScoreOneBelowThresholdFails() {
+        // intelligence = 49, stress 0 → score 49 → fail
+        player.getIntelligence().increase(44); // 5 + 44 = 49
+        CommandResult result = execute("takeexam", context);
+        assertEquals(CommandState.GAME_OVER, result.getResultState());
+    }
+}

+ 100 - 0
test/cz/davza/studentadventure/commands/WalkCommandTest.java

@@ -0,0 +1,100 @@
+package cz.davza.studentadventure.commands;
+
+import cz.davza.studentadventure.Items.Book;
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static cz.davza.studentadventure.TestHelpers.execute;
+import static org.junit.jupiter.api.Assertions.*;
+
+class WalkCommandTest {
+
+    private GameData context;
+
+    @BeforeEach
+    void setUp() {
+        context = TestHelpers.buildGameData();
+        // starts in Bedroom
+    }
+
+    @Test
+    void walkToValidExitSetsNextRoom() {
+        execute("walk hallway", context);
+        assertTrue(context.getMap().hasPendingRoomChange());
+    }
+
+    @Test
+    void walkToValidExitReturnsRoomChanged() {
+        CommandResult result = execute("walk hallway", context);
+        assertEquals(CommandState.ROOM_CHANGED, result.getResultState());
+    }
+
+    @Test
+    void walkAdvancesTimeByTwoHours() {
+        int before = context.getTime().getHour();
+        execute("walk hallway", context);
+        assertEquals(before + 2, context.getTime().getHour());
+    }
+
+    @Test
+    void walkAdvancesHoursAwake() {
+        execute("walk hallway", context);
+        // 1 hour awake — still NORMAL
+        assertEquals(cz.davza.studentadventure.enums.DeprivationState.NORMAL,
+                context.getPlayer().getDeprivationState());
+    }
+
+    @Test
+    void walkToInvalidExitReturnsFailed() {
+        CommandResult result = execute("walk narnia", context);
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+
+    @Test
+    void walkWithNoParamsReturnsInvalidParams() {
+        CommandResult result = execute("walk", context);
+        assertEquals(CommandState.INVALID_PARAMS, result.getResultState());
+    }
+
+    @Test
+    void walkToClassroomWithoutBookReturnsFailed() {
+        // Bedroom → hallway (commit) → classroom (no book)
+        execute("walk hallway", context);
+        context.getMap().commitRoomChange();
+        CommandResult result = execute("walk classroom", context);
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+
+    @Test
+    void walkToClassroomWithoutBookAppliesStressPenalty() {
+        execute("walk hallway", context);
+        context.getMap().commitRoomChange();
+        int stressBefore = context.getPlayer().getStress().getValue();
+        execute("walk classroom", context);
+        assertTrue(context.getPlayer().getStress().getValue() > stressBefore);
+    }
+
+    @Test
+    void walkToClassroomWithBookReturnsRoomChanged() {
+        context.getPlayer().getInventory().Add(new Book(), 1);
+        execute("walk hallway", context);
+        context.getMap().commitRoomChange();
+        CommandResult result = execute("walk classroom", context);
+        assertEquals(CommandState.ROOM_CHANGED, result.getResultState());
+    }
+
+    @Test
+    void walkToExamRoomReturnsActionPending() {
+        // Bedroom → hallway → classroom (with book) → examroom
+        context.getPlayer().getInventory().Add(new Book(), 1);
+        execute("walk hallway", context);
+        context.getMap().commitRoomChange();
+        execute("walk classroom", context);
+        context.getMap().commitRoomChange();
+        CommandResult result = execute("walk examroom", context);
+        assertEquals(CommandState.ACTION_PENDING, result.getResultState());
+    }
+}

+ 147 - 0
test/cz/davza/studentadventure/integration/GameIntegrationTest.java

@@ -0,0 +1,147 @@
+package cz.davza.studentadventure.integration;
+
+import cz.davza.studentadventure.Items.Book;
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.TestHelpers.CapturingOutputSink;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.Game;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class GameIntegrationTest {
+
+    private Game game;
+    private CapturingOutputSink sink;
+
+    @BeforeEach
+    void setUp() {
+        sink = new CapturingOutputSink();
+        game = new Game(sink);
+    }
+
+    @Test
+    void quitReturnsGameOver() {
+        CommandResult result = game.sendCommand("quit");
+        assertEquals(CommandState.GAME_OVER, result.getResultState());
+    }
+
+    @Test
+    void commandAfterQuitIsRejected() {
+        game.sendCommand("quit");
+        CommandResult result = game.sendCommand("study 1");
+        assertEquals(CommandState.GAME_OVER, result.getResultState());
+    }
+
+    @Test
+    void unknownCommandReturnsFailed() {
+        CommandResult result = game.sendCommand("fly");
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+
+    @Test
+    void walkToHallwayChangesRoom() {
+        CommandResult result = game.sendCommand("walk hallway");
+        assertEquals(CommandState.ROOM_CHANGED, result.getResultState());
+        assertEquals("hallway", game.getContext().getMap().getCurrentRoom().getExitCode());
+    }
+
+    @Test
+    void walkToInvalidRoomFails() {
+        CommandResult result = game.sendCommand("walk narnia");
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+
+    @Test
+    void studyInBedroomWorks() {
+        int intBefore = game.getContext().getPlayer().getIntelligence().getValue();
+        game.sendCommand("study 1");
+        assertEquals(intBefore + 5, game.getContext().getPlayer().getIntelligence().getValue());
+    }
+
+    @Test
+    void deadlinePassedCausesGameOver() {
+        // Advance time past day 7, 09:00
+        for (int i = 0; i < 7; i++) game.getContext().getTime().advanceToNextMorning();
+        game.getContext().getTime().advanceHours(2);
+        // Any command should trigger the deadline check
+        CommandResult result = game.sendCommand("stats");
+        assertEquals(CommandState.GAME_OVER, result.getResultState());
+    }
+
+    @Test
+    void stressAt100CausesGameOver() {
+        game.getContext().getPlayer().getStress().increase(100);
+        CommandResult result = game.sendCommand("stats");
+        assertEquals(CommandState.GAME_OVER, result.getResultState());
+    }
+
+    @Test
+    void fullWalkSequenceBedroomToHallway() {
+        game.sendCommand("walk hallway");
+        assertEquals("hallway", game.getContext().getMap().getCurrentRoom().getExitCode());
+    }
+
+    @Test
+    void enteringClassroomWithoutBookFails() {
+        game.sendCommand("walk hallway");
+        CommandResult result = game.sendCommand("walk classroom");
+        assertEquals(CommandState.FAILED, result.getResultState());
+        // should still be in hallway
+        assertEquals("hallway", game.getContext().getMap().getCurrentRoom().getExitCode());
+    }
+
+    @Test
+    void fullPathToExamAndPass() {
+        // Max out intelligence, walk to exam room, enter with correct ID, take exam
+        game.getContext().getPlayer().getIntelligence().increase(100);
+        game.getContext().getPlayer().getInventory().Add(new Book(), 1);
+
+        game.sendCommand("walk hallway");
+        game.sendCommand("walk classroom");
+        game.sendCommand("walk examroom"); // triggers ID prompt
+
+        String studentId = game.getContext().getPlayer().getId();
+        game.sendCommand(studentId); // enter exam room
+
+        CommandResult result = game.sendCommand("takeexam");
+        assertEquals(CommandState.GAME_WIN, result.getResultState());
+    }
+
+    @Test
+    void buyAndConsumeEnergyDrinkFlow() {
+        // Walk to shop: hallway → studyroom → computerlab → shop
+        game.sendCommand("walk hallway");
+        game.sendCommand("walk studyroom");
+        game.sendCommand("walk computerlab");
+        game.sendCommand("walk shop");
+
+        game.getContext().getPlayer().getEnergy().decrease(50);
+        int energyBefore = game.getContext().getPlayer().getEnergy().getValue();
+
+        game.sendCommand("buy energy_drink");
+        game.sendCommand("consume energy_drink");
+
+        assertTrue(game.getContext().getPlayer().getEnergy().getValue() > energyBefore);
+    }
+
+    @Test
+    void computerInteractionFullFlow() {
+        game.sendCommand("walk hallway");
+        game.sendCommand("walk studyroom");
+        game.sendCommand("walk computerlab");
+
+        game.sendCommand("interact computer");       // → asks for password
+        game.sendCommand("wrongpassword");           // → wrong
+        game.sendCommand("java123");                 // → correct, now active
+
+        int intBefore = game.getContext().getPlayer().getIntelligence().getValue();
+        game.sendCommand("study 1");                 // study while at computer
+        assertEquals(intBefore + 5,
+                game.getContext().getPlayer().getIntelligence().getValue());
+
+        CommandResult result = game.sendCommand("exit"); // leave computer
+        assertEquals(CommandState.ACTION_RESUMED, result.getResultState());
+    }
+}

+ 103 - 0
test/cz/davza/studentadventure/items/ComputerTest.java

@@ -0,0 +1,103 @@
+package cz.davza.studentadventure.items;
+
+import cz.davza.studentadventure.Items.Computer;
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class ComputerTest {
+
+    private Computer computer;
+    private GameData context;
+    private static final String PASSWORD = "java123";
+
+    @BeforeEach
+    void setUp() {
+        context  = TestHelpers.buildGameData();
+        computer = new Computer(PASSWORD);
+        // Navigate to computer lab where study is allowed
+        context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("hallway"));
+        context.getMap().commitRoomChange();
+        context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("studyroom"));
+        context.getMap().commitRoomChange();
+        context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("computerlab"));
+        context.getMap().commitRoomChange();
+    }
+
+    @Test
+    void interactWhileLockedRequestsPassword() {
+        CommandResult result = computer.Interact(context);
+        assertEquals(CommandState.ACTION_PENDING, result.getResultState());
+        assertTrue(result.getResultMessage().contains("locked"));
+    }
+
+    @Test
+    void exitWhileAtPasswordPromptResumesGame() {
+        computer.Interact(context);
+        CommandResult result = computer.handleCommand("exit", context);
+        assertEquals(CommandState.ACTION_RESUMED, result.getResultState());
+    }
+
+    @Test
+    void wrongPasswordReturnsFailed() {
+        computer.Interact(context);
+        CommandResult result = computer.handleCommand("wrongpass", context);
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+
+    @Test
+    void wrongPasswordKeepsComputerLocked() {
+        computer.Interact(context);
+        computer.handleCommand("wrongpass", context);
+        // Should still be locked — interacting again should ask for password
+        CommandResult result = computer.Interact(context);
+        assertTrue(result.getResultMessage().contains("locked"));
+    }
+
+    @Test
+    void correctPasswordUnlocksComputer() {
+        computer.Interact(context);
+        CommandResult result = computer.handleCommand(PASSWORD, context);
+        // Should return success (handler already on computer)
+        assertEquals(CommandState.SUCCESS, result.getResultState());
+    }
+
+    @Test
+    void afterUnlockInteractGoesActiveNotPassword() {
+        computer.Interact(context);
+        computer.handleCommand(PASSWORD, context);
+        // Next interact should go straight to active state
+        CommandResult result = computer.Interact(context);
+        assertEquals(CommandState.ACTION_PENDING, result.getResultState());
+        assertFalse(result.getResultMessage().contains("locked"));
+    }
+
+    @Test
+    void exitWhileActiveResumesGame() {
+        computer.Interact(context);
+        computer.handleCommand(PASSWORD, context);
+        CommandResult result = computer.handleCommand("exit", context);
+        assertEquals(CommandState.ACTION_RESUMED, result.getResultState());
+    }
+
+    @Test
+    void studyCommandWorksWhileActiveOnComputer() {
+        computer.Interact(context);
+        computer.handleCommand(PASSWORD, context);
+        int intBefore = context.getPlayer().getIntelligence().getValue();
+        computer.handleCommand("study 1", context);
+        assertEquals(intBefore + 5, context.getPlayer().getIntelligence().getValue());
+    }
+
+    @Test
+    void unknownCommandWhileActiveReturnsFailed() {
+        computer.Interact(context);
+        computer.handleCommand(PASSWORD, context);
+        CommandResult result = computer.handleCommand("sleep", context);
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+}

+ 84 - 0
test/cz/davza/studentadventure/objects/GameTimeTest.java

@@ -0,0 +1,84 @@
+package cz.davza.studentadventure.objects;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class GameTimeTest {
+
+    @Test
+    void startsOnDay1At8() {
+        GameTime time = new GameTime();
+        assertEquals(1, time.getDay());
+        assertEquals(8, time.getHour());
+    }
+
+    @Test
+    void advanceHoursWithinSameDay() {
+        GameTime time = new GameTime();
+        time.advanceHours(3);
+        assertEquals(1, time.getDay());
+        assertEquals(11, time.getHour());
+    }
+
+    @Test
+    void advanceHoursRollsOverToNextDay() {
+        GameTime time = new GameTime();
+        time.advanceHours(20); // 8 + 20 = 28 → day 2, hour 4
+        assertEquals(2, time.getDay());
+        assertEquals(4, time.getHour());
+    }
+
+    @Test
+    void advanceHoursRollsOverMultipleDays() {
+        GameTime time = new GameTime();
+        time.advanceHours(48); // 8 + 48 = 56 → day 3, hour 8
+        assertEquals(3, time.getDay());
+        assertEquals(8, time.getHour());
+    }
+
+    @Test
+    void advanceToNextMorningSetsDay2Hour8() {
+        GameTime time = new GameTime();
+        time.advanceHours(5); // move to hour 13
+        time.advanceToNextMorning();
+        assertEquals(2, time.getDay());
+        assertEquals(8, time.getHour());
+    }
+
+    @Test
+    void notPastDeadlineAtStart() {
+        GameTime time = new GameTime();
+        assertFalse(time.isPastDeadline());
+    }
+
+    @Test
+    void notPastDeadlineJustBeforeExam() {
+        GameTime time = new GameTime();
+        // advance to day 7, hour 8 — one hour before deadline
+        for (int i = 0; i < 6; i++) time.advanceToNextMorning();
+        assertEquals(7, time.getDay());
+        assertEquals(8, time.getHour());
+        assertFalse(time.isPastDeadline());
+    }
+
+    @Test
+    void pastDeadlineAtExactDeadline() {
+        GameTime time = new GameTime();
+        for (int i = 0; i < 6; i++) time.advanceToNextMorning();
+        time.advanceHours(1); // day 7, hour 9
+        assertTrue(time.isPastDeadline());
+    }
+
+    @Test
+    void pastDeadlineAfterDay7() {
+        GameTime time = new GameTime();
+        for (int i = 0; i < 7; i++) time.advanceToNextMorning();
+        assertTrue(time.isPastDeadline());
+    }
+
+    @Test
+    void toStringFormat() {
+        GameTime time = new GameTime();
+        assertEquals("Day 1, 08:00", time.toString());
+    }
+}

+ 170 - 0
test/cz/davza/studentadventure/objects/ItemCollectionBaseTest.java

@@ -0,0 +1,170 @@
+package cz.davza.studentadventure.objects;
+
+import cz.davza.studentadventure.Items.Book;
+import cz.davza.studentadventure.Items.EnergyDrink;
+import cz.davza.studentadventure.Items.StickyNote;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests ItemCollectionBase behaviour through PlayerInventory.
+ * Inventory capacity = 20 bulk units.
+ * Book bulk = 3, EnergyDrink bulk = 2, StickyNote bulk = 1.
+ */
+class ItemCollectionBaseTest {
+
+    private PlayerInventory inventory;
+
+    @BeforeEach
+    void setUp() {
+        inventory = new PlayerInventory(20);
+    }
+
+    // ── Add ──────────────────────────────────────────────────────────────────
+
+    @Test
+    void addItemIncreasesUsedCapacity() {
+        inventory.Add(new Book(), 1);
+        assertEquals(20 - 3, inventory.GetRemainingCapacity());
+    }
+
+    @Test
+    void addMultipleItemsStackUnderSameKey() {
+        inventory.Add(new Book(), 2);
+        ItemStack stack = inventory.GetByItemCode("book");
+        assertNotNull(stack);
+        assertEquals(2, stack.getAmount());
+    }
+
+    @Test
+    void addReturnsZeroWhenAllFit() {
+        int leftOver = inventory.Add(new Book(), 2); // costs 6 bulk, capacity 20
+        assertEquals(0, leftOver);
+    }
+
+    @Test
+    void addReturnsLeftoverWhenCapacityExceeded() {
+        // capacity 20, book bulk 3 → can fit 6 books (18 bulk), 7th won't fit
+        int leftOver = inventory.Add(new Book(), 7);
+        assertEquals(1, leftOver);
+    }
+
+    @Test
+    void addToExistingStackMergesCorrectly() {
+        inventory.Add(new Book(), 2);
+        inventory.Add(new Book(), 1);
+        ItemStack stack = inventory.GetByItemCode("book");
+        assertEquals(3, stack.getAmount());
+    }
+
+    @Test
+    void addZeroBulkItemAlwaysFits() {
+        // StickyNote has bulk 1 actually — test with a full inventory of high-bulk items
+        // fill to 18 bulk (6 books), then add sticky note (bulk 1) — still 2 bulk remaining
+        inventory.Add(new Book(), 6);
+        int leftOver = inventory.Add(new StickyNote("test"), 1);
+        assertEquals(0, leftOver);
+    }
+
+    // ── Remove ───────────────────────────────────────────────────────────────
+
+    @Test
+    void removeDecreasesStackAmount() {
+        inventory.Add(new Book(), 3);
+        inventory.Remove("book", 1);
+        assertEquals(2, inventory.GetByItemCode("book").getAmount());
+    }
+
+    @Test
+    void removeDecrementsUsedCapacity() {
+        inventory.Add(new Book(), 2); // 6 bulk used
+        inventory.Remove("book", 1); // 3 bulk freed
+        assertEquals(20 - 3, inventory.GetRemainingCapacity());
+    }
+
+    @Test
+    void removeEntireStackClearsEntry() {
+        inventory.Add(new Book(), 2);
+        inventory.Remove("book", 2);
+        assertNull(inventory.GetByItemCode("book"));
+    }
+
+    @Test
+    void removeMoreThanStackSizeClearsEntry() {
+        inventory.Add(new Book(), 1);
+        inventory.Remove("book", 99);
+        assertNull(inventory.GetByItemCode("book"));
+    }
+
+    @Test
+    void removeNonExistentCodeDoesNotThrow() {
+        assertDoesNotThrow(() -> inventory.Remove("nonexistent", 1));
+    }
+
+    @Test
+    void removeRestoresCapacityCorrectly() {
+        inventory.Add(new EnergyDrink(), 3); // 6 bulk used
+        inventory.Remove("energy_drink", 3); // all removed
+        assertEquals(20, inventory.GetRemainingCapacity());
+    }
+
+    // ── GetByItemCode ────────────────────────────────────────────────────────
+
+    @Test
+    void getByItemCodeReturnsNullWhenNotPresent() {
+        assertNull(inventory.GetByItemCode("book"));
+    }
+
+    @Test
+    void getByItemCodeReturnsCorrectStack() {
+        inventory.Add(new Book(), 2);
+        ItemStack stack = inventory.GetByItemCode("book");
+        assertNotNull(stack);
+        assertEquals("book", stack.getItem().getCode());
+    }
+
+    // ── Capacity ─────────────────────────────────────────────────────────────
+
+    @Test
+    void remainingCapacityStartsAtFullCapacity() {
+        assertEquals(20, inventory.GetRemainingCapacity());
+    }
+
+    @Test
+    void capacityReturnsConfiguredMax() {
+        assertEquals(20, inventory.GetCapacity());
+    }
+
+    // ── Empty ────────────────────────────────────────────────────────────────
+
+    @Test
+    void emptyClearsAllItems() {
+        inventory.Add(new Book(), 2);
+        inventory.Add(new EnergyDrink(), 1);
+        inventory.Empty();
+        assertNull(inventory.GetByItemCode("book"));
+        assertNull(inventory.GetByItemCode("energy_drink"));
+    }
+
+    @Test
+    void emptyResetsUsedCapacity() {
+        inventory.Add(new Book(), 3);
+        inventory.Empty();
+        assertEquals(20, inventory.GetRemainingCapacity());
+    }
+
+    // ── GetItems ─────────────────────────────────────────────────────────────
+
+    @Test
+    void getItemsReturnsOnlyOccupiedEntries() {
+        inventory.Add(new Book(), 1);
+        inventory.Add(new EnergyDrink(), 1);
+        assertEquals(2, inventory.GetItems().size());
+    }
+
+    @Test
+    void getItemsIsEmptyWhenInventoryEmpty() {
+        assertTrue(inventory.GetItems().isEmpty());
+    }
+}

+ 157 - 0
test/cz/davza/studentadventure/objects/ItemCollectionTest.java

@@ -0,0 +1,157 @@
+package cz.davza.studentadventure.objects;
+
+import cz.davza.studentadventure.Items.Book;
+import cz.davza.studentadventure.Items.EnergyDrink;
+import cz.davza.studentadventure.interfaces.IItem;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class ItemCollectionTest {
+
+    private PlayerInventory inventory;
+    private IItem book;
+    private IItem drink;
+
+    @BeforeEach
+    void setUp() {
+        inventory = new PlayerInventory(20); // capacity of 20 bulk
+        book  = new Book();       // bulk = 3
+        drink = new EnergyDrink();// bulk = 2
+    }
+
+    // ── Add ──────────────────────────────────────────────────────────
+
+    @Test
+    void addItemStoresIt() {
+        inventory.Add(book, 1);
+        assertNotNull(inventory.GetByItemCode("book"));
+    }
+
+    @Test
+    void addItemReturnsZeroLeftoverWhenFits() {
+        int leftOver = inventory.Add(book, 1);
+        assertEquals(0, leftOver);
+    }
+
+    @Test
+    void addItemDecreasesRemainingCapacity() {
+        inventory.Add(book, 1); // bulk 3
+        assertEquals(17, inventory.GetRemainingCapacity());
+    }
+
+    @Test
+    void addMultipleOfSameItemStacksTogether() {
+        inventory.Add(book, 2);
+        assertEquals(2, inventory.GetByItemCode("book").getAmount());
+    }
+
+    @Test
+    void addReturnsLeftoverWhenCapacityExceeded() {
+        // capacity 20, book bulk 3 → fits 6 (uses 18), 7th overflows
+        int leftOver = inventory.Add(book, 7);
+        assertEquals(1, leftOver);
+    }
+
+    @Test
+    void addDifferentItemsTrackSeparately() {
+        inventory.Add(book, 1);
+        inventory.Add(drink, 1);
+        assertNotNull(inventory.GetByItemCode("book"));
+        assertNotNull(inventory.GetByItemCode("energy_drink"));
+    }
+
+    @Test
+    void addZeroItemsDoesNothing() {
+        // capacity / bulk = 20/3 = 6, asking for 0 → canInsert = min(0, 6) = 0
+        inventory.Add(book, 0);
+        assertNull(inventory.GetByItemCode("book"));
+        assertEquals(20, inventory.GetRemainingCapacity());
+    }
+
+    // ── Remove ───────────────────────────────────────────────────────
+
+    @Test
+    void removeReducesStackAmount() {
+        inventory.Add(book, 3);
+        inventory.Remove("book", 1);
+        assertEquals(2, inventory.GetByItemCode("book").getAmount());
+    }
+
+    @Test
+    void removeRestoresCapacity() {
+        inventory.Add(book, 2); // uses 6
+        inventory.Remove("book", 1); // frees 3
+        assertEquals(17, inventory.GetRemainingCapacity());
+    }
+
+    @Test
+    void removeEntireStackCleansUpEntry() {
+        inventory.Add(book, 1);
+        inventory.Remove("book", 1);
+        assertNull(inventory.GetByItemCode("book"));
+    }
+
+    @Test
+    void removeMoreThanStackRemovesAll() {
+        inventory.Add(book, 2);
+        inventory.Remove("book", 10);
+        assertNull(inventory.GetByItemCode("book"));
+    }
+
+    @Test
+    void removeMissingCodeIsNoOp() {
+        assertDoesNotThrow(() -> inventory.Remove("nonexistent", 1));
+    }
+
+    @Test
+    void removeFullyRestoresCapacityToInitial() {
+        inventory.Add(book, 2);
+        inventory.Remove("book", 2);
+        assertEquals(20, inventory.GetRemainingCapacity());
+    }
+
+    // ── GetByItemCode ─────────────────────────────────────────────────
+
+    @Test
+    void getByItemCodeReturnsNullForMissingItem() {
+        assertNull(inventory.GetByItemCode("book"));
+    }
+
+    @Test
+    void getByItemCodeReturnsCorrectStack() {
+        inventory.Add(drink, 3);
+        ItemStack stack = inventory.GetByItemCode("energy_drink");
+        assertNotNull(stack);
+        assertEquals(3, stack.getAmount());
+    }
+
+    // ── Capacity ──────────────────────────────────────────────────────
+
+    @Test
+    void getRemainingCapacityStartsAtMax() {
+        assertEquals(20, inventory.GetRemainingCapacity());
+    }
+
+    @Test
+    void getCapacityReturnsInitialSize() {
+        assertEquals(20, inventory.GetCapacity());
+    }
+
+    // ── Empty ─────────────────────────────────────────────────────────
+
+    @Test
+    void emptyRemovesAllItems() {
+        inventory.Add(book, 2);
+        inventory.Add(drink, 1);
+        inventory.Empty();
+        assertTrue(inventory.GetItems().isEmpty());
+    }
+
+    @Test
+    void emptyResetsUsedCapacity() {
+        inventory.Add(book, 2);
+        inventory.Empty();
+        assertEquals(20, inventory.GetRemainingCapacity());
+    }
+}

+ 66 - 0
test/cz/davza/studentadventure/objects/PlayerStatTest.java

@@ -0,0 +1,66 @@
+package cz.davza.studentadventure.objects;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class PlayerStatTest {
+
+    @Test
+    void initialValueIsSet() {
+        PlayerStat stat = new PlayerStat("Energy", 50);
+        assertEquals(50, stat.getValue());
+    }
+
+    @Test
+    void initialValueClampsAbove100() {
+        PlayerStat stat = new PlayerStat("Energy", 150);
+        assertEquals(100, stat.getValue());
+    }
+
+    @Test
+    void initialValueClampsBelow0() {
+        PlayerStat stat = new PlayerStat("Energy", -10);
+        assertEquals(0, stat.getValue());
+    }
+
+    @Test
+    void increaseAddsValue() {
+        PlayerStat stat = new PlayerStat("Energy", 50);
+        stat.increase(20);
+        assertEquals(70, stat.getValue());
+    }
+
+    @Test
+    void increaseClampAt100() {
+        PlayerStat stat = new PlayerStat("Energy", 90);
+        stat.increase(50);
+        assertEquals(100, stat.getValue());
+    }
+
+    @Test
+    void decreaseSubtractsValue() {
+        PlayerStat stat = new PlayerStat("Energy", 50);
+        stat.decrease(20);
+        assertEquals(30, stat.getValue());
+    }
+
+    @Test
+    void decreaseClampAt0() {
+        PlayerStat stat = new PlayerStat("Energy", 10);
+        stat.decrease(50);
+        assertEquals(0, stat.getValue());
+    }
+
+    @Test
+    void increaseByZeroChangesNothing() {
+        PlayerStat stat = new PlayerStat("Energy", 50);
+        stat.increase(0);
+        assertEquals(50, stat.getValue());
+    }
+
+    @Test
+    void toStringFormat() {
+        PlayerStat stat = new PlayerStat("Energy", 75);
+        assertEquals("Energy: 75/100", stat.toString());
+    }
+}

+ 101 - 0
test/cz/davza/studentadventure/objects/PlayerTest.java

@@ -0,0 +1,101 @@
+package cz.davza.studentadventure.objects;
+
+import cz.davza.studentadventure.enums.DeprivationState;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class PlayerTest {
+
+    private Player player;
+
+    @BeforeEach
+    void setUp() {
+        player = new Player("Dave", 20);
+    }
+
+    @Test
+    void startsWithDefaultStats() {
+        assertEquals(5,   player.getIntelligence().getValue());
+        assertEquals(100, player.getEnergy().getValue());
+        assertEquals(0,   player.getStress().getValue());
+    }
+
+    @Test
+    void startsWithStartingMoney() {
+        assertEquals(50, player.getWallet().getMoney());
+    }
+
+    @Test
+    void nameIsSet() {
+        assertEquals("Dave", player.getName());
+    }
+
+    @Test
+    void idIsNotNull() {
+        assertNotNull(player.getId());
+    }
+
+    @Test
+    void addHoursAwakeAccumulates() {
+        player.addHoursAwake(5);
+        player.addHoursAwake(5);
+        assertEquals(DeprivationState.NORMAL, player.getDeprivationState()); // 10 < tired threshold 14
+    }
+
+    @Test
+    void deprivationStateTiredAt14Hours() {
+        player.addHoursAwake(14);
+        assertEquals(DeprivationState.TIRED, player.getDeprivationState());
+    }
+
+    @Test
+    void deprivationStateDeprivedAt20Hours() {
+        player.addHoursAwake(20);
+        assertEquals(DeprivationState.DEPRIVED, player.getDeprivationState());
+    }
+
+    @Test
+    void resetHoursAwakeSetsToNormal() {
+        player.addHoursAwake(25);
+        player.resetHoursAwake();
+        assertEquals(DeprivationState.NORMAL, player.getDeprivationState());
+    }
+
+    @Test
+    void multiplierIs1WhenNormal() {
+        assertEquals(1.0, player.getDeprivationMultiplier(), 0.001);
+    }
+
+    @Test
+    void multiplierIs1point3WhenTired() {
+        player.addHoursAwake(14);
+        assertEquals(1.3, player.getDeprivationMultiplier(), 0.001);
+    }
+
+    @Test
+    void multiplierIncreasesWhenDeprived() {
+        player.addHoursAwake(21); // 1 hour past deprived threshold
+        assertTrue(player.getDeprivationMultiplier() > 1.3);
+    }
+
+    @Test
+    void multiplierCapsAt3() {
+        player.addHoursAwake(100);
+        assertEquals(3.0, player.getDeprivationMultiplier(), 0.001);
+    }
+
+    @Test
+    void printStatsContainsMoneyFromWallet() {
+        String stats = player.printStats();
+        assertTrue(stats.contains("50 coins"));
+    }
+
+    @Test
+    void printStatsContainsAllStatNames() {
+        String stats = player.printStats();
+        assertTrue(stats.contains("Intelligence"));
+        assertTrue(stats.contains("Energy"));
+        assertTrue(stats.contains("Stress"));
+    }
+}

+ 55 - 0
test/cz/davza/studentadventure/objects/WalletTest.java

@@ -0,0 +1,55 @@
+package cz.davza.studentadventure.objects;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class WalletTest {
+
+    @Test
+    void startsWithGivenAmount() {
+        Wallet wallet = new Wallet(50);
+        assertEquals(50, wallet.getMoney());
+    }
+
+    @Test
+    void earnMoneyIncreasesBalance() {
+        Wallet wallet = new Wallet(10);
+        wallet.earnMoney(20);
+        assertEquals(30, wallet.getMoney());
+    }
+
+    @Test
+    void spendMoneyReducesBalance() {
+        Wallet wallet = new Wallet(50);
+        boolean result = wallet.spendMoney(20);
+        assertTrue(result);
+        assertEquals(30, wallet.getMoney());
+    }
+
+    @Test
+    void spendMoneyReturnsFalseWhenInsufficient() {
+        Wallet wallet = new Wallet(10);
+        boolean result = wallet.spendMoney(20);
+        assertFalse(result);
+    }
+
+    @Test
+    void balanceUnchangedAfterFailedSpend() {
+        Wallet wallet = new Wallet(10);
+        wallet.spendMoney(20);
+        assertEquals(10, wallet.getMoney());
+    }
+
+    @Test
+    void spendExactBalanceSucceeds() {
+        Wallet wallet = new Wallet(20);
+        assertTrue(wallet.spendMoney(20));
+        assertEquals(0, wallet.getMoney());
+    }
+
+    @Test
+    void defaultConstructorStartsAtZero() {
+        Wallet wallet = new Wallet();
+        assertEquals(0, wallet.getMoney());
+    }
+}

+ 71 - 0
test/cz/davza/studentadventure/rooms/ClassroomRoomTest.java

@@ -0,0 +1,71 @@
+package cz.davza.studentadventure.rooms;
+
+import cz.davza.studentadventure.Items.Book;
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class ClassroomRoomTest {
+
+    private GameData context;
+
+    @BeforeEach
+    void setUp() {
+        context = TestHelpers.buildGameData();
+        // Bedroom → Hallway
+        context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("hallway"));
+        context.getMap().commitRoomChange();
+    }
+
+    private ClassroomRoom getClassroomRoom() {
+        return (ClassroomRoom) context.getMap().getCurrentRoom().getExit("classroom");
+    }
+
+    @Test
+    void enterWithoutBookReturnsFailed() {
+        CommandResult result = getClassroomRoom().onEnter(context);
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+
+    @Test
+    void enterWithoutBookAppliesStressPenalty() {
+        int stressBefore = context.getPlayer().getStress().getValue();
+        getClassroomRoom().onEnter(context);
+        assertEquals(stressBefore + 15, context.getPlayer().getStress().getValue());
+    }
+
+    @Test
+    void enterWithoutBookCancelsRoomChange() {
+        getClassroomRoom().onEnter(context);
+        assertFalse(context.getMap().hasPendingRoomChange());
+    }
+
+    @Test
+    void enterWithBookReturnsSuccess() {
+        context.getPlayer().getInventory().Add(new Book(), 1);
+        CommandResult result = getClassroomRoom().onEnter(context);
+        assertEquals(CommandState.SUCCESS, result.getResultState());
+    }
+
+    @Test
+    void enterWithBookDoesNotAddStress() {
+        context.getPlayer().getInventory().Add(new Book(), 1);
+        int stressBefore = context.getPlayer().getStress().getValue();
+        getClassroomRoom().onEnter(context);
+        assertEquals(stressBefore, context.getPlayer().getStress().getValue());
+    }
+
+    @Test
+    void classroomHasSleepCommand() {
+        assertTrue(getClassroomRoom().getAllowedCommands().contains("sleep"));
+    }
+
+    @Test
+    void classroomHasStickyNote() {
+        assertNotNull(getClassroomRoom().getItems().GetByItemCode("sticky_note"));
+    }
+}

+ 95 - 0
test/cz/davza/studentadventure/rooms/ExamRoomRoomTest.java

@@ -0,0 +1,95 @@
+package cz.davza.studentadventure.rooms;
+
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.*;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class ExamRoomRoomTest {
+
+    private GameData context;
+    private ExamRoomRoom examRoom;
+
+    @BeforeEach
+    void setUp() {
+        context  = TestHelpers.buildGameData();
+        examRoom = new ExamRoomRoom(context);
+    }
+
+    // ── onEnter ───────────────────────────────────────────────────────────────
+
+    @Test
+    void onEnterReturnsActionPending() {
+        assertEquals(CommandState.ACTION_PENDING, examRoom.onEnter(context).getResultState());
+    }
+
+    @Test
+    void onEnterSetsHandlerToExamRoom() {
+        CommandResult result = examRoom.onEnter(context);
+        assertEquals(examRoom, result.getNewCommandHandler());
+    }
+
+    // ── handleCommand — correct ID ────────────────────────────────────────────
+
+    @Test
+    void correctStudentIdResumesAction() {
+        String id = context.getPlayer().getId();
+        CommandResult result = examRoom.handleCommand(id, context);
+        assertEquals(CommandState.ACTION_RESUMED, result.getResultState());
+    }
+
+    @Test
+    void correctStudentIdIsCaseSensitive() {
+        // ID is generated as name+random — verify exact match is required
+        String id = context.getPlayer().getId();
+        CommandResult correct = examRoom.handleCommand(id, context);
+        assertEquals(CommandState.ACTION_RESUMED, correct.getResultState());
+    }
+
+    // ── handleCommand — wrong ID ─────────────────────────────────────────────
+
+    @Test
+    void wrongStudentIdReturnsFailed() {
+        CommandResult result = examRoom.handleCommand("wrongid123", context);
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+
+    @Test
+    void wrongIdKeepsPlayerOutOfRoom() {
+        examRoom.handleCommand("wrongid", context);
+        // map should have no pending room change set by exam room
+        // (ExamRoomRoom doesn't set nextRoom on wrong ID — that's correct behaviour)
+        assertFalse(context.getMap().hasPendingRoomChange());
+    }
+
+    // ── handleCommand — leave ────────────────────────────────────────────────
+
+    @Test
+    void leaveResumesAction() {
+        CommandResult result = examRoom.handleCommand("leave", context);
+        assertEquals(CommandState.ACTION_RESUMED, result.getResultState());
+    }
+
+    @Test
+    void leaveIsCaseInsensitive() {
+        assertEquals(CommandState.ACTION_RESUMED,
+                examRoom.handleCommand("LEAVE", context).getResultState());
+    }
+
+    @Test
+    void leaveClearsNextRoom() {
+        context.getMap().setNextRoom(examRoom); // simulate pending entry
+        examRoom.handleCommand("leave", context);
+        assertFalse(context.getMap().hasPendingRoomChange());
+    }
+
+    // ── Room contents ─────────────────────────────────────────────────────────
+
+    @Test
+    void examRoomAllowsTakeExamCommand() {
+        assertTrue(examRoom.getAllowedCommands().contains("takeexam"));
+    }
+}

+ 83 - 0
test/cz/davza/studentadventure/rooms/ExamRoomTest.java

@@ -0,0 +1,83 @@
+package cz.davza.studentadventure.rooms;
+
+import cz.davza.studentadventure.Items.Book;
+import cz.davza.studentadventure.TestHelpers;
+import cz.davza.studentadventure.enums.CommandState;
+import cz.davza.studentadventure.objects.CommandResult;
+import cz.davza.studentadventure.objects.GameData;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class ExamRoomTest {
+
+    private GameData context;
+    private ExamRoomRoom examRoom;
+
+    @BeforeEach
+    void setUp() {
+        context = TestHelpers.buildGameData();
+        // Navigate to exam room entrance: bedroom → hallway → classroom → examroom
+        context.getPlayer().getInventory().Add(new Book(), 1);
+        context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("hallway"));
+        context.getMap().commitRoomChange();
+        context.getMap().setNextRoom(context.getMap().getCurrentRoom().getExit("classroom"));
+        context.getMap().commitRoomChange();
+        examRoom = (ExamRoomRoom) context.getMap().getCurrentRoom().getExit("examroom");
+    }
+
+    @Test
+    void onEnterReturnsActionPending() {
+        CommandResult result = examRoom.onEnter(context);
+        assertEquals(CommandState.ACTION_PENDING, result.getResultState());
+    }
+
+    @Test
+    void onEnterSetsExamRoomAsHandler() {
+        CommandResult result = examRoom.onEnter(context);
+        assertEquals(examRoom, result.getNewCommandHandler());
+    }
+
+    @Test
+    void correctIdAllowsEntry() {
+        examRoom.onEnter(context);
+        String correctId = context.getPlayer().getId();
+        CommandResult result = examRoom.handleCommand(correctId, context);
+        assertEquals(CommandState.ACTION_RESUMED, result.getResultState());
+    }
+
+    @Test
+    void wrongIdReturnsFailed() {
+        examRoom.onEnter(context);
+        CommandResult result = examRoom.handleCommand("wrongid999", context);
+        assertEquals(CommandState.FAILED, result.getResultState());
+    }
+
+    @Test
+    void leaveCommandCancelsEntry() {
+        examRoom.onEnter(context);
+        CommandResult result = examRoom.handleCommand("leave", context);
+        assertEquals(CommandState.ACTION_RESUMED, result.getResultState());
+    }
+
+    @Test
+    void leaveCommandCancelsRoomChange() {
+        context.getMap().setNextRoom(examRoom);
+        examRoom.onEnter(context);
+        examRoom.handleCommand("leave", context);
+        assertNull(context.getMap().getNextRoom());
+    }
+
+    @Test
+    void idCheckIsCaseInsensitive() {
+        examRoom.onEnter(context);
+        String id = context.getPlayer().getId();
+        CommandResult result = examRoom.handleCommand(id.toUpperCase(), context);
+        assertEquals(CommandState.ACTION_RESUMED, result.getResultState());
+    }
+
+    @Test
+    void examRoomHasTakeExamCommand() {
+        assertTrue(examRoom.getAllowedCommands().contains("takeexam"));
+    }
+}