Compare commits
34 commits
main
...
refactored
Author | SHA1 | Date | |
---|---|---|---|
75052a42c3 | |||
6c1b074846 | |||
6d1d80d022 | |||
e8a108d89f | |||
6df81726d1 | |||
4c70ba2800 | |||
|
5786e58842 | ||
|
d2ce23fead | ||
|
6341503a70 | ||
aafbf94434 | |||
69072097bb | |||
d0d2db70a4 | |||
9564a205dc | |||
718e736032 | |||
37f6bc2ff9 | |||
|
214bed9a13 | ||
|
42732e0fdd | ||
52d98a4ece | |||
a782ec5ad7 | |||
40675f5a39 | |||
|
05a63e0399 | ||
|
e6b96d69d1 | ||
|
4a7eca5302 | ||
|
2e12ab7f1f | ||
|
5d098bf32d | ||
|
0a5c67ea69 | ||
|
f4dc0fac73 | ||
|
4b01c0a85c | ||
6b3d2d142c | |||
8e54d0684b | |||
|
7cc2352b90 | ||
|
008818c357 | ||
6e374918b5 | |||
2406dac418 |
60 changed files with 2585 additions and 1490 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/*.iml
|
||||
/.idea/
|
||||
/target/
|
BIN
Assignment03-refactoring.pdf
Normal file
BIN
Assignment03-refactoring.pdf
Normal file
Binary file not shown.
5
Makefile
5
Makefile
|
@ -1,5 +0,0 @@
|
|||
all:
|
||||
javac *.java
|
||||
|
||||
clean:
|
||||
rm -rf *.class
|
23
README
23
README
|
@ -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
|
18
README.md
18
README.md
|
@ -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.
|
|
@ -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
|
||||
Can’t go that way.
|
||||
Can't go that way.
|
||||
>N
|
||||
Error
|
||||
>n
|
||||
|
@ -34,7 +31,7 @@ Error
|
|||
>attack gnome with face!
|
||||
Error
|
||||
>w
|
||||
Can’t 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
|
||||
|
|
127
pom.xml
Normal file
127
pom.xml
Normal 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
BIN
report.pdf
Normal file
Binary file not shown.
333
report.tex
Normal file
333
report.tex
Normal 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}
|
||||
|
50
src/main/java/com/github/dtschust/zork/Zork.java
Normal file
50
src/main/java/com/github/dtschust/zork/Zork.java
Normal 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]);
|
||||
}
|
||||
}
|
14
src/main/java/com/github/dtschust/zork/ZorkCommand.java
Normal file
14
src/main/java/com/github/dtschust/zork/ZorkCommand.java
Normal 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);
|
||||
}
|
||||
}
|
12
src/main/java/com/github/dtschust/zork/ZorkCondition.java
Normal file
12
src/main/java/com/github/dtschust/zork/ZorkCondition.java
Normal 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);
|
||||
}
|
30
src/main/java/com/github/dtschust/zork/ZorkConditionHas.java
Normal file
30
src/main/java/com/github/dtschust/zork/ZorkConditionHas.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
126
src/main/java/com/github/dtschust/zork/ZorkGame.java
Normal file
126
src/main/java/com/github/dtschust/zork/ZorkGame.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
66
src/main/java/com/github/dtschust/zork/ZorkTrigger.java
Normal file
66
src/main/java/com/github/dtschust/zork/ZorkTrigger.java
Normal 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;
|
||||
}
|
||||
}
|
19
src/main/java/com/github/dtschust/zork/ZorkTriggerType.java
Normal file
19
src/main/java/com/github/dtschust/zork/ZorkTriggerType.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
43
src/main/java/com/github/dtschust/zork/objects/ZorkItem.java
Normal file
43
src/main/java/com/github/dtschust/zork/objects/ZorkItem.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
91
src/main/java/com/github/dtschust/zork/objects/ZorkRoom.java
Normal file
91
src/main/java/com/github/dtschust/zork/objects/ZorkRoom.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
28
src/main/java/com/github/dtschust/zork/parser/ParserIOC.java
Normal file
28
src/main/java/com/github/dtschust/zork/parser/ParserIOC.java
Normal 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;
|
||||
}
|
||||
}
|
91
src/main/java/com/github/dtschust/zork/parser/Property.java
Normal file
91
src/main/java/com/github/dtschust/zork/parser/Property.java
Normal 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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.github.dtschust.zork.parser;
|
||||
|
||||
public interface PropertyParseStrategy<T> {
|
||||
T parse(final Property source);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
19
src/main/java/com/github/dtschust/zork/repl/Action.java
Normal file
19
src/main/java/com/github/dtschust/zork/repl/Action.java
Normal 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);
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
42
src/test/java/com/github/dtschust/zork/ZorkTest.java
Normal file
42
src/test/java/com/github/dtschust/zork/ZorkTest.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
67
src/test/java/com/github/dtschust/zork/utils/IOWrapper.java
Normal file
67
src/test/java/com/github/dtschust/zork/utils/IOWrapper.java
Normal 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);
|
||||
}
|
||||
}
|
Reference in a new issue