Compare commits

...
This repository has been archived on 2022-12-21. You can view files and clone it, but cannot push or open issues or pull requests.

34 Commits

Author SHA1 Message Date
Claudio Maggioni 75052a42c3 Added assginment 2022-12-21 10:19:56 +01:00
Claudio Maggioni 6c1b074846 Added report 2022-12-20 16:59:15 +01:00
Claudio Maggioni 6d1d80d022 Minor fixes 2022-11-22 18:46:05 +01:00
Claudio Maggioni e8a108d89f Minor fixes 2022-11-22 18:44:40 +01:00
Claudio Maggioni 6df81726d1 Minor cleanup 2022-11-22 18:36:17 +01:00
Claudio Maggioni 4c70ba2800 Removed ZorkMap 2022-11-22 18:32:53 +01:00
RaffaeleMorganti 5786e58842 output stream now in zorkgame 2022-11-22 18:03:09 +01:00
RaffaeleMorganti d2ce23fead actions print on stream 2022-11-22 17:25:40 +01:00
RaffaeleMorganti 6341503a70 actions print error once 2022-11-22 16:46:23 +01:00
Claudio Maggioni aafbf94434 Minor cleanup 2022-11-22 15:20:35 +01:00
Claudio Maggioni 69072097bb Trigger evaluation moved from Zork class 2022-11-22 15:12:55 +01:00
Claudio Maggioni d0d2db70a4 Parser refactor complete 2022-11-22 13:43:15 +01:00
Claudio Maggioni 9564a205dc Minor cleanups 2022-11-22 11:32:57 +01:00
Claudio Maggioni 718e736032 Encapsulation on zork types 2022-11-22 11:27:56 +01:00
Claudio Maggioni 37f6bc2ff9 Parser strategies for all entities 2022-11-22 10:16:26 +01:00
RaffaeleMorganti 214bed9a13 move userInput to method arguments and add some final 2022-11-22 09:55:26 +01:00
RaffaeleMorganti 42732e0fdd switch from harcoded string to enum and remove complexity in some actions 2022-11-22 00:46:20 +01:00
Claudio Maggioni 52d98a4ece parser partial refactor 2022-11-21 22:29:36 +01:00
Claudio Maggioni a782ec5ad7 Deduplicated print-action logic 2022-11-21 17:10:08 +01:00
Claudio Maggioni 40675f5a39 Command pattern over actions - tested, it works 2022-11-21 16:36:38 +01:00
RaffaeleMorganti 05a63e0399 some attribute visibility improvements, added sonar-scanner to maven, bug fix 2022-11-20 14:11:58 +01:00
RaffaeleMorganti e6b96d69d1 ZorkReader refactored 2022-11-19 10:46:28 +01:00
RaffaeleMorganti 4a7eca5302 changed types of some collections 2022-11-19 08:48:21 +01:00
RaffaeleMorganti 2e12ab7f1f remove duplicate operation on trigger 2022-11-18 21:00:28 +01:00
RaffaeleMorganti 5d098bf32d Zork class extract methods on actions and triggers 2022-11-18 16:56:32 +01:00
RaffaeleMorganti 0a5c67ea69 pom regression 2022-11-17 22:17:14 +01:00
RaffaeleMorganti f4dc0fac73 fixed test 2022-11-17 21:58:02 +01:00
RaffaeleMorganti 4b01c0a85c kind of test 2022-11-17 00:51:58 +01:00
Claudio Maggioni 6b3d2d142c removed duplication in ZorkReader with static methods 2022-11-16 17:56:58 +01:00
Claudio Maggioni 8e54d0684b ZorkConditions are immutable objects 2022-11-16 17:42:56 +01:00
RaffaeleMorganti 7cc2352b90 command 2022-11-16 17:35:32 +01:00
RaffaeleMorganti 008818c357 parser moved outside 2022-11-16 17:18:19 +01:00
Claudio Maggioni 6e374918b5 refactor: initial IDEA-aided refactors on syntax 2022-11-16 16:36:08 +01:00
Claudio Maggioni 2406dac418 chore: migrate to Maven project, update README 2022-11-16 16:18:00 +01:00
60 changed files with 2585 additions and 1490 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/*.iml
/.idea/
/target/

Binary file not shown.

View File

@ -1,5 +0,0 @@
all:
javac *.java
clean:
rm -rf *.class

23
README
View File

@ -1,23 +0,0 @@
###################
# To compile code #
###################
make all
###################
# To cleanup code #
###################
make clean
###################
# To execute code #
###################
java Zork [game xml]
######################
# Sample Walkthrough #
######################
(Example inputs can be found in RunThroughResults.txt as to how to beat the sample game)
java Zork sampleGame.xml

View File

@ -2,3 +2,21 @@
Original and refactored sources for the https://github.com/dtschust/Zork Github
project.
# Building
Build the project with the command:
```shell
mvn clean package
```
# Executing
Example inputs can be found in RunThroughResults.txt as to how to beat the sample game. To run the sample game execute:
```shell
mvn exec:java
```
This command will load the `sampleGame.xml` XML game spec.

View File

@ -1,9 +1,6 @@
Sample Run Through
>IPA1 sample.xml
You find yourself at the mouth of a cave and decide that in spite of common sense and any sense of self preservation that you're going to go exploring north into it. It's a little dark, but luckily there are some torches on the wall.
>e
Cant go that way.
Can't go that way.
>N
Error
>n
@ -34,7 +31,7 @@ Error
>attack gnome with face!
Error
>w
Cant go that way.
Can't go that way.
>read chest
Error
>attack chest with torch
@ -48,9 +45,9 @@ chest contains explosive.
>take explosive
Item explosive added to inventory.
>open chest
chest is empty.
chest is empty
>i
Inventory: torch, explosive
Inventory: explosive, torch
>attack gnome with explosive
Error
>read explosive

1455
Zork.java

File diff suppressed because it is too large Load Diff

127
pom.xml Normal file
View File

@ -0,0 +1,127 @@
<?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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.dtschust</groupId>
<artifactId>zork</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.stefanbirkner</groupId>
<artifactId>system-lambda</artifactId>
<version>1.2.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>23.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mainClass>com.github.dtschust.zork.Zork</mainClass>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>${mainClass}</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>${mainClass}</mainClass>
<arguments>
<argument>${project.basedir}/sampleGame.xml</argument>
</arguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.0</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.9.1.2184</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.8</version>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>coverage</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

BIN
report.pdf Normal file

Binary file not shown.

333
report.tex Normal file
View File

@ -0,0 +1,333 @@
\documentclass[a4paper,11pt]{scrartcl}
\topskip=0pt
\parskip=5pt
\parindent=0pt
%\baselineskip=0pt
\usepackage[utf8]{inputenc}
\usepackage{geometry}
\usepackage{enumitem}
\usepackage{lmodern}
\usepackage{multirow}
\usepackage{graphicx}
\usepackage{booktabs}
\usepackage{listings}
\usepackage{float}
\usepackage{hyperref}
\usepackage{cleveref}
\usepackage{listings}
\usepackage[justification=centering]{caption}
\usepackage[bottom]{footmisc}
\usepackage{xcolor}
\definecolor{codegreen}{rgb}{0,0.6,0}
\definecolor{codegray}{rgb}{0.5,0.5,0.5}
\definecolor{codepurple}{rgb}{0.58,0,0.82}
\definecolor{backcolour}{rgb}{0.95,0.95,0.92}
\lstdefinestyle{mystyle}{
backgroundcolor=\color{backcolour},
commentstyle=\color{codegreen},
keywordstyle=\color{magenta},
keywordstyle=[2]{\color{olive}},
numberstyle=\tiny\color{codegray},
stringstyle=\color{codepurple},
basicstyle=\ttfamily\footnotesize,
breakatwhitespace=false,
breaklines=true,
captionpos=b,
keepspaces=true,
numbers=left,
numbersep=5pt,
showspaces=false,
showstringspaces=false,
showtabs=false,
tabsize=2,
aboveskip=0.8em,
belowcaptionskip=0.8em
}
\lstset{style=mystyle}
\geometry{left=2cm,right=2cm,top=2cm,bottom=3cm} % se serve spazio
\title{
\vspace{-5ex} % se serve spazio
Assignment 3 -- Software Design and Modelling \\\vspace{0.5cm}
\Large Refactoring the Design of an Existing Project
\vspace{-1ex} % se serve spazio
}
\author{Claudio Maggioni \and Raffaele Morganti}
\date{\vspace{-3ex}} % se serve spazio
\begin{document}
\maketitle
\section{Project Selection}
The aim of this project is to refactor a part of or a complete application, to achieve good software design without changing the application behaviour. No restrictions are placed on the size or type of project, other than being hosted on GitHub as a public repository.
We choose the project \textbf{\href{https://github.com/dtschust/Zork}{dtschust/Zork}}, which is inspired by the namesake game \textit{Zork}\footnote{\url{https://en.wikipedia.org/wiki/Zork}} released in the early 1980s.
This project is written in Java and it provides a configurable framework of text-based adventure game mechanics. The program is able to parse and execute a given story, which must be provided in an XML file following a specific format.
We choose this project mainly for two reasons:
\begin{itemize}[itemsep=0pt,topsep=0pt]
\item It is small enough to be deeply understood and refactored in a couple of weeks. According to \textit{cloc} and as shown in \cref{fig:cloc}, the project has less than 2000 lines of executable code;
\item The project has a large number of design anti-patterns, and we think we have an opportunity to perform a significant and interesting refactor.
\end{itemize}
\begin{figure}[h]
\centering
\begin{tabular}{lrrrr}
\toprule
Language & Files & Blank & Comment & Code \\
\midrule
Java & 1 & 114 & 77 & 1264 \\
XML & 10 & 0 & 0 & 530 \\
Text & 1 & 1 & 0 & 83 \\
make & 1 & 1 & 0 & 4 \\
Markdown & 1 & 1 & 0 & 3 \\
Properties & 1 & 0 & 2 & 3 \\
\midrule
Total & 15 & 117 & 79 & 1887 \\
\bottomrule
\end{tabular}
\caption{Output of the \textit{cloc} tool for the \textbf{dtschust/Zork} project before refactoring.}
\label{fig:cloc}
\end{figure}
\section{The Project Before Refactoring}
A copy of the contents of \textbf{dtschust/Zork} can be found in branch \texttt{main} of the repository
\begin{center}
{\small\href{https://gitlab.com/usi-si-teaching/msde/2022-2023/software-design-and-modeling/assignment-3-refactoring/group7}{usi-si-teaching/msde/2022-2023/software-design-and-modeling/assignment-3-refactoring/group7}}
\end{center}
on \textit{gitlab.com}.
As figure \ref{fig:cloc} proves, all the implementation code is contained in a single Java file. A total of 1264 code lines, 77 comments, and 114 blank lines are found. As the project is composed by only 11 classes, we decide to manually inspect the source code to find instances of bad design that we can improve with refactoring.
The \textit{Zork} class is over 1200 lines long (including blanks and comments) and handles almost all the application logic, making it an obvious instance of the god class anti-pattern. The class violates the single choice responsibility principle, as it handles both XML parsing and the actual execution of the given story. Given that the XML story file specification is non-trivial, the parsing logic may be modular. However, the relevant code is all placed in a single method, as shown in \cref{lst:pre-zork}.
\marginpar[right text]{\color{white}\url{https://youtu.be/icpS6CgfGLo}}
\begin{lstlisting}[caption=An excerpt from the XML parsing logic found in the \textit{Zork} class. All the contents of the files are parsed in this section\, without any delegation to other methods or classes other than the Java DOM API.,language=java,label=lst:pre-zork]
Element rootElement = doc.getDocumentElement();
/* Every single first generation child is a room, container, creature, or item. So load them in*/
NodeList nodes = rootElement.getChildNodes();
for (k=0;k<nodes.getLength();k++)
{
Node node = nodes.item(k);
Element element;
if (node instanceof Element)
{
/* [511 lines omitted] */
}
}
\end{lstlisting}
5 of the other 10 classes, namely \textit{ZorkObject}, \textit{ZorkCreature}, \textit{ZorkItem}, \textit{ZorkContainer} and \textit{ZorkRoom} do not contain any methods, making them data clumps. The sources for \textit{ZorkContainer} are shown in \cref{lst:pre-cont} as an example of this. Attributes in classes are generally public and mutable, allowing other classes to modify their internal state. This indicates low encapsulation and tight coupling between the modules in this application.
\begin{lstlisting}[caption=Complete source code for the \textit{ZorkContainer} class before refactoring.,language=java,label=lst:pre-cont]
class ZorkContainer extends ZorkObject
{
public String name;
public HashMap<String,String> item = new HashMap<String,String>();
public String description;
public ArrayList<String> accept = new ArrayList<String>();
boolean isOpen;
public ZorkContainer()
{
}
}
\end{lstlisting}
The class \textit{ZorkTrigger} encodes an action to be performed in the game state when a given condition is met. The class however, is subject to feature envy by the aforementioned \textit{Zork} class. While condition evaluation is correctly implemented inside it (specifically in the method \textit{boolean evaluate(Zork)}), the action executing code is instead placed in the god class. Moreover, the condition evaluation is itself envious of \textit{Zork} as it takes the responsibility to fetch the current user input (stored in field \textit{String Zork.userInput}). Listing \ref{lst:pre-cond} shows the code where this takes place.
\begin{lstlisting}[caption=The class \textit{ZorkCommand} (a sub-class of \textit{ZorkCondition}) is envious of how to fetch the current user input by retrieving it from a public field in \textit{Zork} class.,language=java,label=lst:pre-cond]
class ZorkCommand extends ZorkCondition
{
String command;
public boolean evaluate(Zork zork)
{
if (command.equals(zork.userInput))
return true;
else
return false;
}
}
\end{lstlisting}
Finally, we execute the \textit{Sonarqube} tool to have a wider overview of the project in terms of metrics. A summary of the \textit{Sonar Scanner} output is shown in \cref{fig:sonar_initial}. Unsurprisingly the results are not stellar. In particular, we want to point out the ``Cognitive Complexity'' metric, which is related to the number of boolean conditions checked in methods. Our refactoring manages to lower its value by a factor of 6, showing how the project originally has a very high density of logic per method.
\begin{figure}[ht]
\centering
\includegraphics[width=\textwidth,clip]{sonar_initial.png}
\caption{Summary of the \textit{Sonar Scanner} detection on the initial project}
\label{fig:sonar_initial}
\end{figure}
In conclusion, we determine that the code in \textbf{dtschust/Zork} does not follow OOP design best practices, in particular falling short of providing loose coupling and defining an effective separation of concerns between its classes. Our refactor aims to introduce better design to the project to increase modularity, which in turn increases the testability of each class and the readability of the overall code base.
\section{Refactoring}
A copy our refactor of \textbf{dtschust/Zork} can be found in branch \texttt{refactored} of the repository
\begin{center}
{\small\href{https://gitlab.com/usi-si-teaching/msde/2022-2023/software-design-and-modeling/assignment-3-refactoring/group7/-/tree/refactored}{usi-si-teaching/msde/2022-2023/software-design-and-modeling/assignment-3-refactoring/group7}}
\end{center}
on \textit{gitlab.com}.
\subsection{Source Code Structure and Build System}
The first change we make on the repository is to introduce \textit{Maven} as a build system and following its standard directory structure mandated for source code and tests. We also split the \textit{Zork.java} file in several files, one per class, and we move each class under the \textit{com.github.dtschust.zork} package as placing classes in the default package (their original position) is considered bad practice in Java.
These steps are relatively trivial but however crucial to make our work easier with further refactoring efforts. Additionally, this simplified the implementation of (originally missing) test code to check the behaviour of the program remains the same.
\subsection{XML Parsing Logic}
\begin{figure}[ht]
\centering
\includegraphics[width=\textwidth,clip]{parser.png}
\caption{UML class diagram of the \textit{com.github.dtschust.zork.parser} package}
\label{fig:umlp}
\end{figure}
We then separate the XML parsing logic in the package \textit{com.github.dtschust.zork.parser}, whose UML class diagram is shown in \cref{fig:umlp}.
Here, the XML-specific logic is separate from the application-specific allocation of the story data structure.
DOM elements and lists of elements are encapsulated in the \textit{dom.DOMElement} and \textit{dom.DOMElementList} proxy classes. These classes in turn are used to implement an encoding-agnostic interface named \textit{Property}, which abstracts the story file to a collection of property names, property values and sub-properties.
At each nesting level a different parsing strategy is defined as a class under the \textit{strategy} package. Each class implements a method that takes a \textit{Property} and returns an instance of the matching application-specific entity. Each strategy may depend to other strategies through dependency injection, with inversion of control handled by the \textit{ParserIOC} class.
The parser is now a collection of 11 classes and 3 interfaces with a single entry point. With the line of code \texttt{ParserIOC.xmlParser().parse(filename, System.out)} the entire story data structure is parsed from the XML file and instantiated. Our new design increases testability by separating each nesting level into easily mockable classes. Moreover, the new abstractions allow for easy implementation of parsers for other encodings.
\subsection{User Actions}
\begin{lstlisting}[caption=Implementation of the \textit{read} action in the command class \textit{action.ReadAction}.,language=java,label=lst:readaction]
public class ReadAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return arguments.get(0).equals("read");
}
@Override
public int getMinimumArgCount() {
return 2;
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
return game.getItem(arguments.get(1)).map(i -> {
game.stream.println(i.getWriting());
return true;
}).orElse(false);
}
}
\end{lstlisting}
We decide to implement the execution of user invoked or triggered actions as a variation of the command pattern. All this logic is now in the package \textit{com.github.dtschust.zork.repl}.
All actions are implemented as a command class under the \textit{action} package, which implement a method \texttt{run} taking as parameters the story data and returning a \textit{boolean} indicating success of failure. An example of command class is shown in \cref{lst:readaction}.
The dispatcher is implemented in the class \textit{ActionDispatcher}. Our variation of the command pattern makes this dispatcher responsible for parsing the action string coming from triggers and the user, choosing the right action to invoke aided by methods in each command class, and dispatching the command providing the parsed action arguments as parameters of the method \textit{run}. This variation is done to cater the domain-specific need of providing a text based REPL (Read-eval-print loop) like interface to the user.
\subsection{Story Data}
In the overall project we also perform several refactor on the classes modelling the story data and the game state. Fields on these classes are now immutable whenever possible and always \textit{private} to provide better information hiding and to loosen coupling. Additionally, the feature envy anti-patterns are solved by moving the respective methods into the classes they fit in. This required an extensive refactor of the the trigger-related logic from methods in class \textit{Zork} to methods in \textit{GameData} and improved condition evaluation methods in the \textit{ZorkCondition} hierarchy exploiting dynamic dispatch.
\section{Testing}
A problem we found was the absence of tests, so we needed to write them to ensure to don't introduce behavioral changes.
The original GitHub repository includes the two files named \textit{sampleGame.xml} and \textit{RunThroughResults.txt}. The first contains the game configuration (with the definition of all the rooms, items, creatures, etc. in the game). The latter is instead a log-file containing a sequence of inputs and their respective output.
We used these files to build a system test. In particular, our test is starting the game in a thread and mocks the default system input/output interface to automatically send the commands to the game. The implementation logic of this test is shown in \cref{lst:test}.
\begin{lstlisting}[caption={Code of the system test we implemented.},language=java,label={lst:test}]
@Test
void testSampleGame() {
String gameConfig = "sampleGame.xml";
String gameExecution = "RunThroughResults.txt";
CommandReader run = new CommandReader(gameExecution);
IOWrapper io = new IOWrapper(true);
new Thread(() -> {
try {
catchSystemExit(() -> Zork.runZork(gameConfig));
} catch (Exception ignored) {}
}).start();
while(true){
switch(run.getInstructionType()) {
case SEND:
io.write(run.getInstruction());
break;
case RECV:
assertEquals(run.getInstruction(), io.read());
break;
default:
io.restore();
return;
}
}
}
\end{lstlisting}
Although it doesn't cover all of the possible outcomes, it was the best option available to ensure the behavioral consistency. Since this game has been realized as an assignment for a university course, a \textit{zorkRequirements.pdf} file with all the specification is available, and we also relied on it.
To verify the behavior of the initial project, we don't added tests to the original sources, but to the ones already converted to a Maven project for convenience.
A copy of the tests added to the \textbf{dtschust/Zork} can be found in branch \texttt{main-with-test} of the repository
\begin{center}
{\small\href{https://gitlab.com/usi-si-teaching/msde/2022-2023/software-design-and-modeling/assignment-3-refactoring/group7}{usi-si-teaching/msde/2022-2023/software-design-and-modeling/assignment-3-refactoring/group7}}
\end{center}
on \textit{gitlab.com}.
\section{Conclusions}
At the end of the refactoring we ran again \textit{Sonar Scanner}, with a summary of the results in \cref{fig:sonar_final}.
\begin{figure}[ht]
\centering
\includegraphics[width=\textwidth,clip]{sonar_final.png}
\caption{Summary of the \textit{Sonar Scanner} detection on the refactored project}
\label{fig:sonar_final}
\end{figure}
By comparing these with the ones in \cref{fig:sonar_final} described at the start of this report, we can asses there is a huge improvement. The main benefits are the reduced ``cognitive complexity" from a score of 631 to 108 and the removal of code duplication.
In the end the project size is almost identical, however the source deeply changed, with the initial 20 methods and 11 classes extracted in a total of 171 methods and 47 classes. Now the code takes advantage from the basic object-oriented programming principles.
At the first impact the code wasn't really easy to understand, so our first steps in our refactor process were the method extraction of logically separated tasks. After that we started to extract classes and find some design-patterns (like the command pattern described above). Only in the end we cared about the visibility of the attributes and we enforced encapsulation. By working with this step-by-step strategy made our job easier and we didn't encountered any insurmountable barrier. Approaching to refactor in a continuous way allowed us to don't stuck with too hard and too big changes to be done in a single run. If we need to state what was the hardest task, we found the implementation of the command pattern the most challenging overall.
\section{Future work}
As always happens when talking about refactoring, it will be always possible to do something more. However we think our extensive refactor improved all aspects of this project and removed all the anti-patterns.
Still some improvements are possible, as also the last run of \textit{Sonar Scanner} shows in the ``code smells" section. In particular the 3 detected as ``critical" are related to the usage of the generics (\cref{lst:future}) and they can be removed. However since in all the three cases these generics are bounded we don't see to this as a high priority issue and we consider the required effort bigger than any potential benefit of fixing these.
\begin{lstlisting}[caption={Usage of wildcard types in the code.}, language=java,label={lst:future}]
// class ZorkGame
public Optional<? extends ZorkObject> getObject(final String objectName);
//class Property
List<? extends Property> subProperties();
List<? extends Property> subPropertiesByName(String name);
\end{lstlisting}
The 6 ``major" issues instead, are less interesting:
\begin{itemize}
\item Two of them are about the number of parameters in the constructor for the \textit{ZorkRoom} and \textit{ZorkCreature} classes, but they are instantiated through the respective \textit{StrategyParser.parser} method that is working as a builder.
\item The other four are related to the usage of calls to the \textit{System.out} (all of them are sort of logs printed when some exceptions are thrown if trying to load of a wrong game configuration file). Making more informative logs of the causes of these exception would certainly be useful, but this improvement is out of the scope of refactoring.
\end{itemize}
In conclusion, although \textit{Sonar Scanner} detected them, we don't think they are really relevant.
\end{document}

View File

@ -0,0 +1,50 @@
/*Drew Schuster
dtschust
ECE462
*/
package com.github.dtschust.zork;
import com.github.dtschust.zork.parser.ParserIOC;
import com.github.dtschust.zork.repl.ActionDispatcher;
import java.util.Scanner;
/* And away we go*/
public class Zork {
public static void runZork(final String filename) {
ZorkGame game = ParserIOC.xmlParser().parse(filename, System.out);
ActionDispatcher d = new ActionDispatcher(game);
/* starting the game!*/
d.dispatch("Start at Entrance");
/* There is no stopping in Zork, until we're done!!*/
while (game.isRunning()) {
Scanner input = new Scanner(System.in);
String userInput = input.nextLine();
/*Now that we have the user command, check the input*/
if (!game.evaluateTriggers(userInput)) {
/* If we haven't skipped, perform the user action*/
d.dispatch(userInput);
// check the triggers again (various states have changed, gnomes need to be found!)
game.evaluateTriggers("");
}
}
// single point of termination
System.exit(0);
}
/* I love how basic java main functions are sometimes.*/
public static void main(final String[] args) {
if (args.length != 1) {
System.out.println("Usage: java Zork [filename]");
return;
}
runZork(args[0]);
}
}

View File

@ -0,0 +1,14 @@
package com.github.dtschust.zork;
/* Special Command condition */
public class ZorkCommand {
private final String commandName;
public ZorkCommand(String command) {
this.commandName = command;
}
public boolean matchesInput(String userInput) {
return commandName.equals(userInput);
}
}

View File

@ -0,0 +1,12 @@
package com.github.dtschust.zork;
/* Generic condition*/
public abstract class ZorkCondition {
protected final String object;
protected ZorkCondition(String object) {
this.object = object;
}
public abstract boolean evaluate(ZorkGame game);
}

View File

@ -0,0 +1,30 @@
package com.github.dtschust.zork;
/* Has conditions*/
public class ZorkConditionHas extends ZorkCondition {
private final String has;
private final String owner;
public ZorkConditionHas(String has, String object, String owner) {
super(object);
this.has = has;
this.owner = owner;
}
@Override
public boolean evaluate(ZorkGame game) {
// Inventory is a special case as it isn't the name of any object in the game, check for it specifically
if (owner.equals("inventory")) {
return evaluateCondition(game.inventory.contains(object));
} else {
return game.getRoom(owner).map(r -> evaluateCondition(r.containsItem(object))).orElseGet(() ->
game.getContainer(owner).map(c -> evaluateCondition(c.containsItem(object))).orElse(false));
}
}
private boolean evaluateCondition(boolean contained) {
if (has.equals("yes")) return contained;
else if (has.equals("no")) return !contained;
return false;
}
}

View File

@ -0,0 +1,16 @@
package com.github.dtschust.zork;
/* Status conditions*/
public class ZorkConditionStatus extends ZorkCondition {
private final String status;
public ZorkConditionStatus(final String status, final String object) {
super(object);
this.status = status;
}
@Override
public boolean evaluate(final ZorkGame game) {
return game.getObject(object).map(o -> o.isStatusEqualTo(status)).orElse(false);
}
}

View File

@ -0,0 +1,126 @@
package com.github.dtschust.zork;
import com.github.dtschust.zork.objects.*;
import com.github.dtschust.zork.types.ObjectCollector;
import com.github.dtschust.zork.types.ZorkMapByName;
import java.io.PrintStream;
import java.util.*;
import java.util.stream.Stream;
public class ZorkGame {
public final PrintStream stream;
public final Set<String> inventory = new HashSet<>();
private final ZorkMapByName<ZorkRoom> rooms;
private final ZorkMapByName<ZorkItem> items;
private final ZorkMapByName<ZorkContainer> containers;
private final ZorkMapByName<ZorkCreature> creatures;
private boolean running = false;
private String currentRoom;
public ZorkGame(final Collection<ZorkRoom> rooms,
final Collection<ZorkItem> items,
final Collection<ZorkContainer> containers,
final Collection<ZorkCreature> creatures,
final PrintStream stream) {
this.stream = stream;
this.rooms = new ZorkMapByName<>(rooms);
this.items = new ZorkMapByName<>(items);
this.containers = new ZorkMapByName<>(containers);
this.creatures = new ZorkMapByName<>(creatures);
}
public ZorkRoom getCurrentRoom() {
return rooms.get(currentRoom).orElseThrow(() ->
new IllegalStateException("current room not found: " + currentRoom));
}
public boolean changeRoom(String newRoom) {
if (rooms.containsName(newRoom)) {
currentRoom = newRoom;
running = true;
return true;
}
return false;
}
public boolean isRunning() {
return running;
}
public void setGameOver() {
running = false;
}
public void removeFromBorders(final ZorkRoom room) {
for (final ZorkRoom bordering : rooms.values()) {
bordering.removeBorderingRoom(room.getName());
}
rooms.put(room);
}
public void updateObjectStatus(final String objectName, final String status) {
for (final ZorkMapByName<? extends ZorkObject> map : List.of(containers, rooms, items, creatures)) {
final Optional<? extends ZorkObject> o = map.get(objectName);
if (o.isPresent()) {
o.get().updateStatus(status);
break;
}
}
}
public Optional<? extends ZorkObject> getObject(final String objectName) {
return Stream.of(containers, rooms, items, creatures)
.map(m -> m.get(objectName))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
}
public void addObjectToCollection(final ZorkObject object, final String destinationName) {
for (final ZorkMapByName<? extends ObjectCollector> map : List.of(containers, rooms)) {
final Optional<? extends ObjectCollector> o = map.get(destinationName);
if (o.isPresent()) {
o.get().addObject(object);
return;
}
}
throw new UnsupportedOperationException("destination " + destinationName + " not a room or container");
}
public void removeObjectFromCollections(final ZorkObject object) {
for (final ZorkMapByName<? extends ObjectCollector> map : List.of(containers, rooms)) {
for (ObjectCollector v : map.values()) {
v.removeObject(object);
}
}
}
public Optional<ZorkRoom> getRoom(final String roomName) {
return this.rooms.get(roomName);
}
public Optional<ZorkCreature> getCreature(final String creatureName) {
return this.creatures.get(creatureName);
}
public Optional<ZorkItem> getItem(final String itemName) {
return this.items.get(itemName);
}
public Optional<ZorkContainer> getContainer(final String containerName) {
return this.containers.get(containerName);
}
public boolean evaluateTriggers(final String currentCommand) {
final boolean currentRoom1 = getCurrentRoom().evaluateTriggers(this, currentCommand);
final boolean itemsInInventory = ZorkTrigger.evaluateTriggersFor(inventory.stream(), this, currentCommand);
return currentRoom1 || itemsInInventory;
}
}

View File

@ -0,0 +1,66 @@
package com.github.dtschust.zork;
import com.github.dtschust.zork.types.HasPrintsAndActions;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
public class ZorkTrigger implements HasPrintsAndActions {
private final List<String> print;
private final List<String> action;
/* By default, "single" */
private final ZorkTriggerType type;
private final List<ZorkCondition> conditions;
private final List<ZorkCommand> commands;
public ZorkTrigger(final ZorkTriggerType type,
final List<ZorkCondition> conditions,
final List<ZorkCommand> commands,
final List<String> print,
final List<String> action) {
this.conditions = conditions;
this.commands = commands;
this.print = print;
this.action = action;
this.type = type;
}
public static boolean evaluateTriggersFor(final Stream<String> collection,
final ZorkGame game,
final String input) {
// non short-circuited to execute all side effects of evaluateTriggers
return collection
.map(game::getObject)
.filter(Optional::isPresent)
.map(Optional::get)
.map(a -> a.evaluateTriggers(game, input))
.reduce(false, (a, b) -> a || b);
}
public boolean hasCommand() {
return !this.commands.isEmpty();
}
public boolean isTriggered(final ZorkGame game, final String currentCommand) {
return commands.stream().allMatch(c -> c.matchesInput(currentCommand)) &&
conditions.stream().allMatch(c -> c.evaluate(game));
}
@Override
public List<String> getPrints() {
return Collections.unmodifiableList(print);
}
@Override
public List<String> getActions() {
return Collections.unmodifiableList(action);
}
public ZorkTriggerType getType() {
return type;
}
}

View File

@ -0,0 +1,19 @@
package com.github.dtschust.zork;
import java.util.EnumSet;
import java.util.Optional;
public enum ZorkTriggerType {
SINGLE("single"),
PERMANENT("permanent");
private final String name;
ZorkTriggerType(final String name) {
this.name = name;
}
public static Optional<ZorkTriggerType> fromName(final String name) {
return EnumSet.allOf(ZorkTriggerType.class).stream().filter(e -> e.name.equals(name)).findAny();
}
}

View File

@ -0,0 +1,72 @@
package com.github.dtschust.zork.objects;
import com.github.dtschust.zork.ZorkTrigger;
import com.github.dtschust.zork.types.ObjectCollector;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/* Container*/
public class ZorkContainer extends ZorkObject implements ObjectCollector {
private final Set<String> items;
private boolean open;
public ZorkContainer(final String name,
final String description,
final String status,
final Collection<String> items,
final Collection<String> accepts,
final Collection<ZorkTrigger> triggers) {
super(name, description, status, triggers);
// If a container has an accepts attribute, then it is always open
this.open = !accepts.isEmpty();
this.items = new HashSet<>(items);
}
public String getContents() {
if (this.items.isEmpty()) {
return getName() + " is empty";
} else {
return getName() + " contains " + String.join(", ", items) + ".";
}
}
public void addItem(final String item) {
this.items.add(item);
}
public void removeItem(final String item) {
this.items.remove(item);
}
public boolean containsItem(final String item) {
return this.items.contains(item);
}
public boolean isOpen() {
return open;
}
public void open() {
open = true;
}
@Override
public void addObject(final ZorkObject object) {
if (!(object instanceof ZorkItem)) {
throw new UnsupportedOperationException(
"a container cannot store " + object.getClass().getSimpleName() + " objects");
}
addItem(object.getName());
}
@Override
public void removeObject(final ZorkObject object) {
if (!(object instanceof ZorkItem)) {
return;
}
removeItem(object.getName());
}
}

View File

@ -0,0 +1,55 @@
package com.github.dtschust.zork.objects;
import com.github.dtschust.zork.ZorkCondition;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.ZorkTrigger;
import com.github.dtschust.zork.types.HasPrintsAndActions;
import java.util.*;
/* Creature*/
public class ZorkCreature extends ZorkObject implements HasPrintsAndActions {
private final Set<String> vulnerabilities;
private final List<ZorkCondition> conditions;
private final List<String> print;
private final List<String> action;
public ZorkCreature(final String name,
final String description,
final String status,
final Collection<ZorkTrigger> triggers,
final Collection<String> vulnerabilities,
final Collection<ZorkCondition> conditions,
final Collection<String> prints,
final Collection<String> actions) {
super(name, description, status, triggers);
this.vulnerabilities = new HashSet<>(vulnerabilities);
this.conditions = new ArrayList<>(conditions);
this.print = new ArrayList<>(prints);
this.action = new ArrayList<>(actions);
}
/* Evaluate the success of an attack*/
/**
* Given a game instance and a weapon, returns whether the attack is successful, i.e. if the creature is vulnerable
* to the weapon and all conditions for a successful attack are satisfied
*
* @param game the game
* @param weapon the weapon
* @return true if the attack is successful
*/
public boolean isAttackSuccessful(final ZorkGame game, final String weapon) {
return vulnerabilities.contains(weapon) && conditions.stream().allMatch(c -> c.evaluate(game));
}
@Override
public List<String> getPrints() {
return Collections.unmodifiableList(print);
}
@Override
public List<String> getActions() {
return Collections.unmodifiableList(action);
}
}

View File

@ -0,0 +1,43 @@
package com.github.dtschust.zork.objects;
import com.github.dtschust.zork.ZorkTrigger;
import com.github.dtschust.zork.types.HasPrintsAndActions;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/* Item*/
public class ZorkItem extends ZorkObject implements HasPrintsAndActions {
private final String writing;
private final List<String> turnOnPrint;
private final List<String> turnOnAction;
public ZorkItem(final String name,
final String description,
final String status,
final String writing,
final Collection<ZorkTrigger> triggers,
final Collection<String> turnOnPrint,
final Collection<String> turnOnAction) {
super(name, description, status, triggers);
this.writing = writing;
this.turnOnPrint = new ArrayList<>(turnOnPrint);
this.turnOnAction = new ArrayList<>(turnOnAction);
}
public String getWriting() {
return writing != null && !writing.isEmpty() ? writing : "Nothing written.";
}
@Override
public List<String> getPrints() {
return Collections.unmodifiableList(turnOnPrint);
}
@Override
public List<String> getActions() {
return Collections.unmodifiableList(turnOnAction);
}
}

View File

@ -0,0 +1,58 @@
package com.github.dtschust.zork.objects;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.ZorkTrigger;
import com.github.dtschust.zork.ZorkTriggerType;
import java.util.*;
/* Generic object, everything inherits from this*/
public abstract class ZorkObject {
private final String name;
private final String description;
private final List<ZorkTrigger> trigger;
private String status;
protected ZorkObject(final String name,
final String description,
final String status,
final Collection<ZorkTrigger> triggers) {
this.name = name;
this.description = description;
this.status = status;
this.trigger = new ArrayList<>(triggers);
}
public void updateStatus(String status) {
this.status = status;
}
public boolean isStatusEqualTo(String status) {
return this.status.equals(status);
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public boolean evaluateTriggers(final ZorkGame game, final String input) {
boolean skip = false;
final Iterator<ZorkTrigger> iterator = trigger.iterator();
while (iterator.hasNext()) {
final ZorkTrigger t = iterator.next();
if (t.isTriggered(game, input)) {
t.printAndExecuteActions(game);
skip = skip || t.hasCommand();
if (t.getType() == ZorkTriggerType.SINGLE) {
iterator.remove();
}
}
}
return skip;
}
}

View File

@ -0,0 +1,91 @@
package com.github.dtschust.zork.objects;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.ZorkTrigger;
import com.github.dtschust.zork.types.ObjectCollector;
import com.github.dtschust.zork.types.ZorkDirection;
import java.util.*;
/* Room*/
public class ZorkRoom extends ZorkObject implements ObjectCollector {
private final String type;
private final Map<ZorkDirection, String> border;
private final Set<String> container;
private final Set<String> item;
private final Set<String> creature;
public ZorkRoom(final String name,
final String description,
final String type,
final String status,
final Collection<ZorkTrigger> triggers,
final Map<ZorkDirection, String> borders,
final Collection<String> containers,
final Collection<String> items,
final Collection<String> creatures) {
super(name, description, status, triggers);
this.type = type;
this.border = new EnumMap<>(borders);
this.container = new HashSet<>(containers);
this.item = new HashSet<>(items);
this.creature = new HashSet<>(creatures);
}
public boolean isExit() {
return "exit".equals(type);
}
public void removeBorderingRoom(String roomName) {
for (final Map.Entry<ZorkDirection, String> d : this.border.entrySet()) {
if (d.getValue().equals(roomName)) {
this.border.remove(d.getKey());
}
}
}
public Optional<String> getBorderingRoom(ZorkDirection border) {
return Optional.ofNullable(this.border.get(border));
}
@Override
public boolean evaluateTriggers(ZorkGame game, String input) {
final boolean items = ZorkTrigger.evaluateTriggersFor(item.stream(), game, input);
final boolean creatures = ZorkTrigger.evaluateTriggersFor(creature.stream(), game, input);
final boolean containers = ZorkTrigger.evaluateTriggersFor(container.stream(), game, input);
return super.evaluateTriggers(game, input) || items || creatures || containers;
}
public Set<String> getContainer() {
return container;
}
public boolean containsItem(final String item) {
return this.item.contains(item);
}
@Override
public void addObject(ZorkObject object) {
if (object instanceof ZorkContainer) {
container.add(object.getName());
} else if (object instanceof ZorkItem) {
item.add(object.getName());
} else if (object instanceof ZorkCreature) {
creature.add(object.getName());
} else {
throw new UnsupportedOperationException("room cannot store " + object.getClass().getSimpleName() + " objects");
}
}
@Override
public void removeObject(ZorkObject object) {
if (object instanceof ZorkContainer) {
container.remove(object.getName());
} else if (object instanceof ZorkItem) {
item.remove(object.getName());
} else if (object instanceof ZorkCreature) {
creature.remove(object.getName());
}
}
}

View File

@ -0,0 +1,28 @@
package com.github.dtschust.zork.parser;
import com.github.dtschust.zork.ZorkCondition;
import com.github.dtschust.zork.objects.ZorkContainer;
import com.github.dtschust.zork.objects.ZorkCreature;
import com.github.dtschust.zork.objects.ZorkItem;
import com.github.dtschust.zork.objects.ZorkRoom;
import com.github.dtschust.zork.parser.strategies.*;
/**
* Inversion of control for Zork parse strategies
*/
public final class ParserIOC {
private static final PropertyParseStrategy<ZorkCondition> condition = new ZorkConditionParseStrategy();
private static final TriggerPropertyParseStrategy trigger = new ZorkTriggerParseStrategy(condition);
private static final PropertyParseStrategy<ZorkContainer> container = new ZorkContainerParseStrategy(trigger);
private static final PropertyParseStrategy<ZorkItem> item = new ZorkItemParseStrategy(trigger);
private static final PropertyParseStrategy<ZorkRoom> room = new ZorkRoomParseStrategy(trigger);
private static final PropertyParseStrategy<ZorkCreature> creature = new ZorkCreatureParseStrategy(condition, trigger);
private static final ZorkParser xmlParser = new ZorkXMLParser(creature, container, item, room);
private ParserIOC() {
}
public static ZorkParser xmlParser() {
return xmlParser;
}
}

View File

@ -0,0 +1,91 @@
package com.github.dtschust.zork.parser;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* A property is an encoding-agnostic representation of a 3-tuple made of:
* - A property name (string);
* - A property value (string);
* - An (optional) list of sub-properties.
*/
public interface Property {
/**
* Returns the property name
*
* @return the property name
*/
String name();
/**
* Returns the property value
*
* @return the property value
*/
String value();
/**
* Returns a list of all sub-properties
*
* @return a list af all sub-properties
*/
List<? extends Property> subProperties();
/**
* Returns a list of the sub-properties matching the given name
*
* @param name the name of the sub-properties
* @return a list of sub-properties
*/
List<? extends Property> subPropertiesByName(String name);
/**
* Given a sub-property name and an optional default value, returns:
* - The sub-property value for the first sub-property with the given name, or;
* - If no such sub-properties are found and if the default value is given, the default value, or;
* - nothing, throwing an exception, if no default value is given
*
* @param elementName the sub-property name
* @param defaultValue the default value or Optional.empty()
* @return the sub-property value
* @throws IllegalStateException when the default value is not given and no sub-property is found
*/
String subPropertyValue(String elementName, Optional<String> defaultValue);
/**
* Returns whether at least one sub-property with the given name is found
*
* @param name the sub-property name
* @return whether at least one sub-property with the given name is found
*/
boolean hasSubProperty(String name);
/**
* Overload of {@link #subPropertyValue(String, Optional)} with empty default value
*/
default String subPropertyValue(String name) {
return subPropertyValue(name, Optional.empty());
}
/**
* Overload of {@link #subPropertyValue(String, Optional)} with the given default value
* boxed as an {@link Optional}
*/
default String subPropertyValue(String name, String defaultValue) {
return subPropertyValue(name, Optional.of(defaultValue));
}
/**
* Returns a list of property values for the sub-properties matching the given name
*
* @param propertyName the name of the sub-properties
* @return a list of sub-property values
*/
default List<String> subPropertyValues(final String propertyName) {
return subPropertiesByName(propertyName).stream()
.map(Property::value)
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,5 @@
package com.github.dtschust.zork.parser;
public interface PropertyParseStrategy<T> {
T parse(final Property source);
}

View File

@ -0,0 +1,19 @@
package com.github.dtschust.zork.parser;
import com.github.dtschust.zork.ZorkTrigger;
import java.util.function.Function;
public interface TriggerPropertyParseStrategy {
ZorkTrigger parseTrigger(final Property source, final Property parent);
/**
* Partial function application for the parseTrigger method able to define the parent element first
*
* @param parent the parent element
* @return a lambda mapping a source, a child element of `parent`, to ZorkTrigger objects
*/
default Function<Property, ZorkTrigger> parse(final Property parent) {
return source -> this.parseTrigger(source, parent);
}
}

View File

@ -0,0 +1,61 @@
package com.github.dtschust.zork.parser;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.objects.ZorkContainer;
import com.github.dtschust.zork.objects.ZorkCreature;
import com.github.dtschust.zork.objects.ZorkItem;
import com.github.dtschust.zork.objects.ZorkRoom;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
public abstract class ZorkParser {
private final PropertyParseStrategy<ZorkCreature> creatureStrategy;
private final PropertyParseStrategy<ZorkContainer> containerStrategy;
private final PropertyParseStrategy<ZorkItem> itemStrategy;
private final PropertyParseStrategy<ZorkRoom> roomStrategy;
protected ZorkParser(final PropertyParseStrategy<ZorkCreature> creatureStrategy,
final PropertyParseStrategy<ZorkContainer> containerStrategy,
final PropertyParseStrategy<ZorkItem> itemStrategy,
final PropertyParseStrategy<ZorkRoom> roomStrategy) {
this.creatureStrategy = creatureStrategy;
this.containerStrategy = containerStrategy;
this.itemStrategy = itemStrategy;
this.roomStrategy = roomStrategy;
}
protected abstract Property getRootProperty(final String filename);
public ZorkGame parse(final String filename, final PrintStream stream) {
final List<ZorkCreature> creatureList = new ArrayList<>();
final List<ZorkContainer> containerList = new ArrayList<>();
final List<ZorkRoom> roomList = new ArrayList<>();
final List<ZorkItem> itemList = new ArrayList<>();
final Property rootElement = getRootProperty(filename);
// Every single first generation child is a room, container, creature, or item. So load them in
for (final Property element : rootElement.subProperties()) {
switch (element.name()) {
case "creature":
creatureList.add(creatureStrategy.parse(element));
break;
case "container":
containerList.add(containerStrategy.parse(element));
break;
case "room":
roomList.add(roomStrategy.parse(element));
break;
case "item":
itemList.add(itemStrategy.parse(element));
break;
default:
throw new IllegalArgumentException(element.name() + " not recognized");
}
}
return new ZorkGame(roomList, itemList, containerList, creatureList, stream);
}
}

View File

@ -0,0 +1,46 @@
package com.github.dtschust.zork.parser;
import com.github.dtschust.zork.objects.ZorkContainer;
import com.github.dtschust.zork.objects.ZorkCreature;
import com.github.dtschust.zork.objects.ZorkItem;
import com.github.dtschust.zork.objects.ZorkRoom;
import com.github.dtschust.zork.parser.dom.DOMElement;
import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.nio.channels.NonReadableChannelException;
public class ZorkXMLParser extends ZorkParser {
public ZorkXMLParser(final PropertyParseStrategy<ZorkCreature> creatureStrategy,
final PropertyParseStrategy<ZorkContainer> containerStrategy,
final PropertyParseStrategy<ZorkItem> itemStrategy,
final PropertyParseStrategy<ZorkRoom> roomStrategy) {
super(creatureStrategy, containerStrategy, itemStrategy, roomStrategy);
}
@Override
protected Property getRootProperty(String filename) {
File file = new File(filename);
if (!file.canRead()) {
System.out.println("Error opening file. Exiting...");
throw new NonReadableChannelException();
}
try {
// Open the xml file
final DocumentBuilderFactory builder = DocumentBuilderFactory.newInstance();
// Limit XML features to mitigate vulnerabilities
builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
final Element rootElement = builder.newDocumentBuilder().parse(file).getDocumentElement();
return DOMElement.of(rootElement);
} catch (final Exception ignored) {
System.out.println("Invalid XML file, exiting");
System.exit(-1);
return null; // never reached
}
}
}

View File

@ -0,0 +1,83 @@
package com.github.dtschust.zork.parser.dom;
import com.github.dtschust.zork.parser.Property;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class DOMElement implements Property {
private final Element backing;
public DOMElement(final Element backing) {
this.backing = backing;
}
public static DOMElement of(final Element backing) {
return new DOMElement(backing);
}
/**
* Given a DOM element with one and only one child of text node type, returns the text as a string. If there is no
* such node, '?' is returned
* Get a string from an element (XML parsing stuff)
*
* @return the text as string, or '?'
*/
@Override
public String value() {
final Node child = backing.getFirstChild();
return child instanceof CharacterData ? ((CharacterData) child).getData() : "?";
}
@Override
public List<? extends Property> subPropertiesByName(final String name) {
return DOMElementList.byTagName(backing, name);
}
@Override
public String subPropertyValue(final String elementName,
final Optional<String> defaultValue) {
final NodeList field = backing.getElementsByTagName(elementName);
if (field.getLength() == 0) {
return defaultValue.orElseThrow(() ->
new IllegalArgumentException(elementName + " element count in container is not 1"));
}
final Node first = field.item(0);
if (!(first instanceof Element)) {
// the contract of getElementsByTagName states that it returns a list of Element objects
throw new IllegalStateException("unreachable");
}
return DOMElement.of((Element) first).value();
}
@Override
public String name() {
return backing.getTagName();
}
@Override
public boolean hasSubProperty(final String name) {
return backing.getElementsByTagName(name).getLength() > 0;
}
@Override
public List<Property> subProperties() {
final List<Property> elements = new ArrayList<>();
final NodeList children = backing.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
final Node item = children.item(i);
if (item instanceof Element) {
elements.add(DOMElement.of((Element) item));
}
}
return elements;
}
}

View File

@ -0,0 +1,50 @@
package com.github.dtschust.zork.parser.dom;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.AbstractList;
import java.util.Objects;
import java.util.RandomAccess;
public class DOMElementList extends AbstractList<DOMElement> implements RandomAccess {
private final NodeList list;
private DOMElementList(final NodeList l) {
list = l;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
DOMElementList that = (DOMElementList) o;
return list.equals(that.list);
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), list);
}
static DOMElementList byTagName(final Element parent, final String name) {
return new DOMElementList(parent.getElementsByTagName(name));
}
@Override
public DOMElement get(int index) {
final Node e = list.item(index);
if (!(e instanceof Element)) {
// the contract of getElementsByTagName states that it returns a list of Element objects
throw new IllegalStateException("unreachable");
}
return DOMElement.of((Element) e);
}
@Override
public int size() {
return list.getLength();
}
}

View File

@ -0,0 +1,25 @@
package com.github.dtschust.zork.parser.strategies;
import com.github.dtschust.zork.ZorkCondition;
import com.github.dtschust.zork.ZorkConditionHas;
import com.github.dtschust.zork.ZorkConditionStatus;
import com.github.dtschust.zork.parser.Property;
import com.github.dtschust.zork.parser.PropertyParseStrategy;
public class ZorkConditionParseStrategy implements PropertyParseStrategy<ZorkCondition> {
@Override
public ZorkCondition parse(final Property source) {
if (source.hasSubProperty("has")) {
return new ZorkConditionHas(
source.subPropertyValue("has"),
source.subPropertyValue("object"),
source.subPropertyValue("owner")
);
} else {
return new ZorkConditionStatus(
source.subPropertyValue("status"),
source.subPropertyValue("object")
);
}
}
}

View File

@ -0,0 +1,38 @@
package com.github.dtschust.zork.parser.strategies;
import com.github.dtschust.zork.ZorkTrigger;
import com.github.dtschust.zork.objects.ZorkContainer;
import com.github.dtschust.zork.parser.Property;
import com.github.dtschust.zork.parser.PropertyParseStrategy;
import com.github.dtschust.zork.parser.TriggerPropertyParseStrategy;
import java.util.List;
import java.util.stream.Collectors;
public class ZorkContainerParseStrategy implements PropertyParseStrategy<ZorkContainer> {
private final TriggerPropertyParseStrategy triggerStrategy;
public ZorkContainerParseStrategy(TriggerPropertyParseStrategy triggerStrategy) {
this.triggerStrategy = triggerStrategy;
}
@Override
public ZorkContainer parse(final Property element) {
final String name = element.subPropertyValue("name", "");
final String description = element.subPropertyValue("description", "");
final List<ZorkTrigger> triggers = element.subPropertiesByName("trigger").stream()
.map(triggerStrategy.parse(element))
.collect(Collectors.toList());
final String status = element.subPropertyValue("status", "");
final List<String> accepts = element.subPropertyValues("accept");
final List<String> items = element.subPropertyValues("item");
return new ZorkContainer(name, description, status, items, accepts, triggers);
}
}

View File

@ -0,0 +1,56 @@
package com.github.dtschust.zork.parser.strategies;
import com.github.dtschust.zork.ZorkCondition;
import com.github.dtschust.zork.ZorkTrigger;
import com.github.dtschust.zork.objects.ZorkCreature;
import com.github.dtschust.zork.parser.Property;
import com.github.dtschust.zork.parser.PropertyParseStrategy;
import com.github.dtschust.zork.parser.TriggerPropertyParseStrategy;
import java.util.List;
import java.util.stream.Collectors;
public class ZorkCreatureParseStrategy implements PropertyParseStrategy<ZorkCreature> {
private final PropertyParseStrategy<ZorkCondition> conditionStrategy;
private final TriggerPropertyParseStrategy triggerStrategy;
public ZorkCreatureParseStrategy(final PropertyParseStrategy<ZorkCondition> conditionStrategy,
final TriggerPropertyParseStrategy triggerStrategy) {
this.conditionStrategy = conditionStrategy;
this.triggerStrategy = triggerStrategy;
}
@Override
public ZorkCreature parse(final Property source) {
final List<? extends Property> attacks = source.subPropertiesByName("attack");
// Get all possible creature attributes
final List<ZorkCondition> conditions = attacks.stream()
.flatMap(e -> e.subPropertiesByName("condition").stream())
.map(conditionStrategy::parse)
.collect(Collectors.toList());
final List<String> prints = attacks.stream()
.flatMap(e -> e.subPropertyValues("print").stream())
.collect(Collectors.toList());
final List<String> actions = attacks.stream()
.flatMap(e -> e.subPropertyValues("action").stream())
.collect(Collectors.toList());
final List<String> vulnerabilities = source.subPropertyValues("vulnerability");
final List<ZorkTrigger> triggers = source.subPropertiesByName("trigger").stream()
.map(triggerStrategy.parse(source))
.collect(Collectors.toList());
final String name = source.subPropertyValue("name", "");
final String description = source.subPropertyValue("description", "");
final String status = source.subPropertyValue("status", "");
return new ZorkCreature(name, description, status, triggers, vulnerabilities, conditions, prints, actions);
}
}

View File

@ -0,0 +1,46 @@
package com.github.dtschust.zork.parser.strategies;
import com.github.dtschust.zork.ZorkTrigger;
import com.github.dtschust.zork.objects.ZorkItem;
import com.github.dtschust.zork.parser.Property;
import com.github.dtschust.zork.parser.PropertyParseStrategy;
import com.github.dtschust.zork.parser.TriggerPropertyParseStrategy;
import java.util.List;
import java.util.stream.Collectors;
public class ZorkItemParseStrategy implements PropertyParseStrategy<ZorkItem> {
private final TriggerPropertyParseStrategy triggerStrategy;
public ZorkItemParseStrategy(final TriggerPropertyParseStrategy triggerStrategy) {
this.triggerStrategy = triggerStrategy;
}
@Override
public ZorkItem parse(final Property source) {
final List<String> prints = source.subPropertiesByName("turnon").stream()
.flatMap(e -> e.subPropertyValues("print").stream())
.collect(Collectors.toList());
final List<String> actions = source.subPropertiesByName("turnon").stream()
.flatMap(e -> e.subPropertyValues("action").stream())
.collect(Collectors.toList());
final List<ZorkTrigger> triggers = source.subPropertiesByName("trigger").stream()
.map(triggerStrategy.parse(source))
.collect(Collectors.toList());
/* Get all possible item attributes*/
return new ZorkItem(
source.subPropertyValue("name", ""),
source.subPropertyValue("description", ""),
source.subPropertyValue("status", ""),
source.subPropertyValue("writing", ""),
triggers,
prints,
actions
);
}
}

View File

@ -0,0 +1,50 @@
package com.github.dtschust.zork.parser.strategies;
import com.github.dtschust.zork.ZorkTrigger;
import com.github.dtschust.zork.objects.ZorkRoom;
import com.github.dtschust.zork.parser.Property;
import com.github.dtschust.zork.parser.PropertyParseStrategy;
import com.github.dtschust.zork.parser.TriggerPropertyParseStrategy;
import com.github.dtschust.zork.types.ZorkDirection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class ZorkRoomParseStrategy implements PropertyParseStrategy<ZorkRoom> {
private final TriggerPropertyParseStrategy triggerStrategy;
public ZorkRoomParseStrategy(final TriggerPropertyParseStrategy triggerStrategy) {
this.triggerStrategy = triggerStrategy;
}
@Override
public ZorkRoom parse(final Property source) {
// Get all possible Room attributes
final String name = source.subPropertyValue("name", "");
final String description = source.subPropertyValue("description", "");
final String type = source.subPropertyValue("type", "regular");
final List<ZorkTrigger> triggers = source.subPropertiesByName("trigger").stream()
.map(triggerStrategy.parse(source))
.collect(Collectors.toList());
final String status = source.subPropertyValue("status", "");
final List<String> items = source.subPropertyValues("item");
final List<String> creatures = source.subPropertyValues("creature");
final List<String> containers = source.subPropertyValues("container");
final Map<ZorkDirection, String> borders = source.subPropertiesByName("border").stream()
.collect(Collectors.toMap(
e -> ZorkDirection.fromLong(e.subPropertyValue("direction")),
e -> e.subPropertyValue("name")
));
return new ZorkRoom(name, description, type, status, triggers, borders, containers, items, creatures);
}
}

View File

@ -0,0 +1,42 @@
package com.github.dtschust.zork.parser.strategies;
import com.github.dtschust.zork.ZorkCommand;
import com.github.dtschust.zork.ZorkCondition;
import com.github.dtschust.zork.ZorkTrigger;
import com.github.dtschust.zork.ZorkTriggerType;
import com.github.dtschust.zork.parser.Property;
import com.github.dtschust.zork.parser.PropertyParseStrategy;
import com.github.dtschust.zork.parser.TriggerPropertyParseStrategy;
import java.util.List;
import java.util.stream.Collectors;
public class ZorkTriggerParseStrategy implements TriggerPropertyParseStrategy {
private final PropertyParseStrategy<ZorkCondition> conditionStrategy;
public ZorkTriggerParseStrategy(final PropertyParseStrategy<ZorkCondition> conditionStrategy) {
this.conditionStrategy = conditionStrategy;
}
@Override
public ZorkTrigger parseTrigger(final Property source, final Property parent) {
final String typeString = parent.subPropertyValue("type", "single");
final ZorkTriggerType type = ZorkTriggerType.fromName(typeString).orElseThrow(() ->
new IllegalArgumentException(typeString + " is not a valid trigger type"));
final List<ZorkCommand> commands = source.subPropertiesByName("command").stream()
.map(Property::value)
.map(ZorkCommand::new)
.collect(Collectors.toList());
final List<ZorkCondition> conditions = source.subPropertiesByName("condition").stream()
.map(conditionStrategy::parse)
.collect(Collectors.toList());
final List<String> prints = source.subPropertyValues("print");
final List<String> actions = source.subPropertyValues("action");
return new ZorkTrigger(type, conditions, commands, prints, actions);
}
}

View File

@ -0,0 +1,19 @@
package com.github.dtschust.zork.repl;
import com.github.dtschust.zork.ZorkGame;
import java.util.List;
public interface Action {
boolean matchesInput(final List<String> arguments);
default int getMinimumArgCount() {
return 1;
}
default int getMaximumArgCount() {
return Integer.MAX_VALUE;
}
boolean run(final ZorkGame game, final List<String> arguments);
}

View File

@ -0,0 +1,53 @@
package com.github.dtschust.zork.repl;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.repl.actions.*;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
public class ActionDispatcher {
private static final List<Action> actionList = List.of(
new AddAction(),
new AttackAction(),
new DeleteAction(),
new DropItemAction(),
new GameOverAction(),
new InventoryAction(),
new MoveAction(),
new OpenAction(),
new PutAction(),
new ReadAction(),
new StartGameAction(),
new TakeAction(),
new TurnOnAction(),
new UpdateAction()
);
private final ZorkGame game;
public ActionDispatcher(ZorkGame game) {
this.game = game;
}
private Optional<Action> findAction(final List<String> arguments) {
if (arguments.isEmpty()) return Optional.empty();
final int size = arguments.size();
return actionList.stream().filter(
u -> size >= u.getMinimumArgCount() &&
size <= u.getMaximumArgCount() &&
u.matchesInput(arguments)
).findAny();
}
public void dispatch(String input) {
final List<String> arguments = Arrays.asList(input.split(" "));
final Optional<Action> action = findAction(arguments);
if (action.isEmpty() || !action.get().run(game, arguments)) {
game.stream.println("Error");
}
}
}

View File

@ -0,0 +1,37 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.repl.Action;
import java.util.List;
/**
* Add: figure out what type the destination is, then what type the object is. Then add object to destination if it makes sense
*/
public class AddAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return arguments.get(0).equals("Add");
}
@Override
public int getMinimumArgCount() {
return 4;
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
final String objectName = arguments.get(1);
final String destination = arguments.get(3);
try {
return game.getObject(objectName).map(o -> {
game.addObjectToCollection(o, destination);
return true;
}).orElse(false);
} catch (final UnsupportedOperationException ignored) {
return false;
}
}
}

