2 Коміти fba52f4aea ... 21ce4ef69c

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

+ 0 - 25
.gitignore

@@ -1,25 +0,0 @@
-# ---> Java
-*.class
-
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
-*.jar
-*.war
-*.ear
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
-
-# ---> Maven
-target/
-pom.xml.tag
-pom.xml.releaseBackup
-pom.xml.versionsBackup
-pom.xml.next
-release.properties
-dependency-reduced-pom.xml
-buildNumber.properties
-.mvn/timing.properties
-

+ 0 - 8
LICENSE

@@ -1,8 +0,0 @@
-MIT License
-Copyright (c) <year> <copyright holders>
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 0 - 3
README.md

@@ -1,3 +0,0 @@
-# JavaAdventureRepo
-
-Code for Java text adventure. Have fun

+ 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"));
+    }
+}