View File

@ -0,0 +1,38 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.objects.ZorkCreature;
import com.github.dtschust.zork.repl.Action;
import java.util.List;
import java.util.Optional;
/**
* Attempt an attack, do you feel lucky?
*/
public class AttackAction implements Action {
@Override
public boolean matchesInput(final List<String> arguments) {
return arguments.get(0).equals("attack");
}
@Override
public int getMinimumArgCount() {
return 4;
}
@Override
public boolean run(final ZorkGame game, final List<String> arguments) {
final String what = arguments.get(1);
final String weapon = arguments.get(3);
final Optional<ZorkCreature> tempCreature = game.getCreature(what);
if (tempCreature.isPresent() && game.inventory.contains(weapon) &&
tempCreature.get().isAttackSuccessful(game, weapon)) {
game.stream.println("You assault the " + what + " with the " + weapon + ".");
tempCreature.get().printAndExecuteActions(game);
return true;
}
return false;
}
}

View File

@ -0,0 +1,35 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.objects.ZorkRoom;
import com.github.dtschust.zork.repl.Action;
import java.util.List;
/**
* Delete: figure out what object it is and delete it accordingly. Rooms are especially tricky
*/
public class DeleteAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return arguments.get(0).equals("Delete");
}
@Override
public int getMinimumArgCount() {
return 2;
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
return game.getObject(arguments.get(1)).map(o -> {
if (o instanceof ZorkRoom) {
game.removeFromBorders((ZorkRoom) o);
} else {
game.removeObjectFromCollections(o);
}
return true;
}).orElse(false);
}
}

View File

@ -0,0 +1,39 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.objects.ZorkItem;
import com.github.dtschust.zork.objects.ZorkRoom;
import com.github.dtschust.zork.repl.Action;
import java.util.List;
import java.util.Optional;
public class DropItemAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return arguments.get(0).equals("drop");
}
@Override
public int getMinimumArgCount() {
return 2;
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
final String whatName = arguments.get(1);
final Optional<ZorkItem> what = game.getItem(whatName);
if (game.inventory.contains(whatName) && what.isPresent()) {
final ZorkRoom tempRoom = game.getCurrentRoom();
game.inventory.remove(whatName);
tempRoom.addObject(what.get());
game.stream.println(whatName + " dropped.");
return true;
} else {
return false;
}
}
}

View File

@ -0,0 +1,28 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.repl.Action;
import java.util.List;
/**
* The "Game Over" action marks the end of the game.
*/
public class GameOverAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return arguments.get(0).equals("Game") && arguments.get(1).equals("Over");
}
@Override
public int getMinimumArgCount() {
return 2;
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
game.stream.println("Victory!");
game.setGameOver();
return true;
}
}

View File

@ -0,0 +1,25 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.repl.Action;
import java.util.List;
public class InventoryAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return arguments.get(0).equals("i");
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
if (game.inventory.isEmpty()) {
game.stream.println("Inventory: empty");
} else {
final String output = "Inventory: " + String.join(", ", game.inventory);
game.stream.println(output);
}
return true;
}
}

View File

@ -0,0 +1,42 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.repl.Action;
import com.github.dtschust.zork.types.ZorkDirection;
import java.util.List;
import java.util.Optional;
/**
* If it's not a "Special Action", just treat it normally
* Execute a user action or an action command from some <action> element that is not one of the "Special Commands"
* Movement
*/
public class MoveAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return ZorkDirection.fromShort(arguments.get(0)).isPresent();
}
@Override
public int getMaximumArgCount() {
return 1;
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
// we are guaranteed to have a valid short direction name by matchesInput
final ZorkDirection direction = ZorkDirection.fromShort(arguments.get(0)).orElseThrow(() ->
new IllegalStateException("unreachable"));
final Optional<String> roomName = game.getCurrentRoom().getBorderingRoom(direction);
if (roomName.isPresent() && game.changeRoom(roomName.get())) {
game.stream.println(game.getCurrentRoom().getDescription());
} else {
game.stream.println("Can't go that way.");
}
return true;
}
}

View File

@ -0,0 +1,34 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.repl.Action;
import java.util.List;
public class OpenAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return arguments.get(0).equals("open");
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
final String what = arguments.get(1);
if (what.equals("exit")) {
if (game.getCurrentRoom().isExit()) {
game.stream.println("Game Over");
game.setGameOver();
return true;
} else {
return false;
}
} else {
return game.getContainer(what).map(cont -> {
cont.open();
game.stream.println(cont.getContents());
return true;
}).orElse(false);
}
}
}

View File

@ -0,0 +1,31 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.repl.Action;
import java.util.List;
public class PutAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return arguments.get(0).equals("put");
}
@Override
public int getMinimumArgCount() {
return 4;
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
final String what = arguments.get(1);
return game.getContainer(arguments.get(3))
.filter(c -> c.isOpen() && game.inventory.contains(what))
.map(tempContainer -> {
tempContainer.addItem(what);
game.inventory.remove(what);
game.stream.println("Item " + what + " added to " + tempContainer.getName() + ".");
return true;
}).orElse(false);
}
}

View File

@ -0,0 +1,26 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.repl.Action;
import java.util.List;
public class ReadAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return arguments.get(0).equals("read");
}
@Override
public int getMinimumArgCount() {
return 2;
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
return game.getItem(arguments.get(1)).map(i -> {
game.stream.println(i.getWriting());
return true;
}).orElse(false);
}
}

View File

@ -0,0 +1,33 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.repl.Action;
import java.util.List;
/**
* Add: figure out what type the destination is, then what type the object is. Then add object to destination if it makes sense
*/
public class StartGameAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return arguments.get(0).equals("Start") && arguments.get(1).equals("at");
}
@Override
public int getMinimumArgCount() {
return 3;
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
final String room = arguments.get(2);
if (!game.isRunning() && game.changeRoom(room)) {
game.stream.println(game.getCurrentRoom().getDescription());
return true;
}
return false;
}
}

View File

@ -0,0 +1,45 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.objects.ZorkContainer;
import com.github.dtschust.zork.repl.Action;
import java.util.List;
public class TakeAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return arguments.get(0).equals("take");
}
@Override
public int getMinimumArgCount() {
return 2;
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
return game.getItem(arguments.get(1)).map(i -> {
if (game.getCurrentRoom().containsItem(i.getName())) {
game.inventory.add(i.getName());
game.getCurrentRoom().removeObject(i);
game.stream.println("Item " + i.getName() + " added to inventory.");
return true;
} else {
// Search all containers in the current room for the item!
for (final String key : game.getCurrentRoom().getContainer()) {
final ZorkContainer tempContainer = game.getContainer(key).orElseThrow(() ->
new IllegalStateException("container " + key + " in room " +
game.getCurrentRoom().getName() + " not found"));
if (tempContainer != null && tempContainer.isOpen() && tempContainer.containsItem(i.getName())) {
game.inventory.add(i.getName());
tempContainer.removeObject(i);
game.stream.println("Item " + i.getName() + " added to inventory.");
return true;
}
}
}
return false;
}).orElse(false);
}
}

View File

@ -0,0 +1,36 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.repl.Action;
import java.util.List;
import java.util.Optional;
/**
* Turn on an item
*/
public class TurnOnAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return arguments.get(0).equals("turn") && arguments.get(1).equals("on");
}
@Override
public int getMinimumArgCount() {
return 3;
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
final String what = arguments.get(2);
final boolean inInventory = game.inventory.contains(what);
return Optional.ofNullable(inInventory ? what : null)
.flatMap(game::getItem)
.map(i -> {
game.stream.println("You activate the " + i.getName() + ".");
i.printAndExecuteActions(game);
return true;
}).orElse(false);
}
}

View File

@ -0,0 +1,30 @@
package com.github.dtschust.zork.repl.actions;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.repl.Action;
import java.util.List;
/**
* The "Update" command figures out what type of item it is, and then change its status
*/
public class UpdateAction implements Action {
@Override
public boolean matchesInput(List<String> arguments) {
return arguments.get(0).equals("Update");
}
@Override
public int getMinimumArgCount() {
return 1;
}
@Override
public boolean run(ZorkGame game, List<String> arguments) {
final String objectName = arguments.get(1);
final String newStatus = arguments.get(3);
game.updateObjectStatus(objectName, newStatus);
return true;
}
}

View File

@ -0,0 +1,22 @@
package com.github.dtschust.zork.types;
import com.github.dtschust.zork.ZorkGame;
import com.github.dtschust.zork.repl.ActionDispatcher;
import java.util.List;
public interface HasPrintsAndActions {
List<String> getPrints();
List<String> getActions();
default void printAndExecuteActions(final ZorkGame game) {
for (final String print : getPrints()) {
game.stream.println(print);
}
final ActionDispatcher effectsDispatcher = new ActionDispatcher(game);
for (final String action : getActions()) {
effectsDispatcher.dispatch(action);
}
}
}

View File

@ -0,0 +1,9 @@
package com.github.dtschust.zork.types;
import com.github.dtschust.zork.objects.ZorkObject;
public interface ObjectCollector {
void addObject(final ZorkObject object);
void removeObject(final ZorkObject object);
}

View File

@ -0,0 +1,32 @@
package com.github.dtschust.zork.types;
import java.util.EnumSet;
import java.util.Optional;
public enum ZorkDirection {
NORTH("north", "n"),
EAST("east", "e"),
SOUTH("south", "s"),
WEST("west", "w");
private final String longName;
private final String shortName;
ZorkDirection(final String longName, final String shortName) {
this.longName = longName;
this.shortName = shortName;
}
public static Optional<ZorkDirection> fromShort(final String shortName) {
return EnumSet.allOf(ZorkDirection.class).stream()
.filter(e -> e.shortName.equals(shortName))
.findFirst();
}
public static ZorkDirection fromLong(final String longName) {
return EnumSet.allOf(ZorkDirection.class).stream()
.filter(e -> e.longName.equals(longName))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(longName + " is not a valid direction long name"));
}
}

View File

@ -0,0 +1,34 @@
package com.github.dtschust.zork.types;
import com.github.dtschust.zork.objects.ZorkObject;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
public class ZorkMapByName<T extends ZorkObject> {
private final Map<String, T> backing;
public ZorkMapByName(final Collection<T> source) {
backing = source.stream().collect(Collectors.toMap(ZorkObject::getName, Function.identity()));
}
public void put(final T object) {
backing.put(object.getName(), object);
}
public Optional<T> get(final String name) {
return Optional.ofNullable(backing.get(name));
}
public boolean containsName(final String name) {
return backing.containsKey(name);
}
public Collection<T> values() {
return backing.values();
}
}

View File

@ -0,0 +1,42 @@
package com.github.dtschust.zork;
import com.github.dtschust.zork.utils.CommandReader;
import com.github.dtschust.zork.utils.IOWrapper;
import org.junit.jupiter.api.Test;
import static com.github.stefanbirkner.systemlambda.SystemLambda.catchSystemExit;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ZorkTest {
/** Test the game interacting as a real player
* WARNING: when looking at inventory (i) we are relying on the HashMap order, so the test may be unsafe
*/
@Test
void testSampleGame() {
String gameConfig = "sampleGame.xml";
String gameExecution = "RunThroughResults.txt";
CommandReader run = new CommandReader(gameExecution);
IOWrapper io = new IOWrapper(true);
new Thread(() -> {
try {
catchSystemExit(() -> Zork.runZork(gameConfig));
} catch (Exception ignored) {}
}).start();
while(true){
switch(run.getInstructionType()) {
case SEND:
io.write(run.getInstruction());
break;
case RECV:
assertEquals(run.getInstruction(), io.read());
break;
default:
io.restore();
return;
}
}
}
}

View File

@ -0,0 +1,51 @@
package com.github.dtschust.zork.utils;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
/**
* CommandReader reads the .txt file containing expected input and output
*/
public class CommandReader{
private String instruction;
private static Scanner scanner;
public enum Type{ SEND, RECV, END }
/** CommandReader Constructor
* @param filename file containing command sent (with leading ">") and expected responses
*/
public CommandReader(String filename) {
try {
scanner = new Scanner(new File(filename));
} catch (FileNotFoundException e){
e.printStackTrace();
}
}
/** Load the next instruction (overwrite current instruction) and detect its type
* @return Type of the next instruction:
* - END if the game ended
* - SEND if it's th command to send
* - RECV if it's a game output
*/
public Type getInstructionType(){
if(!scanner.hasNextLine())
return Type.END;
instruction = scanner.nextLine();
if(instruction.startsWith(">")) {
instruction = instruction.substring(1);
return Type.SEND;
}
return Type.RECV;
}
/** Returns a text line (can be both an input or output depending on the Type)
* @return The next text line
*/
public String getInstruction() {
return instruction;
}
}

View File

@ -0,0 +1,67 @@
package com.github.dtschust.zork.utils;
import java.io.*;
/**
* IOWrapper allows to automatize in/out communication
*/
public class IOWrapper {
public final PrintStream console;
public final InputStream input;
private PrintStream printer;
private BufferedReader reader;
private final boolean verbose;
/** IOWrapper Constructor
* @param verbose should log on the console all command sent to / received by the game
*/
public IOWrapper(boolean verbose) {
this.verbose = verbose;
console = System.out;
input = System.in;
try{
final PipedOutputStream testInput = new PipedOutputStream();
final PipedOutputStream out = new PipedOutputStream();
final PipedInputStream testOutput = new PipedInputStream(out);
System.setIn(new PipedInputStream(testInput));
System.setOut(new PrintStream(out));
printer = new PrintStream(testInput);
reader = new BufferedReader(new InputStreamReader(testOutput));
} catch (IOException e) {
e.printStackTrace(console);
}
}
/** Sends a command to the game (and print it if verbose)
* @param line text to send to the game
*/
public void write(String line) {
printer.println(line);
if(verbose)
console.println("> " + line);
}
/** Receive a command from the game (and print it if verbose)
* @return line of text sent by the game
*/
public String read() {
String line = null;
try{
line = reader.readLine();
if(verbose)
console.println("< " + line);
} catch (IOException e) {
e.printStackTrace(console);
}
return line;
}
/** Restore the default System IO
*
*/
public void restore(){
System.setIn(input);
System.setOut(console);
}
}