diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index fd8c44d086..ae06f4ee5a 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -33,18 +33,18 @@ jobs:
- name: Build and check with Gradle
run: ./gradlew check
- - name: Perform IO redirection test (*NIX)
- if: runner.os == 'Linux'
- working-directory: ${{ github.workspace }}/text-ui-test
- run: ./runtest.sh
-
- - name: Perform IO redirection test (MacOS)
- if: always() && runner.os == 'macOS'
- working-directory: ${{ github.workspace }}/text-ui-test
- run: ./runtest.sh
-
- - name: Perform IO redirection test (Windows)
- if: always() && runner.os == 'Windows'
- working-directory: ${{ github.workspace }}/text-ui-test
- shell: cmd
- run: runtest.bat
\ No newline at end of file
+ #- name: Perform IO redirection test (*NIX)
+ # if: runner.os == 'Linux'
+ # working-directory: ${{ github.workspace }}/text-ui-test
+ # run: ./runtest.sh
+
+ #- name: Perform IO redirection test (MacOS)
+ # if: always() && runner.os == 'macOS'
+ # working-directory: ${{ github.workspace }}/text-ui-test
+ # run: ./runtest.sh
+
+ #- name: Perform IO redirection test (Windows)
+ # if: always() && runner.os == 'Windows'
+ # working-directory: ${{ github.workspace }}/text-ui-test
+ # shell: cmd
+ # run: runtest.bat
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f69985ef1f..37e259c9a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,8 @@ bin/
/text-ui-test/ACTUAL.txt
text-ui-test/EXPECTED-UNIX.TXT
+seedu.typists.ui.TextUi.log
+seedu.typists.ui.TextUi.log.1
+seedu.typists.ui.TextUi.log.lck
+time-limited_records.txt
+word-limited_records.txt
\ No newline at end of file
diff --git a/TypingGame.txt b/TypingGame.txt
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/build.gradle b/build.gradle
index b0c5528fb5..65db32f9ef 100644
--- a/build.gradle
+++ b/build.gradle
@@ -10,6 +10,7 @@ repositories {
}
dependencies {
+ implementation 'org.fastily:jwiki:1.8.0'
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.5.0'
testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.5.0'
}
@@ -29,11 +30,11 @@ test {
}
application {
- mainClassName = "seedu.duke.Duke"
+ mainClassName = "seedu.typists.Main"
}
shadowJar {
- archiveBaseName = "duke"
+ archiveBaseName = "typists"
archiveClassifier = null
}
@@ -43,4 +44,5 @@ checkstyle {
run{
standardInput = System.in
+ enableAssertions = true
}
diff --git a/docs/AboutUs.md b/docs/AboutUs.md
index 0f072953ea..ed8d323efe 100644
--- a/docs/AboutUs.md
+++ b/docs/AboutUs.md
@@ -2,8 +2,7 @@
Display | Name | Github Profile | Portfolio
--------|:----:|:--------------:|:---------:
- | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md)
- | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md)
- | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md)
- | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md)
- | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md)
+ | Lim Jia Jing | [Github](https://github.com/LimJiaJing) | [Portfolio](team/limjiajing.md)
+ | Lu Sicheng | [Github](https://github.com/Isabella-L) | [Portfolio](team/lusicheng.md)
+| Shi Zhansen | [Github](https://github.com/DuckWillow) | [Portfolio](team/shizhansen.md)
+| William Wahyudi | [Github](https://github.com/maxwireddevs) | [Portfolio](team/williamwahyudi.md)
diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md
index 64e1f0ed2b..c1a895dc49 100644
--- a/docs/DeveloperGuide.md
+++ b/docs/DeveloperGuide.md
@@ -1,38 +1,226 @@
# Developer Guide
+* [Acknowledgements](#acknowledgements)
+* [Setting Up, getting started](#setting-up-getting-started)
+* [Design](#design)
+ * [Architecture](#architecture)
+ * [Command Component](#command-component)
+ * [Game Component](#game-component)
+ * [Content Component](#choose-the-game-content)
+* [Implementation](#implementation)
+* [Product Scope](#product-scope)
+* [User Stories](#user-stories)
+* [Glossary](#glossary)
+* [Manual Testing](#instructions-for-manual-testing)
+
## Acknowledgements
-{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well}
+* [reference code for testing System.out.println()](https://www.baeldung.com/java-testing-system-out-println)
+* [reference code for accessing wiki](https://github.com/fastily/jwiki)
+* [SE-EDU AB3 Developer Guide Format](https://se-education.org/addressbook-level3/DeveloperGuide.html)
+
+## Design
+
+> :bulb: **Tip:** The `.puml` files used to create diagrams in this document
+> can be found in the [diagrams](https://github.com/se-edu/addressbook-level3/tree/master/docs/diagrams/) folder.
+
+### Architecture
+
+
+
+The Architecture diagram above explains the high-level design of the Typists app.
+Given below is a quick overview of main components and how they interact with each other.
+
+**Main Components of the architecture**
+The `Main` class is the entry point of Typist. It is responsible for parsing and running various user's command.
+
+The other core components of Typist:
+* `ui`: The Ui of the app.
+* `command`: Consists of `CommandFactory` which parses user inputs, and various `command` objects.
+* `common`: A collection of classes used by multiple other components.
+* `content`: Holder for the content and logic for `content` command
+* `game`: Game, game summary and game record executor.
+* `storage`: Game storage executor.
+
+### Command Component
+**API:** `CommandFactory.java`
+
+The `Command` component implements the Factory Design Pattern to parse user commands.
+
+Here’s a (partial) class diagram of the `Command` component:
+
+
+
+How the `Command` component works:
+1. Typists `Main` calls upon the `CommandFactory` class to parse the user input.
+2. The `CommandFactory` returns a `Command` object (more precisely, an object that implements it e.g., `GameCommand`).
+3. `Main` will then execute the `Command` by calling `.run(args)` method of the `Command`.
+
+### Game Component
+
+**How the Game Component works:**
+* The `Game` component consists of 2 parts:
+ 1. The actual game execution classes;
+ 2. and the game record classes
+* For 1: When for a `Game` object, the `.runGame()` is the main method that runs the game until termination and
+the`.gameSummary()` displays the summary and stores game data.
+* For 2: Game Record Management, which interacts with record storage, will be explained with further detail in [later section](#proposed-view-statistics-feature).
+
+For instance:
+When game is running from the CLI:
+* The `.run(args)` of `TimeGameCommand` is called, a `TimeLimitGame` object is created.
+* Then, `.run()` method of `TimeLimitCommand` is executed, a Time Limit Game will start running
+until game ends(i.e. timer's up).
+* `.gameSummary()` method will then generate the summary of the game.
+
+The (partial) Class Diagram bellow illustrates the structure of `game` component:
+{some class diagram}
+
+There are 2 constructors in `TimeModeGame` class, each handling different number of parameters specified.
+
+As from the diagram, `WordLimitGame` and `TimeLimitGame`, the 2 major game execution classes inherits from the `Game` Class.
+However, their implementations and functionality varies on many parts due to them being 2 different games.
+Hence, The section below explains in greater detail how [Word Mode](#word-limit-game) and [Time Mode](#time-limit-game) of Typist game are implemented.
+
+
+### Word Limit Game
+
+Sequence Diagram for Word Mode Game:
+
+
+
+The Sequence Diagram above illustrates the working process of the `WordLimitGame` class.
+
+### Time Limit Game
-## Design & implementation
+The Sequence Diagram below illustrates the working process of the `TimeLimitGame` when the `.runGame` method is called:
-{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.}
+
+### Choose the game content
+
+Once the game starts, the main class instantiates a Content object containing a string, which is set to a default
+value (lorem ipsum paragraph). The string can only be changed through the setContent() method.
+
+Users can input the 'content' command to start the text selection.
+
+There are 3 types:
+
+ 1. Opening paragraphs from famous books
+ 2. Custom Wikipedia article
+ 3. Random words
+
+The following UML diagram illustrates the way content selection works in the program.
+
+
+
+There only exists one private content string for all sessions. Each time a set method is called, the string is changed
+depending on the choices that the user made throughout the process. Whenever the user starts a game, the getContent()
+method is called and the text is set accordingly.
+
+## Implementation
+
+### \[Proposed\] View Statistics feature
+#### \[Proposed Implementation\]
+The top-level logic of view statistics feature resides in ViewCommand. It implements the Command
+interface. The key method of the class is `executeCommand()`.
+`executeCommand()` logic:
+1. Calls the retrieveStatistics() method of StatisticsManager to get the statistics.
+2. Calls the displayStatistics() method of ViewCommandUi to display the statistics retrieved.
+
+StatisticsManager performs the logic for processing the game records to obtain the statistics.
+Its key methods are:
+* `retrieveStatistics()` - Decides which of the three methods below to run
+* `calculateBestStatistics()` - Returns the best statistics over the past n games
+* `calculateWorstStatistics()` - Returns the worst statistics over the past n games
+* `calculateAverageStatistics()` - Returns the average statistics over the past n games
+
+Given below is an example usage scenario and how the program implements the feature.
+
+Step 1: The user launches the application (scenario assumes that there are several game records already stored in the text files).
+
+Step 2: The user executes `view -m best -g time -n 4` to view his/her best statistics for the time-limited game mode over the past 4 games.
+
+Step 3: A ViewCommand object is then instantiated.
+
+
+
+Step 4. ViewCommand calls the retrieveStatistics() method of StatisticsManager.
+
+
+Step 5: StatisticsManager creates a GameRecordsManager object.
+* The constructor of GameRecordsManager calls the readGameRecords() method of the Storage class to retrieve the gamer's past game records from the text files.
+
+
+Step 6. StatisticsManager then calls getGamesRecords() method of GameRecordsManager() and self-invokes calculateBestStatistics().
+* Calculated statistics is returned.
+
+Step 7. ViewCommand calls the displayStatistics() method of the ViewCommandUi class to display the statistics.
+
+
+
+The following sequence diagram shows how the above scenario is executed.
+
+
+
+
+#### Alternative Implementation
+* One implementation considered is to do away with the retrieveStatistics() method and immediately call one of calculateBestStatistics,
+calculateWorstStatistics() or calculateAverageStatistics() based on the gamer's imput using a switch statement. This implementation choice
+was not used because it violates the Single Responsibility Principle and does not do SLAP well.
+* Another implementation considered was for GameRecordsManager to do the main logic. This implementation was not done as it violates the
+Single Responsibility Principle.
+
## Product scope
+Our product is a typing game, intent to provide enjoyment for people who are familiar with the CML.
+
### Target user profile
-{Describe the target user profile}
+The target user profile includes bored people who are looking for a fun way to entertain themselves while working on the command line.
### Value proposition
-{Describe the value proposition: what problem does it solve?}
+It solves the lack of entertainment alternatives on the CLI.
## User Stories
|Version| As a ... | I want to ... | So that I can ...|
|--------|----------|---------------|------------------|
|v1.0|new user|see usage instructions|refer to them when I forget how to use the application|
-|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list|
-
-## Non-Functional Requirements
-
-{Give non-functional requirements}
+|v1.0|user|customize the time limit to finish a game|train myself to type faster|
+|v1.0|user|customize the word limit, in multiples of 100|have an optimal gaming experience by being able to choose my preferred length of text for the game|
+|v1.0|user|choose text from famous books, randomly generated text or customized text for my game|make my gaming experience more fun and fulfilling|
+|v1.0|typist|view my error rate|know on average, what my error rate is|
+|v1.0|typist|see my word per minute after I've finished a game|know my typing speed|
+|v2.0|gamer|view my past records|see how I have improved over time|
+|v2.0|gamer|clear my past records|have a fresh start|
+|v2.0|typist|see the words I typed wrongly after I've finished a game|know which words I have to practice more|
## Glossary
-* *glossary item* - Definition
+* Mainstream OS: Windows, Linux, Unix, OS-X
+* Time Limit Game: A game that ends only when timer stops.
+* Word Limit Game: A game that ends only when all the words has been inputted.
## Instructions for manual testing
-{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing}
+### Launch and Shutdown
+1. Initial Launch
+ 1. Download the jar file and copy into an empty folder
+ 2. Double-click the jar file.
+ Expected: shows the starting page and current version of Typist
+2. Obtaining past game records
+ 1. Exit the game through command `bye`.
+ 2. Re-launch the app by double-clicking hte jar file.
+ Expected: The record of previous games is retained.
+
+### Opening a game
+1. Opening a game with optional operands
+ 1. Test case: `game -time 0`
+ Expected: A warning message about error on limit input is shown.
+ The game will still start as if no time limit is specified.
+ User will be asked to "Enter how long you want the game to run: ".
+ 2. Test case: `game -time 30 -sn`
+ Expected: A game will start running. Timer starts immediately together the first line of sentence to be typed is displayed.
+
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
index bbcc99c1e7..b11ed62382 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,6 +1,11 @@
-# Duke
+# Typist
-{Give product intro here}
+Welcome to Typists!
+Typist is a command line interface(CLI) typing game targeted at users who love to type and are
+familiar with the command line interface.
+There are two game modes in types:
+* time-limited (aim to type as much as possible in a limited time)
+* and word-limited (aim to type a chosen number of words as fast as possible).
Useful links:
* [User Guide](UserGuide.md)
diff --git a/docs/UserGuide.md b/docs/UserGuide.md
index abd9fbe891..37f6f5844c 100644
--- a/docs/UserGuide.md
+++ b/docs/UserGuide.md
@@ -1,42 +1,326 @@
# User Guide
+1. [Introduction](#1-introduction)
+2. [Quick Start](#2-quick-start)
+3. [Features](#3-features)
+ 1. [Set Content](#31-set-content-content)
+ 2. [Open Game](#32-open-game-game)
+ 1. [Word Mode Game](#321-word-limit-game)
+ 2. [Time Mode Game](#322-time-limit-game)
+ 3. [View history](#33-view-past-records-history)
+ 4. [Clear history](#34-clear-past-records-clear)
+ 5. [Display Summary](#35-view-summary-of-game)
+ 6. [Save Data](#36-saving-the-data)
+ 7. [Man Pages](#37-man-pages-man)
+4. [FAQ](#faq)
+5. [Command Summary](#command-summary)
-## Introduction
+## 1. Introduction
-{Give a product intro}
+Welcome to Typist! The ultimate CLI typing game!
+Go to [Quick Start](#quick-start) to get started!
-## Quick Start
+## 2. Quick Start
-{Give steps to get started quickly}
+1. Ensure that you have Java 11 installed in your local environment.
+2. Down the latest version of `Typist` from [here](https://github.com/AY2122S1-CS2113-T13-4/tp/releases).
+3. Navigate to the folder containing the jar file and run `java -jar tp.jar`.
+4. If everything goes well, the command interface should display the following:
+
+```
+ | Typist - Version 2.0
+ | ===========================================================
+ | ______ _ __
+ | /_ __/_ ______ (_)____/ /_
+ | / / / / / / __ \/ / ___/ __/
+ | / / / /_/ / /_/ / (__ ) /_
+ | /_/ \__, / .___/_/____/\__/
+ | /____/_/
+ | Welcome to Typist -- the ultimate cli typing game.
+ | Brought to you by -- AY2122S1-CS2113-T13-4.
+ | Manual:
+ | content: set the content
+ | game -time: start a new time game
+ | game -word: start a new word game
+ | history -g GAME_MODE [-n NUMBER_OF_RECORDS]: view past game records
+ | clear [-g GAME_MODE]: clear all game records
+ | bye: exit typist
+ | ===========================================================
+```
-1. Ensure that you have Java 11 or above installed.
-1. Down the latest version of `Duke` from [here](http://link.to/duke).
+## 3. Features
-## Features
+### Notes about the command format:
-{Give detailed description of each feature}
+* Words in UPPER_CASE are arguments to be supplied by the user.
+e.g. in `history -g GAME_MODE`, `GAME_MODE` is an argument which can be used as `history -g time`.
+* Items in square brackets are optional.
+e.g. `-g GAME_MODE [-n NUMBER_OF_RECORDS]` can be used as `-g time -n 9` or as `-g time`
+* Parameters can be in any order.
+e.g. if command specifies `history -g GAME_MODE [-n NUMBER_OF_RECORDS]`, `history [-n NUMBER_OF_RECORDS] -g GAME_MODE`
+is also acceptable.
+Note: One parameter is defined as the option e.g. `-g` and its value (if there is such a field) e.g. `time`
+* If a parameter is expected only once in the command, but you specified it multiple times,
+only the first occurrence of the parameter will be taken
+e.g. if you specify `history -g time -g time -n 10`, only `history -g time -n 10` will be taken.
+* For commands which have a `-h` parameter, if `-h` is among the parameters provided, all other parameters will be
+ignored and the guide for that command will be displayed.
+e.g `history -g time -h -n 10` will be interpreted as `history -h`.
+* All parameters which do not conform to the command syntax will be ignored.
+e.g.2 `clear -gtime` will be interpreted as `clear`
-### Adding a todo: `todo`
-Adds a new item to the list of todo items.
+### 3.1 Set Content: `content`
+Set the typing content before the game.
+There are two game modes in Typist:
-Format: `todo n/TODO_NAME d/DEADLINE`
+* time-limited (aim to type as much as possible in a limited time)
+* word-limited (aim to type a chosen number of words as fast as possible)
-* The `DEADLINE` can be in a natural language format.
-* The `TODO_NAME` cannot contain punctuation.
+Format: `content` `1-3` `[optional]`
-Example of usage:
+After entering `content`, user can type an integer from 1-3 to choose from following:
-`todo n/Write the rest of the User Guide d/next week`
+1. Opening of famous books
+2. Wikipedia article
+3. Random sentence of custom length
-`todo n/Refactor the User Guide to remove passive voice d/13/04/2020`
+
+
+Examples
+* `content` `1` `3`
+
+
+
+### 3.2 Open Game: `game`
+Start a typing game.
+Format: `game -GAME_MODE [GAME_LIMIT] [-c] [-sn]`
+
+* GAME_MODE
+ * `word` for game in Word Limit Mode
+ * `time` for game in Time Limit Mode
+
+#### 3.2.1 Word Limit Game
+ In a Word-Limit game, user needs to specify the total number of words
+they want to type. Game will terminate once they finish typing the specified
+number of words.
+* Format: `game -word [WORD_LIMIT] [-c] [-sn]`
+* Example: `game -word`
+* ```
+ game -word
+ | Enter how many words you want the game to run:
+ 10
+ | Timer will start once you entered "start":
+ start
+ | Lorem Ipsum is simply dummy
+ lorem ipsum is simply dummy
+ | Your progress:5/10
+ ```
+* Exit: `Exit` allows user to terminate the current game.
+
+#### 3.2.2 Time Limit Game
+In a Time-Limit Game, user needs to specify the duration
+they want to type. Timer will stop once the duration is reached
+and terminates the game at the same time.
+>**NOTE**:
+> The time limit that user inputs needs to be a positive integer that represents time in seconds.
+> It also needs to be multiple of 30.
+
+* Format: `game -time [TIME_LIMIT] [-c] [-sn]`
+* Example: `game -time`
+* ```
+ game -time
+ | Enter how long you want the game to run:
+ 60
+ | Timer will start once you entered "start":
+ start
+ | Lorem Ipsum is simply dummy text of the printing and
+ Lorem Ipsum is simply dummy text of the printing and
+ | Timer's Up!
+ ```
+
+ **Exceeding Time:**
+ When you are at the last sentence. Even though Timer is up, the game will wait
+ until you finish the last sentence to terminate. You will be informed of the exceeding time,
+ and it will count towards your game summary.
+
+#### Optional arguments
+* SET_CONTENT `-c`: allows user to set input content before game starts.
+* START_NOW `-sn`: allows timer to start immediately without the "start timer" prompt.
+* GAME_LIMIT: a positive integer that sets the word/time limit of the game
+ without the "enter limit" prompt.
+ > **NOTE**:
+ > if you want to specify GAME_LIMIT, it needs to be right after the GAME_MODE argument.
+ > If the GAME_LIMIT entered is invalid, game will still start but with the "enter limit" prompt.
+
+ > Any ERROR in the _optional arguments_ will prompt a message but will not terminate the command.
+ > The game will still start as if the optional argument is not given.
+
+
+
+Example 1: `game -time 30`
+Expected outcome:
+```
+game -time 30
+ | Timer will start once you entered "start":
+start
+ | Lorem Ipsum is simply dummy text of the printing and
+```
+Example 2: `game -time 30 -sn`
+Expected outcome:
+```
+game -time -sn
+ | Enter how long you want the game to run:
+30
+ | Lorem Ipsum is simply dummy text of the printing and
+```
+Example 3: `game -word 20 -sn -c`
+Expected outcome:
+```
+(\
+\'\
+ \'\ __________
+ / '| ()_________)
+ \ '/ \ ~~~~~~~~ \
+ \ \ ~~~~~~ \
+ ==). \__________\
+ (__) ()__________)
+ | Content selection (input 0 to go back):
+ | 1. Opening of famous books
+ | 2. Wikipedia article
+ | 3. Random sentence of custom length
+ | Enter your selection:
+```
+
+
+
+### 3.3 View past records: `history`
+View past game records.
+Format: `history -g GAME_MODE [-n NUMBER_OF_RECORDS] [-h]`
+* NUMBER_OF_RECORDS defaults to all records if not provided
+* Possible arguments for GAME_MODE are - `word` or `time`
+
+
+
+Examples
+* `history -g time -n 10`
+* `history -g word`
+
+
+
+Example of usage
+```
+history -g time -n 1
+ __ _____________________ ______ __
+ / / / / _/ ___/_ __/ __ \/ __ \ \/ /
+ / /_/ // / \__ \ / / / / / / /_/ /\ /
+ / __ // / ___/ // / / /_/ / _, _/ / /
+/_/ /_/___//____//_/ \____/_/ |_| /_/
+Game Mode: Time-limited
+WPM: 49.71
+Total Time taken for the game: 36.21 seconds
+Number of Wrong Words: 0/30|0.00%
+Number of Correct Words: 30/30|100.00%
+Mistakes: No words typed wrongly.
+==================================================================
+```
+
+### 3.4 Clear past records: `clear`
+Clear all past game records.
+Format: `clear [-g GAME_MODE] [-h]`
+* GAME_MODE defaults to `all` if not provided
+* Possible arguments for GAME_MODE are - `word`, `time` or `all`
+
+
+
+Examples
+* `clear -g time`
+* `clear`
+
+
+
+Example of usage
+```
+clear -g time
+ ________ _________ ____ ____ ________________ ____ ____ _____
+ / ____/ / / ____/ | / __ \ / __ \/ ____/ ____/ __ \/ __ \/ __ \/ ___/
+ / / / / / __/ / /| | / /_/ / / /_/ / __/ / / / / / / /_/ / / / /\__ \
+/ /___/ /___/ /___/ ___ |/ _, _/ / _, _/ /___/ /___/ /_/ / _, _/ /_/ /___/ /
+\____/_____/_____/_/ |_/_/ |_| /_/ |_/_____/\____/\____/_/ |_/_____//____/
+
+Successfully cleared Time-limited game records.
+```
+
+### 3.5 View summary of game
+View summary of a just-played game.
+Typists automatically generates and displays the summary after a game ends.
+The summary consists of the following fields:
+* Game mode
+* Word per minute (WPM)
+* Total time taken for the game
+* Number of wrong words and its percentage
+* Number of correct words and its percentage
+* Wrongly and not typed words (Mistakes)
+
+
+
+Example of usage
+```
+ _____ __ ____ _____ ______ ______ __
+ / ___// / / / |/ / |/ / | / __ \ \/ /
+ \__ \/ / / / /|_/ / /|_/ / /| | / /_/ /\ /
+ ___/ / /_/ / / / / / / / ___ |/ _, _/ / /
+/____/\____/_/ /_/_/ /_/_/ |_/_/ |_| /_/
+Game Mode: Time-limited
+WPM: 49.71
+Total Time taken for the game: 36.21 seconds
+Number of Wrong Words: 0/30|0.00%
+Number of Correct Words: 30/30|100.00%
+Mistakes: No words typed wrongly.
+```
+### 3.6 Saving the data
+Typists automatically stores the game records (and any changes to them) into text files.
+
+
+### !Warning: Editing the data file
+Typists is a game application and only game records will be stored.
+Gamers should not edit the data it will manipulate the integrity of the records.
+In the event where the data is edited and the wrong format is inputted, the file contents will be cleared,
+hence losing all the game data.
+
+### 3.7 Man Pages: `man`
+
+Review the man pages for different command if you need help.
+Format: `man [COMMAND]`
+* `man` with out specifying the command will display the default man page.
+
+Example: `man clear`
+```
+ | Clear all past game records.
+ | Command format: clear [-g GAME_MODE] [-h]
+```
## FAQ
**Q**: How do I transfer my data to another computer?
-**A**: {your answer here}
+**A**: Copy and paste the save data under the same working directory.
-## Command Summary
+**Q**: The wiki page contains error messages. What should I do?
+
+**A**: Some characters might not be supported by the CLI. You can search for another page as an alternative.
-{Give a 'cheat sheet' of commands here}
+**Q**: Why does the time limit need to be a multiple of 30?
+
+**A**: It's a design choice. You can select the word limit game if you find it too restricting.
+
+
+## Command Summary
-* Add todo `todo n/TODO_NAME d/DEADLINE`
+| Command | Action |
+| ------------- | ------------- |
+|`content`| Set the content
+|`game -word [WORD_LIMIT] [-sn] [-c]` | Start a word-limited game
+|`game -time [TIME_LIMIT] [-sn] [-c]` | Start a time-limited game
+|`history -g GAME_MODE [-n NUMBER_OF_RECORDS] [-h]` | View past game records
+|`clear [-g GAME_MODE] [-h]` | Clear past game records
+|`man [COMMAND]`| View man pages of command
+|`bye`| Exit the program
diff --git a/docs/_config.yml b/docs/_config.yml
new file mode 100644
index 0000000000..c4192631f2
--- /dev/null
+++ b/docs/_config.yml
@@ -0,0 +1 @@
+theme: jekyll-theme-cayman
\ No newline at end of file
diff --git a/docs/diagrams/TimeGame_SequenceDiagram.puml b/docs/diagrams/TimeGame_SequenceDiagram.puml
new file mode 100644
index 0000000000..a128843e20
--- /dev/null
+++ b/docs/diagrams/TimeGame_SequenceDiagram.puml
@@ -0,0 +1,66 @@
+@startuml
+
+box TimeLimitGame
+participant ":TimeLimitGame" as game
+participant "ui:GameUi" as ui
+endBox
+participant ":Util" as util
+participant "in:Scanner" as scanner
+
+[-> game: runGame()
+activate game
+
+create scanner
+game --> scanner: <>
+
+game -> game: getTimeNow()
+activate game
+game --> game: beginTime
+deactivate game
+
+loop until TimeOver
+ alt Time Not Over
+ game -> game: displayLines(currentRow)
+ activate game
+ game -> util: getDisplayLines(...,currentRow)
+ activate util
+ util --> game
+ deactivate util
+ game --> game
+ deactivate game
+
+ game -> scanner: nextLine()
+ activate scanner
+ scanner --> game: line
+ deactivate scanner
+ else Time Over
+end
+
+game -> game: updateUserLines(inputs)
+activate game
+game --> game
+deactivate game
+
+game -> game: endGame()
+activate game
+game -> ui: printEnd("Time game end")
+activate ui
+ui --> game
+deactivate ui
+opt Time Overshoot
+ game -> ui: printOvershoot(overshoot time)
+ activate ui
+ ui --> game
+ deactivate ui
+end
+
+game --> game
+deactivate game
+
+[<-- game
+deactivate game
+
+<[hidden]- game
+destroy game
+
+@enduml
\ No newline at end of file
diff --git a/docs/diagrams/TimeLimitGame.puml b/docs/diagrams/TimeLimitGame.puml
new file mode 100644
index 0000000000..75b39ab42a
--- /dev/null
+++ b/docs/diagrams/TimeLimitGame.puml
@@ -0,0 +1,65 @@
+@startuml
+!include style.puml
+
+participant ":GameCommand" as command
+participant "game:TimeModeGame" as game
+participant "ui:GameUi" as ui
+participant ":Util" as util
+
+-> command: run()
+activate command
+
+create game
+command -> game: createGame(...)
+activate game
+game -> util: splitStringIntoWordList(content)
+activate util
+util --> game: wordList
+deactivate
+game --> command: game
+deactivate
+
+command -> game: runGame();
+activate game
+game -> game: isReady(startNow)
+activate game
+game --> game: ready
+deactivate
+loop until TimeOver
+ alt Time Over
+ else
+ game -> util: getDisplayLines(wordList);
+ activate util
+ util --> game: display
+ deactivate
+ game -> ui: printLine(display)
+ activate ui
+ ui --> game
+ deactivate
+ end
+end
+game -> game: updateUserLines(userinput))
+activate game
+game --> game
+deactivate
+
+game --> command
+deactivate
+
+command -> game: gameSummary()
+activate game
+game -> game: handleSummary(...,"Time-Limited")
+activate game
+game --> game: summary
+deactivate
+game -> game: handleStorage(summary)
+activate game
+game --> game
+deactivate
+
+game --> command
+deactivate
+destroy game
+
+
+@enduml
\ No newline at end of file
diff --git a/docs/diagrams/ViewStatistics-1.drawio.svg b/docs/diagrams/ViewStatistics-1.drawio.svg
new file mode 100644
index 0000000000..0db5031503
--- /dev/null
+++ b/docs/diagrams/ViewStatistics-1.drawio.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/docs/diagrams/ViewStatistics-2.drawio.svg b/docs/diagrams/ViewStatistics-2.drawio.svg
new file mode 100644
index 0000000000..9c82d85d60
--- /dev/null
+++ b/docs/diagrams/ViewStatistics-2.drawio.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/docs/diagrams/ViewStatistics-3.drawio.svg b/docs/diagrams/ViewStatistics-3.drawio.svg
new file mode 100644
index 0000000000..2057f71a61
--- /dev/null
+++ b/docs/diagrams/ViewStatistics-3.drawio.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/docs/diagrams/ViewStatistics-4.drawio.svg b/docs/diagrams/ViewStatistics-4.drawio.svg
new file mode 100644
index 0000000000..d18486c74f
--- /dev/null
+++ b/docs/diagrams/ViewStatistics-4.drawio.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/docs/diagrams/ViewStatistics-5.drawio.svg b/docs/diagrams/ViewStatistics-5.drawio.svg
new file mode 100644
index 0000000000..3d0e9852b3
--- /dev/null
+++ b/docs/diagrams/ViewStatistics-5.drawio.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/docs/diagrams/WordLimitMode.puml b/docs/diagrams/WordLimitMode.puml
new file mode 100644
index 0000000000..9398cd6779
--- /dev/null
+++ b/docs/diagrams/WordLimitMode.puml
@@ -0,0 +1,75 @@
+@startuml
+
+-> WordGameCommand: run()
+activate WordGameCommand
+WordGameCommand -> WordGameCommand: getNumber(args, WORD_SIGNIFIER)
+activate WordGameCommand
+WordGameCommand --> WordGameCommand: wordLimit
+deactivate
+
+create WordLimitGame
+WordGameCommand -> WordLimitGame: createGame(args, isReady, setContent)
+activate WordLimitGame
+WordLimitGame -> TextUi: getWordLimit()
+activate TextUi
+TextUi --> WordLimitGame: isValidWord(n)
+deactivate
+WordLimitGame --> WordGameCommand
+deactivate
+
+WordGameCommand -> WordLimitGame: runGame()
+activate WordLimitGame
+WordLimitGame -> WordLimitGame: trimContent(limit)
+activate WordLimitGame
+WordLimitGame --> WordLimitGame
+deactivate
+WordLimitGame -> WordLimitGame: getTimeNow()
+activate WordLimitGame
+WordLimitGame --> WordLimitGame
+deactivate
+
+loop until Exit
+ WordLimitGame -> Utils: displayLines(row)
+ activate Utils
+ Utils --> WordLimitGame: line
+ deactivate
+ WordLimitGame -> TextUi: printLine(displayed)
+ activate TextUi
+ TextUi --> WordLimitGame
+ deactivate
+ WordLimitGame -> TextUi: readCommand()
+ activate TextUi
+ TextUi --> WordLimitGame: inputs
+ deactivate
+ alt Exit
+ else continue typing
+ WordLimitGame -> WordLimitGame: updateUserLines(inputs)
+ activate WordLimitGame
+ end
+ deactivate
+end
+
+WordLimitGame --> WordGameCommand
+deactivate
+WordGameCommand -> WordLimitGame: gameSummary()
+activate WordLimitGame
+
+WordLimitGame -> WordLimitGame: getDuration()
+activate WordLimitGame
+WordLimitGame --> WordLimitGame: gameTime
+deactivate
+
+WordLimitGame -> TextUi: handleSummary(...,"Word-Limited")
+activate TextUi
+TextUi --> WordLimitGame: summary
+deactivate
+WordLimitGame -> WordLimitGame: handleStorage(summary)
+activate WordLimitGame
+WordLimitGame --> WordLimitGame
+deactivate
+
+WordLimitGame --> WordGameCommand
+deactivate
+destroy WordLimitGame
+
+@enduml
\ No newline at end of file
diff --git a/docs/diagrams/archdiagram.png b/docs/diagrams/archdiagram.png
new file mode 100644
index 0000000000..eaa327aeb4
Binary files /dev/null and b/docs/diagrams/archdiagram.png differ
diff --git a/docs/diagrams/architecture.puml b/docs/diagrams/architecture.puml
new file mode 100644
index 0000000000..2ba54a2124
--- /dev/null
+++ b/docs/diagrams/architecture.puml
@@ -0,0 +1,80 @@
+@startuml
+'https://plantuml.com/class-diagram
+
+class Main
+interface Command
+class ClearCommand
+class ContentCommand
+class ExitCommand
+abstract class GameCommand
+class HistoryCommand
+class TimeGameCommand
+class WordGameCommand
+class CommandFactory
+class Books
+class Utils
+class SummaryUi
+class StringParser
+class Content
+class TimeLimitGame
+class WordLimitGame
+abstract class Game
+class Storage
+class Animation
+class RandomGenerator
+class WikiImport
+class GameRecord
+class GameRecordsManager
+class SummaryManager
+class FileParser
+class ClearCommandUi
+class GameUi
+class HistoryCommandUi
+class TextUi
+
+ClearCommand <.. Command
+ContentCommand <.. Command
+ExitCommand <.. Command
+GameCommand <.. Command
+HistoryCommand <.. Command
+TimeGameCommand <|-- GameCommand
+WordGameCommand <|-- GameCommand
+TimeGameCommand <|-- Game
+WordLimitGame <|-- Game
+GameUi <|-- TextUi
+SummaryUi <|-- TextUi
+Storage *- Main
+Content *-- Main
+TextUi *-- Main
+CommandFactory *-- Main
+Command *- Main
+Animation *-- TextUi
+Content *-- Animation
+GameUi *-- Game
+SummaryUi *-- Game
+SummaryManager *-- Game
+GameRecordsManager *-- Game
+Game *-- GameCommand
+GameUi *-- GameCommand
+GameRecordsManager *-- ClearCommand
+ClearCommandUi *-- ClearCommand
+GameRecordsManager *-- ExitCommand
+GameRecordsManager *-- HistoryCommand
+HistoryCommandUi *-- HistoryCommand
+TimeLimitGame *-- TimeGameCommand
+WordLimitGame *-- WordGameCommand
+TextUi *- CommandFactory
+TextUi *-- Utils
+TextUi *--- Content
+WikiImport *-- Content
+RandomGenerator *-- Content
+Books *-- Content
+Storage *-- GameRecordsManager
+GameRecord *-- GameRecordsManager
+TextUi *--- TimeLimitGame
+TextUi *-- WordLimitGame
+GameRecord *-- FileParser
+GameRecord *-- Storage
+StringParser *-- WordLimitGame
+
+@enduml
\ No newline at end of file
diff --git a/docs/diagrams/command.puml b/docs/diagrams/command.puml
new file mode 100644
index 0000000000..779342f81a
--- /dev/null
+++ b/docs/diagrams/command.puml
@@ -0,0 +1,57 @@
+@startuml
+!include style.puml
+skinparam arrowThickness 1.1
+skinparam arrowColor LOGIC_COLOR_T4
+skinparam classBackgroundColor LOGIC_COLOR
+
+package Storage {
+}
+
+package Game {
+}
+
+package Command {
+Class CommandFactory
+
+package commands #F4F6F6{
+Class ClearCommand
+Class ContentCommand
+Class ExitCommand
+Class HistoryCommand
+Class "{abstract}\nGameCommand" as GameCommand
+Class TimeGameCommand
+Class WordGameCommand
+
+Interface Command <>
+}
+}
+
+
+Class HiddenOutside #FFFFFF
+HiddenOutside ..> CommandFactory
+
+CommandFactory ..> ClearCommand
+CommandFactory ..> ContentCommand: creates >
+CommandFactory ..> ExitCommand
+CommandFactory ..> GameCommand
+CommandFactory ..> HistoryCommand
+
+GameCommand -> Game
+TimeGameCommand --> Game
+WordGameCommand --> Game
+
+HistoryCommand --> Storage
+ClearCommand --> Storage
+
+ClearCommand ..|> Command
+ContentCommand ..|> Command
+ExitCommand ..|> Command
+GameCommand ..|> Command
+HistoryCommand ..|> Command
+
+TimeGameCommand -up-|> GameCommand
+WordGameCommand -up-|> GameCommand
+
+
+
+@enduml
\ No newline at end of file
diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml
new file mode 100644
index 0000000000..2c46e043f2
--- /dev/null
+++ b/docs/diagrams/style.puml
@@ -0,0 +1,75 @@
+/'
+ 'Commonly used styles and colors across diagrams.
+ 'Refer to https://plantuml-documentation.readthedocs.io/en/latest for a more
+ 'comprehensive list of skinparams.
+ '/
+
+
+'T1 through T4 are shades of the original color from lightest to darkest
+
+!define UI_COLOR #1D8900
+!define UI_COLOR_T1 #83E769
+!define UI_COLOR_T2 #3FC71B
+!define UI_COLOR_T3 #166800
+!define UI_COLOR_T4 #0E4100
+
+!define LOGIC_COLOR #3333C4
+!define LOGIC_COLOR_T1 #C8C8FA
+!define LOGIC_COLOR_T2 #6A6ADC
+!define LOGIC_COLOR_T3 #1616B0
+!define LOGIC_COLOR_T4 #101086
+
+!define MODEL_COLOR #9D0012
+!define MODEL_COLOR_T1 #F97181
+!define MODEL_COLOR_T2 #E41F36
+!define MODEL_COLOR_T3 #7B000E
+!define MODEL_COLOR_T4 #51000A
+
+!define STORAGE_COLOR #A38300
+!define STORAGE_COLOR_T1 #FFE374
+!define STORAGE_COLOR_T2 #EDC520
+!define STORAGE_COLOR_T3 #806600
+!define STORAGE_COLOR_T2 #544400
+
+!define USER_COLOR #000000
+
+skinparam BackgroundColor #FFFFFFF
+
+skinparam Shadowing false
+
+skinparam Class {
+ FontColor #FFFFFF
+ BorderThickness 1
+ BorderColor #FFFFFF
+ StereotypeFontColor #FFFFFF
+ FontName Arial
+}
+
+skinparam Actor {
+ BorderColor USER_COLOR
+ Color USER_COLOR
+ FontName Arial
+}
+
+skinparam Sequence {
+ MessageAlign center
+ BoxFontSize 15
+ BoxPadding 0
+ BoxFontColor #FFFFFF
+ FontName Arial
+}
+
+skinparam Participant {
+ FontColor #FFFFFFF
+ Padding 20
+}
+
+skinparam MinClassWidth 50
+skinparam ParticipantPadding 10
+skinparam Shadowing false
+skinparam DefaultTextAlignment center
+skinparam packageStyle Rectangle
+
+hide footbox
+hide members
+hide circle
\ No newline at end of file
diff --git a/docs/images/Content.png b/docs/images/Content.png
new file mode 100644
index 0000000000..a27f80679b
Binary files /dev/null and b/docs/images/Content.png differ
diff --git a/docs/images/TimeGame_SequenceDiagram.png b/docs/images/TimeGame_SequenceDiagram.png
new file mode 100644
index 0000000000..3e6d6ed097
Binary files /dev/null and b/docs/images/TimeGame_SequenceDiagram.png differ
diff --git a/docs/images/WordLimitMode.png b/docs/images/WordLimitMode.png
new file mode 100644
index 0000000000..4143172c50
Binary files /dev/null and b/docs/images/WordLimitMode.png differ
diff --git a/docs/images/command.png b/docs/images/command.png
new file mode 100644
index 0000000000..fdeade20a6
Binary files /dev/null and b/docs/images/command.png differ
diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md
deleted file mode 100644
index ab75b391b8..0000000000
--- a/docs/team/johndoe.md
+++ /dev/null
@@ -1,6 +0,0 @@
-# John Doe - Project Portfolio Page
-
-## Overview
-
-
-### Summary of Contributions
diff --git a/docs/team/limjiajing.md b/docs/team/limjiajing.md
new file mode 100644
index 0000000000..c6964a6d6d
--- /dev/null
+++ b/docs/team/limjiajing.md
@@ -0,0 +1,64 @@
+# Lim Jia Jing - Project Portfolio Page
+
+## Overview
+Typists is a command line interface typing game targeted at users who love to type and are
+familiar with the command line interface. There are two game modes in types, time-limited
+(aim to type as much as possible in a limited time) and word-limited (aim to type a chosen
+number of words as fast as possible).
+
+### Summary of Contributions
+#### Code contributed: [RepoSense link](https://nus-cs2113-ay2122s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&since=2021-09-25&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=false&tabOpen=true&tabType=authorship&tabAuthor=LimJiaJing&tabRepo=AY2122S1-CS2113-T13-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false)
+
+#### Contribution to code design
+Suggest having all command classes implement a common interface and using factory
+design pattern to create instantiate objects of classes which implement the interface
+for code to have less coupling.
+
+#### Enhancements implemented
+* New feature: Added the summary feature that will show the game's summary after a game finishes.
+ * What it does: Shows the summary of the game to gamers after they complete a game.
+ * Justification: This feature improves the project significantly because gamers mostly
+ want to know how they did in a game.
+ * Highlights: The logic of getting the mistakes made was fairly complicated as it required taking into
+ considerations many conditions and scenarios. The implementation of this feature also affects the implementation
+ of the `view history` feature, hence the design had to be well-thought-out.
+
+
+
+* New feature: Added the ability to view records of past games.
+ * What it does: Allows gamers to select which game mode's past records they want to view
+ and how many records (from the most recent) they want to view. Defaults to
+ all the records in the selected game mode if they do not specify the number to records to view.
+ * Justification: Gamers would want to view how they did in the past and see how they improved over time.
+ The gamers could also be typists who use this application to improve their typing. They might want to view
+ which words they type wrongly most often so that they can work on it.
+ * Highlights: The command related to this feature `history` has both optional and compulsory parameters. The parsing
+ for this command was hence rather complicated to implement.
+
+
+* New feature: Added the ability to clear all past game records.
+ * What it does: Clear the past game records of a selected game mode. If the game mode is
+ not specified, game records from both game modes are wiped.
+ * Justification: Gamers would at times like to clear their past records so that they can start afresh.
+
+
+* New feature: Added automatic storage of game records.
+ * What it does: Automatically stores changes in game records to the corresponding text file
+ (different text files for different game modes).
+ * Justification: This feature is required for the `view history` feature.
+ * Highlights: Extensive exception handling had to be done for the scenario where
+ the gamer modifies the text files to an unreadable format.
+
+#### Documentation
+* User Guide
+ * Added `Notes about command format` section.
+ * Added documentation for features `view game summary`, `view past records`,
+ `clear past records`and `automatic storage`.
+* Developer Guide
+ * Added implementation details for the proposed `view statistics` feature.
+
+#### Project management
+* Assigned issues created during practical examination dry-run to teammates responsible for those parts.
+* Wrapped up milestones.
+* Create issues for v1.0 user stories.
+
diff --git a/docs/team/lusicheng.md b/docs/team/lusicheng.md
new file mode 100644
index 0000000000..87ee49dba1
--- /dev/null
+++ b/docs/team/lusicheng.md
@@ -0,0 +1,58 @@
+# Lu Sicheng - Project Portfolio Page
+
+## Overview: TYPIST
+Typists is a command line interface typing game targeted at users who love to type and are
+familiar with the command line interface. There are two game modes in types, time-limited
+(aim to type as much as possible in a limited time) and word-limited (aim to type a chosen
+number of words as fast as possible).
+
+Given below are my contributions to the project:
+
+#### Code contributed: [RepoSense link](https://nus-cs2113-ay2122s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2021-09-25&tabOpen=true&tabType=zoom&zA=Isabella-L&zR=AY2122S1-CS2113-T13-4%2Ftp%5Bmaster%5D&zACS=167.6350974930362&zS=2021-09-25&zFS=&zU=2021-11-08&zMG=false&zFTF=commit&zFGS=groupByRepos&zFR=false)
+
+#### Features
+
+* **New Feature**: Added the Time-Limited Game
+ * What it does: allows the user to type words within a set period of time and analyse the result.
+ * Justification: It allows the user to control the input time.
+ * Highlights: This feature is one of the 2 game modes. It is the one of the fundamentals of Typist.
+ And it required manipulation of the `Data/Time` java API.
+* **New Feature**: Added the ability to put game usages into 1 command
+ * What it does: gives the user option to input all usage command at once.
+ For instance, the `-sn` tag will enable user to start timer immediately without the "Enter 'start' to start timer" prompt.
+ The `-c` tag enable user to override the current content set before starting game.
+ Adding integer directly after `game -MODE ` will avoid the "enter the game limit" prompt.
+ * Justification: This feature increases the usability significantly because users (assumed familiar
+ with cli and typing) would have a much more convenient, effective and smooth experience using the app.
+ * Highlights: This enhancement affects existing commands and commands to be added in the future. It required
+ rather sophisticated dealing of arraylists, exceptions and constructor overloading for `game` and its subclassess.
+* **New Feature**: Added the feature to display different man pages for different commands.
+
+#### Project Management
+
+* Managed Releases: `v1.0`, `v2.1` on GitHub.
+
+#### Enhancements to existing features:
+
+* Implemented the Factory Design pattern suggested by [Jiajing](limjiajing.md): [#56](https://github.com/AY2122S1-CS2113-T13-4/tp/pull/56).
+* Added Typist starting display (with ASCII) and other display format:
+[#15](https://github.com/AY2122S1-CS2113-T13-4/tp/pull/15),
+[#16](https://github.com/AY2122S1-CS2113-T13-4/tp/pull/16).
+
+#### Documentation
+
+* User Guide:
+ * Added documentation for `game` feature (excluding Word Limit Game section by [Zhansen](shizhansen.md)) and Command Summary: [#152](https://github.com/AY2122S1-CS2113-T13-4/tp/pull/152).
+ * Added documentations for Introduction, quickstart, table of content and other cosmetic tweaks.
+ * Added documentations for `man` feature.
+* Developer Guide:
+ * Added design detail for Architecture (excluding the diagram by [William](williamwahyudi.md).
+ * Added design detail for Command Component, Game Component and Time Limit Game section: [#155](https://github.com/AY2122S1-CS2113-T13-4/tp/pull/155).
+ * Added details for manual testing.
+
+#### Community
+* [16 PRs reviewed](https://github.com/AY2122S1-CS2113-T13-4/tp/pulls?q=is%3Apr+is%3Aclosed)
+* Some parts of `Util` and `TextUi` features I added was adopted by other classmates([#87](https://github.com/AY2122S1-CS2113-T13-4/tp/pull/87)).
+* The `game` class I added is inherited by both `TimeLimitGame` and `WordLimitGame`
+ ([#56](https://github.com/AY2122S1-CS2113-T13-4/tp/pull/56), [#144](https://github.com/AY2122S1-CS2113-T13-4/tp/pull/144)).
+* The `GameCommand` class I added is also applied to both `TimeGameCommand` and `WordGameCommand`([#81](https://github.com/AY2122S1-CS2113-T13-4/tp/pull/81/files)).
diff --git a/docs/team/shizhansen.md b/docs/team/shizhansen.md
new file mode 100644
index 0000000000..e8e7b36453
--- /dev/null
+++ b/docs/team/shizhansen.md
@@ -0,0 +1,20 @@
+# Shi Zhansen - Project Portfolio Page
+
+## Overview
+Typists is a command line interface typing game targeted at users who love to type and are
+familiar with the command line interface. There are two game modes in types, time-limited
+(aim to type as much as possible in a limited time) and word-limited (aim to type a chosen
+number of words as fast as possible).
+
+### Summary of Contributions
+* Code:
+ * Edit the WordLimitGame part of code
+ * Add method testings
+* User Guide
+ * Edit 3.2 Open Game and 3.2.1 Word Limit Game
+* Developer Guide
+ * Details about the WordLimitGame feature and sequence diagram
+
+#### Project management
+* Merged pull requests
+* Ensure code integrate with the team's code
\ No newline at end of file
diff --git a/docs/team/williamwahyudi.md b/docs/team/williamwahyudi.md
new file mode 100644
index 0000000000..7d97c93730
--- /dev/null
+++ b/docs/team/williamwahyudi.md
@@ -0,0 +1,23 @@
+# William Wahyudi - Project Portfolio Page
+
+## Overview
+Typists is a command line interface typing game targeted at users who love to type and are
+familiar with the command line interface. There are two game modes in types, time-limited
+(aim to type as much as possible in a limited time) and word-limited (aim to type a chosen
+number of words as fast as possible).
+
+### Summary of Contributions
+#### Code contributed: [RepoSense link](https://nus-cs2113-ay2122s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&since=2021-09-25&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=false&tabOpen=true&tabType=authorship&tabAuthor=maxwireddevs&tabRepo=AY2122S1-CS2113-T13-4%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false)
+
+#### Contribution to code design
+Added content selection and a bit of effects.
+
+#### Documentation
+* User Guide
+ * FAQ
+* Developer Guide
+ * Details about the content selection feature and architecture diagram
+
+#### Project management
+* Merged pull requests
+* Helped to fix the CI check errors
\ No newline at end of file
diff --git a/seedu.typists.ui.TextUi.log.1 b/seedu.typists.ui.TextUi.log.1
new file mode 100644
index 0000000000..6ce50be5f0
--- /dev/null
+++ b/seedu.typists.ui.TextUi.log.1
@@ -0,0 +1,2 @@
+Nov 06, 2021 5:32:11 PM seedu.typists.ui.SummaryUi setUpLog
+INFO: Set up log in SummaryUi
diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF
new file mode 100644
index 0000000000..bd3a68a258
--- /dev/null
+++ b/src/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Main-Class: seedu.typists.Main
+
diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java
deleted file mode 100644
index 5c74e68d59..0000000000
--- a/src/main/java/seedu/duke/Duke.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package seedu.duke;
-
-import java.util.Scanner;
-
-public class Duke {
- /**
- * Main entry-point for the java.duke.Duke application.
- */
- public static void main(String[] args) {
- String logo = " ____ _ \n"
- + "| _ \\ _ _| | _____ \n"
- + "| | | | | | | |/ / _ \\\n"
- + "| |_| | |_| | < __/\n"
- + "|____/ \\__,_|_|\\_\\___|\n";
- System.out.println("Hello from\n" + logo);
- System.out.println("What is your name?");
-
- Scanner in = new Scanner(System.in);
- System.out.println("Hello " + in.nextLine());
- }
-}
diff --git a/src/main/java/seedu/typists/Main.java b/src/main/java/seedu/typists/Main.java
new file mode 100644
index 0000000000..bdf96202e7
--- /dev/null
+++ b/src/main/java/seedu/typists/Main.java
@@ -0,0 +1,76 @@
+package seedu.typists;
+
+import seedu.typists.content.Content;
+import seedu.typists.command.commands.Command;
+import seedu.typists.command.CommandFactory;
+import seedu.typists.ui.SummaryUi;
+import seedu.typists.ui.TextUi;
+
+import seedu.typists.storage.Storage;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Scanner;
+
+
+public class Main {
+ /**
+ * Version info of the program.
+ */
+ public static final String VERSION = "Typist - Version 2.1";
+ public static final int LINE_LENGTH = 10;
+ public static Content content;
+ public static TextUi uiBot;
+ Storage storage;
+
+
+ public Main() {
+ uiBot = new TextUi();
+ this.storage = new Storage();
+ content = new Content();
+ }
+
+ public void start() {
+ uiBot.showWelcomeMessage(VERSION);
+ }
+
+ public String read() {
+ Scanner sc = new Scanner(System.in);
+ return sc.nextLine();
+ }
+
+ public void runCommandLoop() {
+ CommandFactory cmdFactory = new CommandFactory();
+ String command;
+ do {
+ String[] input = read().split(" ");
+ command = input[0];
+ ArrayList args = new ArrayList<>(Arrays.asList(input));
+ Command c = cmdFactory.getCommand(args);
+ if (c != null) {
+ c.run(args);
+ }
+ } while (!command.equals("bye"));
+ }
+
+ private void setUpLogs() {
+ SummaryUi.setUpLog();
+ Storage.setUpLog();
+ }
+
+ /**
+ * Runs the program until termination.
+ */
+ public void run() {
+ setUpLogs();
+ start();
+ runCommandLoop();
+ }
+
+ /**
+ * Main entry-point for the java.duke.Duke application.
+ */
+ public static void main(String[] args) {
+ new Main().run();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/seedu/typists/command/CommandFactory.java b/src/main/java/seedu/typists/command/CommandFactory.java
new file mode 100644
index 0000000000..c615a09cb7
--- /dev/null
+++ b/src/main/java/seedu/typists/command/CommandFactory.java
@@ -0,0 +1,51 @@
+package seedu.typists.command;
+
+import seedu.typists.command.commands.Command;
+import seedu.typists.command.commands.ContentCommand;
+import seedu.typists.command.commands.HistoryCommand;
+import seedu.typists.command.commands.ClearCommand;
+import seedu.typists.command.commands.ExitCommand;
+import seedu.typists.command.commands.GameCommand;
+import seedu.typists.command.commands.TimeGameCommand;
+import seedu.typists.command.commands.WordGameCommand;
+import seedu.typists.command.commands.ManCommand;
+import seedu.typists.ui.TextUi;
+
+import java.util.ArrayList;
+
+public class CommandFactory {
+ private static final String TIME_SIGNIFIER = "-time";
+ private static final String WORD_SIGNIFIER = "-word";
+ TextUi ui = new TextUi();
+
+ public Command getCommand(ArrayList args) {
+ String commandType = args.get(0);
+ switch (commandType) {
+ case "game":
+ return parseGameCommand(args);
+ case "content":
+ return new ContentCommand();
+ case "history":
+ return new HistoryCommand();
+ case "clear":
+ return new ClearCommand();
+ case "man":
+ return new ManCommand();
+ case "bye":
+ return new ExitCommand();
+ default:
+ ui.printScreen("invalid command");
+ return null;
+ }
+ }
+
+ public GameCommand parseGameCommand(ArrayList args) {
+ if (args.contains(TIME_SIGNIFIER)) {
+ return new TimeGameCommand();
+ } else if (args.contains(WORD_SIGNIFIER)) {
+ return new WordGameCommand();
+ }
+ ui.printScreen("Please specify game type: -time or -word");
+ return null;
+ }
+}
diff --git a/src/main/java/seedu/typists/command/commands/ClearCommand.java b/src/main/java/seedu/typists/command/commands/ClearCommand.java
new file mode 100644
index 0000000000..32eb84d5b2
--- /dev/null
+++ b/src/main/java/seedu/typists/command/commands/ClearCommand.java
@@ -0,0 +1,75 @@
+package seedu.typists.command.commands;
+
+import seedu.typists.common.exception.InvalidCommandException;
+import seedu.typists.game.GameRecordsManager;
+import seedu.typists.ui.ClearCommandUi;
+
+import java.util.ArrayList;
+
+public class ClearCommand implements Command {
+ private int numberOfRecords = 0;
+ private String gameMode;
+ private ArrayList args;
+ GameRecordsManager gameRecordsManager;
+ private static final String HELP_OPTION = "h";
+ private static final String GAME_MODE_OPTION = "g";
+
+ public void run(ArrayList args) {
+ this.args = args;
+ boolean isHelp = isHelpCommand();
+ if (isHelp) {
+ ClearCommandUi.displayHelp();
+ } else {
+ executeCommand();
+ }
+ }
+
+ public void executeCommand() {
+ gameRecordsManager = GameRecordsManager.getGameRecordsManager();
+ try {
+ parseCommand();
+ gameRecordsManager.clearAllRecords(gameMode);
+ ClearCommandUi.displaySuccess(gameMode);
+ } catch (InvalidCommandException e) {
+ String errorMessage = "Error: " + e.toString().split(":")[1];
+ System.out.print(errorMessage);
+ }
+ }
+
+ private boolean isHelpCommand() {
+ int indexHelp = args.indexOf("-" + HELP_OPTION);
+ if (indexHelp != -1) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void parseCommand() throws InvalidCommandException {
+ getGameMode();
+ }
+
+ private void getGameMode() throws InvalidCommandException {
+ int indexGameMode = args.indexOf("-" + GAME_MODE_OPTION);
+ if (indexGameMode == -1) {
+ gameMode = "all";
+ } else {
+ if (args.size() < indexGameMode + 2) {
+ throw new InvalidCommandException("Missing argument for game mode option.\n");
+ }
+ gameMode = args.get(indexGameMode + 1);
+
+ }
+ if (!gameMode.equals("time") && !gameMode.equals("word") && !gameMode.equals("all")) {
+ throw new InvalidCommandException("Invalid argument for game mode option.\n"
+ + "Input \"clear -h\" for help.\n");
+ } else {
+ if (gameMode.equals("all")) {
+ gameMode = "all";
+ } else {
+ gameMode = gameMode.substring(0, 1).toUpperCase() + gameMode.substring(1) + "-limited";
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/seedu/typists/command/commands/Command.java b/src/main/java/seedu/typists/command/commands/Command.java
new file mode 100644
index 0000000000..4f58d011ad
--- /dev/null
+++ b/src/main/java/seedu/typists/command/commands/Command.java
@@ -0,0 +1,7 @@
+package seedu.typists.command.commands;
+
+import java.util.ArrayList;
+
+public interface Command {
+ void run(ArrayList args);
+}
diff --git a/src/main/java/seedu/typists/command/commands/ContentCommand.java b/src/main/java/seedu/typists/command/commands/ContentCommand.java
new file mode 100644
index 0000000000..2c666c6f69
--- /dev/null
+++ b/src/main/java/seedu/typists/command/commands/ContentCommand.java
@@ -0,0 +1,14 @@
+package seedu.typists.command.commands;
+
+import java.util.ArrayList;
+
+import static seedu.typists.Main.content;
+
+public class ContentCommand implements Command {
+
+ @Override
+ public void run(ArrayList args) {
+ content.setContent();
+ }
+
+}
diff --git a/src/main/java/seedu/typists/command/commands/ExitCommand.java b/src/main/java/seedu/typists/command/commands/ExitCommand.java
new file mode 100644
index 0000000000..d5faed77d9
--- /dev/null
+++ b/src/main/java/seedu/typists/command/commands/ExitCommand.java
@@ -0,0 +1,17 @@
+package seedu.typists.command.commands;
+
+import seedu.typists.game.GameRecordsManager;
+
+import java.util.ArrayList;
+
+import static seedu.typists.Main.uiBot;
+
+public class ExitCommand implements Command {
+ @Override
+ public void run(ArrayList args) {
+ System.out.println("Updating game records...");
+ GameRecordsManager gameRecordsManager = GameRecordsManager.getGameRecordsManager();
+ gameRecordsManager.updateGameRecords();
+ uiBot.showBye();
+ }
+}
diff --git a/src/main/java/seedu/typists/command/commands/GameCommand.java b/src/main/java/seedu/typists/command/commands/GameCommand.java
new file mode 100644
index 0000000000..345236fb5f
--- /dev/null
+++ b/src/main/java/seedu/typists/command/commands/GameCommand.java
@@ -0,0 +1,54 @@
+package seedu.typists.command.commands;
+
+import seedu.typists.common.exception.IncompleteCommandException;
+import seedu.typists.common.exception.InvalidCommandException;
+import seedu.typists.game.Game;
+import seedu.typists.ui.GameUi;
+
+import java.util.ArrayList;
+
+import static seedu.typists.Main.LINE_LENGTH;
+
+public abstract class GameCommand implements Command {
+ private static final String START_SIGNIFIER = "-sn";
+ private static final String CONTENT_SIGNIFIER = "-c";
+
+ @Override
+ public void run(ArrayList args) {
+ boolean isReady = getBoolean(args, START_SIGNIFIER);
+ boolean setContent = getBoolean(args, CONTENT_SIGNIFIER);
+
+ try {
+ Game game = createGame(args, isReady, setContent);
+ game.run();
+ game.gameSummary();
+ } catch (NullPointerException e) {
+ //exit
+ } catch (StackOverflowError e) {
+ new GameUi().printResetContent(LINE_LENGTH);
+ }
+ }
+
+ public abstract Game createGame(ArrayList args, boolean isReady, boolean setContent);
+
+ /** Determine whether the user command has the signifier. **/
+ public boolean getBoolean(ArrayList args, String key) {
+ return args.contains(key);
+ }
+
+ /** Get the number after the game type indicator. The number will specify
+ * for time game: time limit (in seconds)
+ * for word game: word limit
+ **/
+ public int getNumber(ArrayList args, String gameType)
+ throws InvalidCommandException, IncompleteCommandException {
+ //if number is negative or out of range, throw exception
+ int index = args.indexOf(gameType);
+ try {
+ return Integer.parseInt(args.get(index + 1));
+ } catch (IndexOutOfBoundsException e) {
+ //no suffix after -
+ throw new IncompleteCommandException();
+ }
+ }
+}
diff --git a/src/main/java/seedu/typists/command/commands/HistoryCommand.java b/src/main/java/seedu/typists/command/commands/HistoryCommand.java
new file mode 100644
index 0000000000..d379bdd7b7
--- /dev/null
+++ b/src/main/java/seedu/typists/command/commands/HistoryCommand.java
@@ -0,0 +1,105 @@
+package seedu.typists.command.commands;
+
+import seedu.typists.common.exception.InvalidCommandException;
+import seedu.typists.game.GameRecord;
+import seedu.typists.game.GameRecordsManager;
+import seedu.typists.ui.HistoryCommandUi;
+
+import java.util.ArrayList;
+
+public class HistoryCommand implements Command {
+ private int numberOfRecords = 10;
+ private int totalNumberOfRecords;
+ private String gameMode;
+ private ArrayList args;
+ private static final String GAME_MODE_OPTION = "g";
+ private static final String NUMBER_OF_RECORDS_OPTION = "n";
+ private static final String HELP_OPTION = "h";
+ private static GameRecordsManager gameRecordsManager;
+
+ public void run(ArrayList args) {
+ this.args = args;
+ boolean isHelp = isHelpCommand();
+ if (isHelp) {
+ HistoryCommandUi.displayHelp();
+ } else {
+ executeCommand();
+ }
+ }
+
+ private void executeCommand() {
+ gameRecordsManager = GameRecordsManager.getGameRecordsManager();
+ try {
+ parseCommand();
+ ArrayList gameRecords = gameRecordsManager.getGameRecords(gameMode, numberOfRecords);
+ HistoryCommandUi.displayGameRecords(gameRecords, totalNumberOfRecords);
+ } catch (InvalidCommandException e) {
+ String errorMessage = "Error: " + e.toString().split(":")[1];
+ System.out.print(errorMessage);
+ }
+ }
+
+
+ private boolean isHelpCommand() {
+ int indexHelp = args.indexOf("-" + HELP_OPTION);
+ if (indexHelp != -1) {
+ return true;
+ } else {
+ return false;
+ }
+
+ }
+
+ private void parseCommand() throws InvalidCommandException {
+ getGameMode();
+ getNumberOfRecords();
+ }
+
+ private void getNumberOfRecords() throws InvalidCommandException {
+ int indexNumberOfRecords = args.indexOf("-" + NUMBER_OF_RECORDS_OPTION);
+ if (indexNumberOfRecords == -1) {
+ totalNumberOfRecords = gameRecordsManager.getNumberOfGameRecords(gameMode);
+ numberOfRecords = totalNumberOfRecords;
+ return;
+ } else {
+ if (args.size() < indexNumberOfRecords + 2) {
+ throw new InvalidCommandException("Missing argument for number of records option.\n");
+ }
+ try {
+ totalNumberOfRecords = gameRecordsManager.getNumberOfGameRecords(gameMode);
+ numberOfRecords = Integer.parseInt(args.get(indexNumberOfRecords + 1));
+ if (numberOfRecords <= 0) {
+ throw new InvalidCommandException("Invalid argument for number of records option.\n"
+ + "Option only accepts positive integer values.\n"
+ + "Input \"history -h\" for help.\n");
+ }
+ } catch (NumberFormatException e) {
+ throw new InvalidCommandException("Invalid argument for number of records option.\n"
+ + "Option only accepts positive integer values.\n"
+ + "Input \"history -h\" for help.\n");
+ }
+ }
+
+ }
+
+ private void getGameMode() throws InvalidCommandException {
+ int indexGameMode = args.indexOf("-" + GAME_MODE_OPTION);
+ if (indexGameMode == -1) {
+ throw new InvalidCommandException("Invalid command.\n"
+ + "Input \"history -h\" for help.\n");
+ } else {
+ if (args.size() < indexGameMode + 2) {
+ throw new InvalidCommandException("Missing argument for game mode option.\n");
+ }
+ gameMode = args.get(indexGameMode + 1);
+
+ }
+ if (!gameMode.equals("time") && !gameMode.equals("word")) {
+ throw new InvalidCommandException("Invalid argument for game mode option.\n"
+ + "Input \"history -h\" for help.\n");
+ } else {
+ gameMode = gameMode.substring(0, 1).toUpperCase() + gameMode.substring(1) + "-limited";
+ }
+ }
+
+}
diff --git a/src/main/java/seedu/typists/command/commands/ManCommand.java b/src/main/java/seedu/typists/command/commands/ManCommand.java
new file mode 100644
index 0000000000..96d4e15986
--- /dev/null
+++ b/src/main/java/seedu/typists/command/commands/ManCommand.java
@@ -0,0 +1,41 @@
+package seedu.typists.command.commands;
+
+import java.util.ArrayList;
+
+import static seedu.typists.Main.uiBot;
+import static seedu.typists.common.Messages.MESSAGE_HELP;
+import static seedu.typists.common.Messages.MESSAGE_HELP_CLEAR;
+import static seedu.typists.common.Messages.MESSAGE_HELP_CONTENT;
+import static seedu.typists.common.Messages.MESSAGE_HELP_GAME;
+import static seedu.typists.common.Messages.MESSAGE_HELP_HISTORY;
+
+public class ManCommand implements Command {
+
+ @Override
+ public void run(ArrayList args) {
+ if (args.size() == 1) {
+ assert args.get(0).equals("man");
+ uiBot.printScreen(MESSAGE_HELP);
+ return;
+ }
+
+ String manType = args.get(1);
+ switch (manType) {
+ case "game":
+ uiBot.printScreen(MESSAGE_HELP_GAME);
+ break;
+ case "history":
+ uiBot.printScreen(MESSAGE_HELP_HISTORY);
+ break;
+ case "content":
+ uiBot.printScreen(MESSAGE_HELP_CONTENT);
+ break;
+ case "clear":
+ uiBot.printScreen(MESSAGE_HELP_CLEAR);
+ break;
+ default:
+ uiBot.printScreen("no such command");
+ break;
+ }
+ }
+}
diff --git a/src/main/java/seedu/typists/command/commands/TimeGameCommand.java b/src/main/java/seedu/typists/command/commands/TimeGameCommand.java
new file mode 100644
index 0000000000..d046eb8d6d
--- /dev/null
+++ b/src/main/java/seedu/typists/command/commands/TimeGameCommand.java
@@ -0,0 +1,36 @@
+package seedu.typists.command.commands;
+
+import seedu.typists.common.exception.IncompleteCommandException;
+import seedu.typists.common.exception.InvalidCommandException;
+import seedu.typists.game.Game;
+import seedu.typists.game.TimeLimitGame;
+
+import java.util.ArrayList;
+
+import static seedu.typists.Main.LINE_LENGTH;
+import static seedu.typists.Main.content;
+import static seedu.typists.common.Utils.isValidTime;
+
+public class TimeGameCommand extends GameCommand {
+ private static final String TIME_SIGNIFIER = "-time";
+
+ @Override
+ public Game createGame(ArrayList args, boolean isReady, boolean setContent) {
+ if (setContent) {
+ content.setContent();
+ }
+ try {
+ int timeInSeconds = getNumber(args, TIME_SIGNIFIER);
+ return new TimeLimitGame(content.getContent(), LINE_LENGTH, timeInSeconds, isReady);
+ } catch (NumberFormatException | IncompleteCommandException | InvalidCommandException e) {
+ return new TimeLimitGame(content.getContent(), LINE_LENGTH, isReady);
+ }
+ }
+
+ @Override
+ public int getNumber(ArrayList args, String gameType)
+ throws InvalidCommandException, IncompleteCommandException {
+ int n = super.getNumber(args, gameType);
+ return isValidTime(n);
+ }
+}
diff --git a/src/main/java/seedu/typists/command/commands/WordGameCommand.java b/src/main/java/seedu/typists/command/commands/WordGameCommand.java
new file mode 100644
index 0000000000..becc0d2c69
--- /dev/null
+++ b/src/main/java/seedu/typists/command/commands/WordGameCommand.java
@@ -0,0 +1,33 @@
+package seedu.typists.command.commands;
+
+import seedu.typists.common.exception.IncompleteCommandException;
+import seedu.typists.common.exception.InvalidCommandException;
+import seedu.typists.game.Game;
+import seedu.typists.game.WordLimitGame;
+
+import java.util.ArrayList;
+
+import static seedu.typists.Main.content;
+
+public class WordGameCommand extends GameCommand {
+ private static final String WORD_SIGNIFIER = "-word";
+
+ @Override
+ public Game createGame(ArrayList args, boolean isReady, boolean setContent) {
+ //ui.printKeyBoard
+ if (setContent) {
+ content.setContent();
+ }
+ try {
+ int wordLimit = getNumber(args, WORD_SIGNIFIER);
+ return new WordLimitGame(content.getContent(), 5, wordLimit, isReady);
+ } catch (InvalidCommandException e) {
+ //won't come here.
+ } catch (NumberFormatException | IncompleteCommandException e) {
+ //constructor without limit
+ return new WordLimitGame(content.getContent(), 5, isReady);
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/seedu/typists/common/Messages.java b/src/main/java/seedu/typists/common/Messages.java
new file mode 100644
index 0000000000..7d64b3f817
--- /dev/null
+++ b/src/main/java/seedu/typists/common/Messages.java
@@ -0,0 +1,111 @@
+package seedu.typists.common;
+
+/**
+ * Container for user visible messages.
+ */
+public class Messages {
+ public static final String LOGO =
+ " ______ _ __\n"
+ + " /_ __/_ ______ (_)____/ /_\n"
+ + " / / / / / / __ \\/ / ___/ __/\n"
+ + " / / / /_/ / /_/ / (__ ) /_\n"
+ + "/_/ \\__, / .___/_/____/\\__/\n"
+ + " /____/_/";
+
+ public static final String SUMMARY =
+ " _____ __ ____ _____ ______ ______ __\n"
+ + " / ___// / / / |/ / |/ / | / __ \\ \\/ /\n"
+ + " \\__ \\/ / / / /|_/ / /|_/ / /| | / /_/ /\\ / \n"
+ + " ___/ / /_/ / / / / / / / ___ |/ _, _/ / / \n"
+ + "/____/\\____/_/ /_/_/ /_/_/ |_/_/ |_| /_/";
+
+ public static final String HISTORY =
+ " __ _____________________ ______ __\n"
+ + " / / / / _/ ___/_ __/ __ \\/ __ \\ \\/ /\n"
+ + " / /_/ // / \\__ \\ / / / / / / /_/ /\\ / \n"
+ + " / __ // / ___/ // / / /_/ / _, _/ / / \n"
+ + "/_/ /_/___//____//_/ \\____/_/ |_| /_/";
+
+ public static final String CLEAR_RECORDS =
+ " ________ _________ ____ ____ ________________ ____ ____ _____\n"
+ + " / ____/ / / ____/ | / __ \\ / __ \\/ ____/ ____/ __ \\/ __ \\/ __ \\/ ___/\n"
+ + " / / / / / __/ / /| | / /_/ / / /_/ / __/ / / / / / / /_/ / / / /\\__ \\ \n"
+ + "/ /___/ /___/ /___/ ___ |/ _, _/ / _, _/ /___/ /___/ /_/ / _, _/ /_/ /___/ / \n"
+ + "\\____/_____/_____/_/ |_/_/ |_| /_/ |_/_____/\\____/\\____/_/ |_/_____//____/ \n";
+
+
+ public static final String KEYBOARD =
+ ",---,---,---,---,---,---,---,---,---,---,---,---,---,-------,\n"
+ + "|1/2| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | + | ' | <- |\n"
+ + "|---'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-----|\n"
+ + "| ->| | Q | W | E | R | T | Y | U | I | O | P | ] | ^ | |\n"
+ + "|-----',--',--',--',--',--',--',--',--',--',--',--',--'| |\n"
+ + "| Caps | A | S | D | F | G | H | J | K | L | \\ | [ | * | |\n"
+ + "|----,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'---'----|\n"
+ + "| | < | Z | X | C | V | B | N | M | , | . | - | |\n"
+ + "|----'-,-',--'--,'---'---'---'---'---'---'-,-'---',--,------|\n"
+ + "| ctrl | | alt | |altgr | | ctrl |\n"
+ + "'------' '-----'--------------------------'------' '------'\n";
+
+ public static final String LETTER =
+ "(\\ \n"
+ + "\\'\\ \n"
+ + " \\'\\ __________ \n"
+ + " / '| ()_________)\n"
+ + " \\ '/ \\ ~~~~~~~~ \\\n"
+ + " \\ \\ ~~~~~~ \\\n"
+ + " ==). \\__________\\\n"
+ + " (__) ()__________)";
+
+ public static final String CLOCK =
+ ".'`~~~~~~~~~~~`'.\n"
+ + "( .'11 12 1'. )\n"
+ + "| :10 \\| 2: |\n"
+ + "| :9 @ 3: |\n"
+ + "| :8 4; |\n"
+ + "'. '..7 6 5..' .'\n"
+ + " ~-------------~ ";
+
+ public static final String MESSAGE_WELCOME = "Welcome to Typist -- the ultimate cli typing game.";
+ public static final String MESSAGE_ACKNOWLEDGE = "Brought to you by -- AY2122S1-CS2113-T13-4.";
+ public static final String MESSAGE_MAN = "man: check man page to get started!";
+ public static final String MESSAGE_HELP = "Manual:\n"
+ + "content: set the content\n"
+ + "game -time: start a new time game\n"
+ + "game -word: start a new word game\n"
+ + "history -g GAME_MODE [-n NUMBER_OF_RECORDS]: view past game records\n"
+ + "clear [-g GAME_MODE]: clear all game records\n"
+ + "man [COMMAND]: view the man page for a the command\n"
+ + "bye: exit typist";
+
+ public static final String MESSAGE_HELP_HISTORY = "View past game records.\n"
+ + "Command format: 'history -g GAME_MODE [-n NUMBER_OF_RECORDS]'\n"
+ + "Input \"history -h\" for complete detail";
+
+ public static final String MESSAGE_HELP_GAME = "Start a typing game.\n"
+ + "Command format: game -GAME_MODE [GAME_LIMIT] [-c] [-sn]\n"
+ + "GAME_MODE:\n"
+ + " time: time-limited (aim to type as much as possible in a limited time)\n"
+ + " word: word-limited (aim to type a chosen number of words as fast as possible)\n"
+ + "[GAME_LIMIT]: number of words/time you want the game to run.\n"
+ + "[-c]: override current content \n"
+ + "[-sn]: start the timer immediately (once all conditionals are specified by user)";
+
+ public static final String MESSAGE_HELP_CONTENT = "Set the typing content before the game.\n"
+ + "Command format: content 1-3";
+
+ public static final String MESSAGE_HELP_CLEAR = "Clear all past game records.\n"
+ + "Command format: clear [-g GAME_MODE] [-h]";
+
+ public static String MESSAGE_TIME_GAME_END = "Timer's UP!";
+
+ //This sample text is gotten from https://www.lipsum.com/
+ public static final String SAMPLE_TEXT =
+ "Lorem Ipsum is simply dummy text of the printing and typesetting industry. "
+ + "Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, "
+ + "when an unknown printer took a galley of type and scrambled it to make a type specimen book. "
+ + "It has survived not only five centuries, but also the leap into electronic typesetting, "
+ + "remaining essentially unchanged. It was popularised in the 1960s with the release of "
+ + "Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing"
+ + " software like Aldus PageMaker including versions of Lorem Ipsum.";
+}
diff --git a/src/main/java/seedu/typists/common/StringParser.java b/src/main/java/seedu/typists/common/StringParser.java
new file mode 100644
index 0000000000..16f7228e26
--- /dev/null
+++ b/src/main/java/seedu/typists/common/StringParser.java
@@ -0,0 +1,28 @@
+package seedu.typists.common;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import seedu.typists.common.exception.InvalidStringInputException;
+
+public class StringParser {
+
+ /**
+ * Splits the string based on the separator provided as parameter.
+ *
+ * @param s the string to be split
+ * @param separator a string representing the separator that you want to split the string on
+ * @return ArrayList of strings
+ */
+ public static ArrayList splitString(String s, String separator) throws InvalidStringInputException {
+
+ if (s == null) {
+ throw new InvalidStringInputException();
+ }
+ ArrayList stringParts = new ArrayList<>();
+ stringParts.addAll(Arrays.asList(s.split(separator)));
+
+ return stringParts;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/seedu/typists/common/Utils.java b/src/main/java/seedu/typists/common/Utils.java
new file mode 100644
index 0000000000..b23a04cdc9
--- /dev/null
+++ b/src/main/java/seedu/typists/common/Utils.java
@@ -0,0 +1,114 @@
+package seedu.typists.common;
+
+import seedu.typists.common.exception.ExceedRangeException;
+import seedu.typists.common.exception.InvalidCommandException;
+import seedu.typists.ui.TextUi;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utility methods.
+ */
+public class Utils {
+
+ /** convert the line of text into an arraylist of word. */
+ public static ArrayList splitStringIntoWordList(String line) {
+ return new ArrayList<>(Arrays.asList(line.split(" ")));
+ }
+
+ /** group the word list into groups of x, where x is the number of words per line. */
+ public static ArrayList getWordLines(String text, int lineLength) {
+ ArrayList wordList = splitStringIntoWordList(text);
+ ArrayList wordLines = new ArrayList<>();
+ int j = 0;
+ String[] line = new String[lineLength];
+ for (String w : wordList) {
+ if (j == lineLength - 1) {
+ line[j] = w;
+ wordLines.add(line.clone());
+ j = 0;
+ } else {
+ line[j] = w;
+ j++;
+ }
+ }
+ return wordLines;
+ }
+
+ /**
+ * get one line that is supposed to be displayed at one time of game.
+ *
+ * @param wordLists content in the form of arraylist of words
+ * @param wordsPerLine number of words to be displayed
+ * @param row current row in the content
+ **/
+ public static String[] getDisplayLines(ArrayList wordLists, int wordsPerLine, int row)
+ throws ExceedRangeException {
+ int startIndex = (row - 1) * wordsPerLine;
+ assert startIndex >= 0 : "word index should be non-negative";
+ String[] line = new String[wordsPerLine];
+
+ try {
+ for (int i = 0; i < wordsPerLine; i++) {
+ line[i] = wordLists.get(startIndex + i);
+ }
+ } catch (IndexOutOfBoundsException e) {
+ throw new ExceedRangeException();
+ }
+ return line;
+ }
+
+ /** same as getDisplayLines but added remove null feature. **/
+ public static String[] getDisplayLinesWithoutNull(ArrayList wordLists, int wordsPerLine, int row)
+ throws ExceedRangeException {
+ int startIndex = (row - 1) * wordsPerLine;
+ assert startIndex >= 0 : "word index should be non-negative";
+ String[] line = new String[wordsPerLine];
+
+ try {
+ for (int i = 0; i < wordsPerLine; i++) {
+ if (startIndex + i > wordLists.size() - 1) {
+ break;
+ }
+ line[i] = wordLists.get(startIndex + i);
+ }
+ } catch (IndexOutOfBoundsException e) {
+ throw new ExceedRangeException();
+ }
+
+ //remove null elements
+ line = Arrays.stream(line)
+ .filter(s -> (s != null && s.length() > 0))
+ .toArray(String[]::new);
+ return line;
+ }
+
+ /** same function as getWordLines, but the param is ArrayList not string. */
+ public static ArrayList getWordLineFromStringArray(List stringList) {
+ ArrayList wordLines = new ArrayList<>();
+ for (String s : stringList) {
+ String[] wordLine = s.split(" ");
+ wordLines.add(wordLine);
+ }
+ return wordLines;
+ }
+
+ /**
+ * Return a valid duration of time mode game.
+ * @param n integer that specify the duration
+ * @return n only if n is valid
+ * @throws InvalidCommandException thrown when the n is not a valid game duration
+ */
+ public static int isValidTime(int n) throws InvalidCommandException {
+ if (n < 0) {
+ new TextUi().printScreen("Duration should not be negative");
+ throw new InvalidCommandException();
+ } else if (n % 30 != 0 || n == 0) {
+ new TextUi().printScreen("Duration should be in multiple of 30 seconds.");
+ throw new InvalidCommandException();
+ }
+ return n;
+ }
+}
diff --git a/src/main/java/seedu/typists/common/exception/ExceedRangeException.java b/src/main/java/seedu/typists/common/exception/ExceedRangeException.java
new file mode 100644
index 0000000000..17c9cf2809
--- /dev/null
+++ b/src/main/java/seedu/typists/common/exception/ExceedRangeException.java
@@ -0,0 +1,4 @@
+package seedu.typists.common.exception;
+
+public class ExceedRangeException extends Throwable {
+}
diff --git a/src/main/java/seedu/typists/common/exception/FaultyInputException.java b/src/main/java/seedu/typists/common/exception/FaultyInputException.java
new file mode 100644
index 0000000000..91bfc7a32d
--- /dev/null
+++ b/src/main/java/seedu/typists/common/exception/FaultyInputException.java
@@ -0,0 +1,7 @@
+package seedu.typists.common.exception;
+
+public class FaultyInputException extends Exception {
+ public FaultyInputException(String errorMessage) {
+ super(errorMessage);
+ }
+}
diff --git a/src/main/java/seedu/typists/common/exception/IncompleteCommandException.java b/src/main/java/seedu/typists/common/exception/IncompleteCommandException.java
new file mode 100644
index 0000000000..9a47ea69b7
--- /dev/null
+++ b/src/main/java/seedu/typists/common/exception/IncompleteCommandException.java
@@ -0,0 +1,5 @@
+package seedu.typists.common.exception;
+
+public class IncompleteCommandException extends Exception {
+ //thrown when the command enter is missing key pointns
+}
diff --git a/src/main/java/seedu/typists/common/exception/InvalidArticleException.java b/src/main/java/seedu/typists/common/exception/InvalidArticleException.java
new file mode 100644
index 0000000000..a18316ecf2
--- /dev/null
+++ b/src/main/java/seedu/typists/common/exception/InvalidArticleException.java
@@ -0,0 +1,8 @@
+package seedu.typists.common.exception;
+
+public class InvalidArticleException extends Exception {
+ @Override
+ public String getMessage() {
+ return "Invalid article given";
+ }
+}
diff --git a/src/main/java/seedu/typists/common/exception/InvalidCommandException.java b/src/main/java/seedu/typists/common/exception/InvalidCommandException.java
new file mode 100644
index 0000000000..5b851d86ab
--- /dev/null
+++ b/src/main/java/seedu/typists/common/exception/InvalidCommandException.java
@@ -0,0 +1,11 @@
+package seedu.typists.common.exception;
+
+public class InvalidCommandException extends Exception {
+ public InvalidCommandException(String errorMessage) {
+ super(errorMessage);
+ }
+
+ public InvalidCommandException() {
+ super();
+ }
+}
diff --git a/src/main/java/seedu/typists/common/exception/InvalidStringInputException.java b/src/main/java/seedu/typists/common/exception/InvalidStringInputException.java
new file mode 100644
index 0000000000..9b476fef86
--- /dev/null
+++ b/src/main/java/seedu/typists/common/exception/InvalidStringInputException.java
@@ -0,0 +1,8 @@
+package seedu.typists.common.exception;
+
+public class InvalidStringInputException extends Exception {
+ @Override
+ public String getMessage() {
+ return "Invalid input given" + super.getMessage();
+ }
+}
diff --git a/src/main/java/seedu/typists/content/Animation.java b/src/main/java/seedu/typists/content/Animation.java
new file mode 100644
index 0000000000..632277a4e7
--- /dev/null
+++ b/src/main/java/seedu/typists/content/Animation.java
@@ -0,0 +1,79 @@
+package seedu.typists.content;
+
+public class Animation {
+ private String lastLine = "";
+
+ public void print(String line) {
+ //clear the last line if longer
+ if (lastLine.length() > line.length()) {
+ String temp = "";
+ for (int i = 0; i < lastLine.length(); i++) {
+ temp += " ";
+ }
+ if (temp.length() > 1) {
+ System.out.print("\r" + temp);
+ }
+ }
+ System.out.print("\r" + line);
+ lastLine = line;
+ }
+
+ private byte animLeft;
+ private byte animRight;
+
+ public void resetAnimLeft() {
+ animLeft = 1;
+ }
+
+ public void resetAnimRight() {
+ animRight = 1;
+ }
+
+ public void animateLeft(String s) {
+ switch (animLeft) {
+ case 1:
+ print(" | " + s);
+ break;
+ case 2:
+ print(" | " + s);
+ break;
+ case 3:
+ print(" | " + s);
+ break;
+ case 4:
+ print(" | " + s);
+ break;
+ case 5:
+ print(" | " + s);
+ break;
+ default:
+ animLeft = 0;
+ print("| " + s);
+ }
+ animLeft++;
+ }
+
+ public void animateRight(String s) {
+ switch (animRight) {
+ case 1:
+ print("| " + s);
+ break;
+ case 2:
+ print(" | " + s);
+ break;
+ case 3:
+ print(" | " + s);
+ break;
+ case 4:
+ print(" | " + s);
+ break;
+ case 5:
+ print(" | " + s);
+ break;
+ default:
+ animRight = 0;
+ print(" | " + s);
+ }
+ animRight++;
+ }
+}
diff --git a/src/main/java/seedu/typists/content/Books.java b/src/main/java/seedu/typists/content/Books.java
new file mode 100644
index 0000000000..30689d1998
--- /dev/null
+++ b/src/main/java/seedu/typists/content/Books.java
@@ -0,0 +1,252 @@
+package seedu.typists.content;
+
+public class Books {
+ private static String[] bookContent = {
+ "A green hunting "
+ + "cap squeezed the top of the fleshy "
+ + "balloon of a head. The green earflaps, "
+ + "full of large ears and uncut hair and the fine "
+ + "bristles that grew in the ears themselves, "
+ + "stuck out on either side like turn signals "
+ + "indicating two directions at once. "
+ + "Full, pursed lips protruded beneath the bushy black "
+ + "moustache and, at their corners, sank into little folds "
+ + "filled with disapproval and potato chip crumbs. "
+ + "In the shadow under the green visor of the cap Ignatius J. "
+ + "Reilly’s supercilious blue and yellow eyes looked down "
+ + "upon the other people waiting under the clock at the D.H. "
+ + "Holmes department store, studying the crowd of "
+ + "people for signs of bad taste in dress. "
+ + "Several of the outfits, Ignatius noticed, "
+ + "were new enough and expensive enough to be properly "
+ + "considered offenses against taste and decency. "
+ + "Possession of anything new or expensive only reflected a "
+ + "person’s lack of theology and geometry;"
+ + "it could even cast doubts upon one’s soul.",
+ "Call me Ishmael. "
+ + "Some years ago - never mind how long precisely - "
+ + "having little or no money in my purse, "
+ + "and nothing particular to interest me on shore, "
+ + "I thought I would sail about "
+ + "a little and see the watery part of the world. It is a way "
+ + "I have of driving off the spleen, "
+ + "and regulating the circulation. "
+ + "Whenever I find myself growing grim about the mouth; "
+ + "whenever it is a damp, drizzly November in my soul; "
+ + "whenever I find myself involuntarily pausing before coffin "
+ + "warehouses, and bringing up the rear of every funeral I meet; "
+ + "and especially whenever my hypos get such an upper hand of me, "
+ + "that it requires a strong moral principle to prevent me from "
+ + "deliberately stepping into the street, "
+ + "and methodically knocking people’s hats off - then, "
+ + "I account it high time to get to sea as soon as I can. "
+ + "This is my substitute for pistol and ball. "
+ + "With a philosophical flourish Cato throws himself "
+ + "upon his sword; I quietly take to the ship. "
+ + "There is nothing surprising in this. If they but knew it, "
+ + "almost all men in their degree, some time or other, "
+ + "cherish very nearly the same "
+ + "feelings towards the ocean with me.",
+ "In the late summer of that year we lived in "
+ + "a house in a village that looked across the river "
+ + "and the plain to the mountains. "
+ + "In the bed of the river there were pebbles and boulders, "
+ + "dry and white in the sun, and the water was clear and swiftly "
+ + "moving and blue in the channels. "
+ + "Troops went by the house and down the road and "
+ + "the dust they raised powdered the leaves of the trees. "
+ + "The trunks of the trees too were dusty and the leaves fell "
+ + "early that year and we saw the troops marching along the "
+ + "road and the dust rising and leaves, stirred by the breeze, "
+ + "falling and the soldiers marching and "
+ + "afterward the road bare and white except for the leaves.",
+ "It was the best of times, "
+ + "it was the worst of times, "
+ + "it was the age of wisdom, it was the age of foolishness, "
+ + "it was the epoch of belief, it was the epoch of incredulity, "
+ + "it was the season of Light, it was the season of Darkness, "
+ + "it was the spring of hope, it was the winter of despair, "
+ + "we had everything before us, we had nothing before us, "
+ + "we were all going direct to Heaven, "
+ + "we were all going direct the other way - "
+ + "in short, the period was so far like the present period, "
+ + "that some of its noisiest authorities insisted on its being "
+ + "received, for good or for evil, "
+ + "in the superlative degree of comparison only.",
+ "I first met Dean not long "
+ + "after my wife and I split up. "
+ + "I had just gotten over a serious illness that "
+ + "I won’t bother to talk about, except that "
+ + "it had something to do with the miserably weary "
+ + "split-up and my feeling that everything was dead. "
+ + "With the coming of Dean Moriarty began the part "
+ + "of my life you could call my life on the road. "
+ + "Before that I’d often dreamed of going West to see the "
+ + "country, always vaguely planning and never taking off. "
+ + "Dean is the perfect guy for the road because he "
+ + "actually was born on the road, "
+ + "when his parents were passing through Salt Lake City in 1926, "
+ + "in a jalopy, on their way to Los Angeles. "
+ + "First reports of him came to me through Chad King, "
+ + "who’d shown me a few letters from him written in a "
+ + "New Mexico reform school. I was tremendously interested "
+ + "in the letters because they so naively and sweetly asked "
+ + "Chad to teach him all about Nietzsche and all the wonderful "
+ + "intellectual things that Chad knew. At one point Carlo and "
+ + "I talked about the letters and wondered if we would ever meet "
+ + "the strange Dean Moriarty. This is all far back, "
+ + "when Dean was not the way he is today, when he was a "
+ + "young jailkid shrouded in mystery. Then news came that Dean "
+ + "was out of reform school and was coming to New York for the "
+ + "first time; also there was talk that he had just "
+ + "married a girl called Marylou.",
+ "Hale knew, before he had been in Brighton "
+ + "three hours, that they meant to murder him. "
+ + "With his inky fingers and his bitten nails, "
+ + "his manner cynical and nervous, "
+ + "anybody could tell he didn’t belong - "
+ + "belong to the early summer sun, the cool "
+ + "Whitsun wind off the sea, the holiday crowd. "
+ + "They came in by train from Victoria every five minutes, "
+ + "rocked down Queen’s Road standing on the "
+ + "tops of the little local trams, stepped off "
+ + "in bewildered multitudes into fresh and "
+ + "glittering air: the new silver paint sparkled on the piers, "
+ + "the cream houses ran away into the west like a "
+ + "pale Victorian water-colour; a race in miniature motors, "
+ + "a band playing, flower gardens in bloom below the "
+ + "front, an aeroplane advertising something for "
+ + "the health in pale vanishing clouds across the sky.",
+ "It must have been around a quarter to eleven. "
+ + "A sailor came in and ordered a chile dog and coffee. "
+ + "I sliced a bun, jerked a frank out of the boiling water, "
+ + "nested it, poured a half-dipper of chile over the frank "
+ + "and sprinkled it liberally with chopped onions. "
+ + "I scribbled a check and put it by his plate. "
+ + "I wouldn’t have recommended the unpalatable mess "
+ + "to a starving animal. The sailor was the only customer, "
+ + "and after he ate his dog he left. "
+ + "That was the exact moment she entered. "
+ + "A small woman, hardly more than five feet. "
+ + "She had the figure of a teenage girl. "
+ + "Her suit was a blue tweed, smartly cut, "
+ + "and over her thin shoulders she wore a fur jacket, "
+ + "bolero length. Tiny gold circular earrings "
+ + "clung to her small pierced ears. "
+ + "Her hands and feet were small, "
+ + "and when she seated herself at "
+ + "the counter I noticed she wasn’t wearing any rings.",
+ "It is a truth universally acknowledged, "
+ + "that a single man in possession of a good fortune must be in "
+ + "want of a wife. However little known the feelings or "
+ + "views of such a man may be on his first entering a "
+ + "neighbourhood, this truth is so well fixed in the minds of the "
+ + "surrounding families, that he is considered as the rightful "
+ + "property of some one or other of their daughters.",
+ "Dark spruce forest frowned on either side of the "
+ + "frozen waterway. The trees had been stripped by a "
+ + "recent wind of their white covering of frost, "
+ + "and they seemed to lean toward each other, "
+ + "black and ominous, in the fading light. "
+ + "A vast silence reigned over the land. "
+ + "The land itself was a desolation, lifeless, "
+ + "without movement, so lone and cold that the spirit of "
+ + "it was not even that of sadness. "
+ + "There was a hint in it of laughter, "
+ + "but of a laughter more terrible than any sadness - "
+ + "a laughter that was mirthless as the smile of the Sphinx, "
+ + "a laughter cold as the frost and partaking of the "
+ + "grimness of infallibility. "
+ + "It was the masterful and incommunicable "
+ + "wisdom of eternity laughing at the futility "
+ + "of life and the effort of life. "
+ + "It was the Wild, the savage, frozen-hearted Northland Wild.",
+ "ABANDON ALL HOPE YE WHO ENTER HERE is "
+ + "scrawled in blood red lettering on "
+ + "the side of the Chemical Bank near the corner of "
+ + "Eleventh and First and is in print large enough to be seen "
+ + "from the backseat of the cab as it lurches forward in "
+ + "the traffic leaving Wall Street and just as Timothy "
+ + "Price notices the words a bus pulls up, "
+ + "the advertisement for Les Miserables on its side blocking "
+ + "his view, but Price who is with Pierce & Pierce "
+ + "and twenty-six doesn't seem to care because he "
+ + "tells the driver he will give him five dollars to turn "
+ + "up the radio, \"Be My Baby\" on WYNN, "
+ + "and the driver, black, not American, does so.",
+ "I am a sick man. ... "
+ + "I am a spiteful man. I am an unattractive man. "
+ + "I believe my liver is diseased. However, "
+ + "I know nothing at all about my disease, "
+ + "and do not know for certain what ails me. "
+ + "I don't consult a doctor for it, and never have, "
+ + "though I have a respect for medicine and doctors. "
+ + "Besides, I am extremely superstitious, "
+ + "sufficiently so to respect medicine, anyway "
+ + "(I am well-educated enough not to be superstitious, "
+ + "but I am superstitious). No, "
+ + "I refuse to consult a doctor from spite. "
+ + "That you probably will not understand. "
+ + "Well, I understand it, though. Of course, "
+ + "I can't explain who it is precisely that "
+ + "I am mortifying in this case by my spite: "
+ + "I am perfectly well aware that I cannot \"pay "
+ + "out\" the doctors by not consulting them; "
+ + "I know better than anyone that by all this "
+ + "I am only injuring myself and no one else. "
+ + "But still, if I don't consult a doctor it is from spite. "
+ + "My liver is bad, well then let it hurt even worse!",
+ "No live organism can continue "
+ + "for long to exist sanely under conditions of absolute "
+ + "reality; even larks and katydids are supposed, by some, "
+ + "to dream. Hill House, not sane, stood by itself against "
+ + "its hills, holding darkness within; it had stood so for "
+ + "eighty years and might stand for eighty more. "
+ + "Within, walls continued upright, bricks met neatly, "
+ + "floors were firm, and doors were sensibly shut; "
+ + "silence lay steadily against the wood and "
+ + "stone of Hill House, and whatever walked there, walked alone...",
+ "As Gregor Samsa awoke one "
+ + "morning from uneasy dreams he found himself transformed "
+ + "in his bed into a gigantic insect. "
+ + "He was lying on his hard, as it were armor-plated, "
+ + "back and when he lifted his head a little he "
+ + "could see his dome-like brown belly divided into "
+ + "stiff arched segments on top of which the "
+ + "bed quilt could hardly keep in position and "
+ + "was about to slide off completely. "
+ + "His numerous legs, which were pitifully thin "
+ + "compared to the rest of his bulk, "
+ + "waved helplessly before his eyes.",
+ "I am an invisible man. "
+ + "No, I am not a spook like those who haunted Edgar Allan Poe; "
+ + "nor am I one of your Hollywood-movie ectoplasms. "
+ + "I am a man of substance, of flesh and bone, fiber "
+ + "and liquids-and I might even be said to possess a mind. "
+ + "I am invisible, understand, simply because people "
+ + "refuse to see me. Like the bodiless heads you see "
+ + "sometimes in circus sideshows, it is as though "
+ + "I have been surrounded by mirrors of hard, distorting glass. "
+ + "When they approach me they see only my surroundings, "
+ + "themselves, or figments of their imagination-"
+ + "indeed, everything and anything except me.",
+ "You don't know "
+ + "about me without you have read a book by the "
+ + "name of The Adventures of Tom Sawyer; "
+ + "but that ain't no matter. That book was made "
+ + "by Mr. Mark Twain, and he told the truth, mainly. "
+ + "There was things which he stretched, "
+ + "but mainly he told the truth. "
+ + "That is nothing. I never seen anybody "
+ + "but lied one time or another, without "
+ + "it was Aunt Polly, or the widow, or "
+ + "maybe Mary. Aunt Polly - Tom's Aunt Polly, "
+ + "she is - and Mary, and the Widow Douglas is "
+ + "all told about in that book, which is mostly a "
+ + "true book, with some stretchers, as I said before."
+ };
+
+ public static String getBook(int index) {
+ return bookContent[index];
+ }
+}
diff --git a/src/main/java/seedu/typists/content/Content.java b/src/main/java/seedu/typists/content/Content.java
new file mode 100644
index 0000000000..9fcd4c8f4d
--- /dev/null
+++ b/src/main/java/seedu/typists/content/Content.java
@@ -0,0 +1,141 @@
+package seedu.typists.content;
+
+import seedu.typists.common.exception.InvalidArticleException;
+import seedu.typists.ui.TextUi;
+
+import java.util.Random;
+import java.util.Scanner;
+import java.util.InputMismatchException;
+import static seedu.typists.common.Messages.SAMPLE_TEXT;
+
+public class Content {
+ private String content = SAMPLE_TEXT;
+ private final TextUi ui;
+
+ public Content() {
+ this.ui = new TextUi();
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent() {
+ ui.printLetter();
+ ui.printLine("Content selection (input 0 to go back):");
+ ui.printLine("1. Opening of famous books");
+ ui.printLine("2. Wikipedia article");
+ ui.printLine("3. Random sentence of custom length");
+
+ Scanner in = new Scanner(System.in);
+ int command = -1;
+ do {
+ ui.printLine("Enter your selection: ");
+ try {
+ command = in.nextInt();
+ } catch (InputMismatchException e) {
+ ui.printLine("Invalid selection.");
+ in.nextLine();
+ }
+ } while (command < 0 || command > 3);
+ switch (command) {
+ case 1:
+ setBooks();
+ break;
+ case 2:
+ setWikipedia();
+ break;
+ case 3:
+ setRandomContent();
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void setBooks() {
+ Scanner in = new Scanner(System.in);
+ ui.printBookSelection();
+ int selection = -1;
+ do {
+ ui.printLine("Enter your selection:");
+ try {
+ selection = in.nextInt();
+ } catch (InputMismatchException e) {
+ ui.printLine("Not an integer.");
+ in.nextLine();
+ }
+ } while (selection < 0 || selection > 15);
+ if (selection == 0) {
+ setContent();
+ } else {
+ this.content = Books.getBook(selection - 1);
+ assert this.content.length() > 0;
+ try {
+ ui.viewAnimateLeft("Content set");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void setWikipedia() {
+ Scanner in = new Scanner(System.in);
+ WikiImport wi = new WikiImport();
+ String title;
+ boolean articleNotFound = true;
+ do {
+ ui.printLine("Enter the article name (input 0 to go back):");
+ title = in.nextLine();
+ if (title.equals("0")) {
+ articleNotFound = false;
+ setContent();
+ } else {
+ try {
+ String temp = wi.getArticle(title);
+ articleNotFound = false;
+ this.content = temp;
+ assert this.content.length() > 0;
+ try {
+ ui.viewAnimateLeft("Content set");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ } catch (InvalidArticleException e) {
+ ui.printLine(e.getMessage());
+ }
+ }
+ } while (articleNotFound);
+ }
+
+ private void setRandomContent() {
+ Scanner in = new Scanner(System.in);
+ RandomGenerator rg = new RandomGenerator();
+ int length = -1;
+ do {
+ ui.printLine("Enter the word count (between 5 - 100, input 0 to go back):");
+ try {
+ length = in.nextInt();
+ } catch (InputMismatchException e) {
+ ui.printLine("Invalid selection.");
+ in.nextLine();
+ }
+ } while (length < 0 || length > 100);
+ if (length < 5) {
+ setContent();
+ } else {
+ String s = "";
+ Random r = new Random();
+ for (int i = 0; i < length; i++) {
+ s += (rg.randomString(r.nextInt(10) + 5) + " ");
+ }
+ this.content = s;
+ assert this.content.length() > 0;
+ try {
+ ui.viewAnimateLeft("Content set");
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/src/main/java/seedu/typists/content/RandomGenerator.java b/src/main/java/seedu/typists/content/RandomGenerator.java
new file mode 100644
index 0000000000..29cf49b823
--- /dev/null
+++ b/src/main/java/seedu/typists/content/RandomGenerator.java
@@ -0,0 +1,22 @@
+package seedu.typists.content;
+
+import java.util.Random;
+
+/**
+ * Generate random string of custom length.
+ */
+public class RandomGenerator {
+ public String randomString(int length) {
+ Random isCaps = new Random();
+ Random rand = new Random();
+ StringBuilder s = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ if (isCaps.nextBoolean()) {
+ s.append((char) (rand.nextInt(26) + 'A'));
+ } else {
+ s.append((char) (rand.nextInt(26) + 'a'));
+ }
+ }
+ return s.toString();
+ }
+}
diff --git a/src/main/java/seedu/typists/content/WikiImport.java b/src/main/java/seedu/typists/content/WikiImport.java
new file mode 100644
index 0000000000..f85bcc4f8d
--- /dev/null
+++ b/src/main/java/seedu/typists/content/WikiImport.java
@@ -0,0 +1,22 @@
+package seedu.typists.content;
+
+import org.fastily.jwiki.core.Wiki;
+import seedu.typists.common.exception.InvalidArticleException;
+
+/**
+ * Imports the first paragraph of a text from Wikipedia.
+ */
+public class WikiImport {
+ public String getArticle(String article) throws InvalidArticleException {
+ Wiki wiki = new Wiki.Builder().build();
+ if (!wiki.exists(article)) {
+ throw new InvalidArticleException();
+ }
+ String temp = wiki.getTextExtract(article);
+ if (temp.length() < 1) {
+ throw new InvalidArticleException();
+ }
+ assert temp.length() > 1;
+ return temp;
+ }
+}
diff --git a/src/main/java/seedu/typists/game/Game.java b/src/main/java/seedu/typists/game/Game.java
new file mode 100644
index 0000000000..c04cd0120f
--- /dev/null
+++ b/src/main/java/seedu/typists/game/Game.java
@@ -0,0 +1,65 @@
+package seedu.typists.game;
+
+import seedu.typists.ui.GameUi;
+import seedu.typists.ui.SummaryUi;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public abstract class Game {
+ protected final GameUi ui;
+ protected boolean isReady;
+ protected int wordsPerLine;
+ protected int limit;
+
+ public ArrayList displayedLines = new ArrayList<>();
+ public ArrayList userLines = new ArrayList<>();
+ public double gameTime;
+
+ public Game(int wordsPerLine, boolean isReady) {
+ this.ui = new GameUi();
+ this.isReady = isReady;
+ this.wordsPerLine = wordsPerLine;
+ }
+
+ public Game(int wordsPerLine, int limit, boolean isReady) {
+ this(wordsPerLine, isReady);
+ this.limit = limit;
+ }
+
+ public void run() {
+ if (!isReady) {
+ ui.readyToStartTimer();
+ }
+ runGame();
+ }
+
+ public long getTimeNow() {
+ return System.currentTimeMillis();
+ }
+
+ public double getDuration(long startTime, long endTime) {
+ return (double) (endTime - startTime) / 1000;
+ }
+
+ public abstract void displayLines(int row);
+
+ public abstract void runGame();
+
+ public abstract void gameSummary();
+
+ public HashMap handleSummary(
+ ArrayList expectedInput, ArrayList actualInput, double timeElapsed, String gameMode
+ ) {
+ HashMap summary = SummaryManager.generateSummary(
+ expectedInput, actualInput, timeElapsed, gameMode
+ );
+ SummaryUi.displaySummary(summary);
+ return summary;
+ }
+
+ public void handleStorage(HashMap summary) {
+ GameRecordsManager gameRecordsManager = GameRecordsManager.getGameRecordsManager();
+ gameRecordsManager.addGameRecord(summary);
+ }
+}
diff --git a/src/main/java/seedu/typists/game/GameRecord.java b/src/main/java/seedu/typists/game/GameRecord.java
new file mode 100644
index 0000000000..43c16e948b
--- /dev/null
+++ b/src/main/java/seedu/typists/game/GameRecord.java
@@ -0,0 +1,100 @@
+package seedu.typists.game;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+
+public class GameRecord {
+
+ private String gameMode;
+ private double timeElapsed;
+ private int errorWordCount;
+ private int correctWordCount;
+ private int totalWordCount;
+ private double errorWordPercentage;
+ private double correctWordPercentage;
+ private double wpm;
+ private ArrayList errorWords;
+
+ public GameRecord(
+ String gameMode, double timeElapsed, int errorWordCount, int correctWordCount, int totalWordCount,
+ double errorWordPercentage, double correctWordPercentage, double wpm, ArrayList errorWords
+ ) {
+
+ this.gameMode = gameMode;
+ this.timeElapsed = timeElapsed;
+ this.errorWordCount = errorWordCount;
+ this.correctWordCount = correctWordCount;
+ this.totalWordCount = totalWordCount;
+ this.errorWordPercentage = errorWordPercentage;
+ this.correctWordPercentage = correctWordPercentage;
+ this.wpm = wpm;
+ this.errorWords = errorWords;
+ }
+
+ public String getGameMode() {
+
+ return gameMode;
+ }
+
+
+ public double getTimeElapsed() {
+ return timeElapsed;
+ }
+
+ public int getErrorWordCount() {
+
+ return errorWordCount;
+ }
+
+ public int getCorrectWordCount() {
+
+ return correctWordCount;
+ }
+
+ public int getTotalWordCount() {
+
+ return totalWordCount;
+ }
+
+ public double getErrorWordPercentage() {
+
+ return errorWordPercentage;
+ }
+
+ public double getCorrectWordPercentage() {
+
+ return correctWordPercentage;
+ }
+
+ public double getWpm() {
+
+ return wpm;
+ }
+
+ public ArrayList getErrorWords() {
+ return errorWords;
+ }
+
+ public String getStringFormat() {
+ String separator = "|";
+ String formatted = gameMode
+ + separator
+ + timeElapsed
+ + separator
+ + errorWordCount
+ + separator
+ + correctWordCount
+ + separator
+ + totalWordCount
+ + separator
+ + errorWordPercentage
+ + separator
+ + correctWordPercentage
+ + separator
+ + wpm
+ + separator
+ + errorWords.toString();
+ return formatted;
+
+ }
+}
diff --git a/src/main/java/seedu/typists/game/GameRecordsManager.java b/src/main/java/seedu/typists/game/GameRecordsManager.java
new file mode 100644
index 0000000000..a1cfc6e70a
--- /dev/null
+++ b/src/main/java/seedu/typists/game/GameRecordsManager.java
@@ -0,0 +1,121 @@
+package seedu.typists.game;
+
+import seedu.typists.storage.Storage;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class GameRecordsManager {
+
+ private static GameRecordsManager instance = null;
+ private ArrayList timeLimitedGameRecords;
+ private ArrayList wordLimitedGameRecords;
+
+ private GameRecordsManager() {
+ // get gameRecords using some file reader.
+ timeLimitedGameRecords = Storage.readGameRecords("Time-limited");
+ wordLimitedGameRecords = Storage.readGameRecords("Word-limited");
+ }
+
+ public void updateGameRecords() {
+ Storage.writeGameRecords(timeLimitedGameRecords, "Time-limited");
+ Storage.writeGameRecords(wordLimitedGameRecords, "Word-limited");
+ }
+
+ public static GameRecordsManager getGameRecordsManager() {
+ if (instance == null) {
+ instance = new GameRecordsManager();
+ }
+ return instance;
+ }
+
+ public void addGameRecord(HashMap gameSummary) {
+ GameRecord newGameRecord = createGameRecord(gameSummary);
+ addRecordToArray(newGameRecord);
+ writeRecordToFile(newGameRecord);
+ }
+
+ private GameRecord createGameRecord(HashMap gameSummary) {
+ String gameMode = (String) gameSummary.get("gameMode");
+ double timeElapsed = (Double) gameSummary.get("timeElapsed");
+ double wpm = (Double) gameSummary.get("wordsPerMinute");
+ int errorWordCount = (Integer) gameSummary.get("errorWordCount");
+ int correctWordCount = (Integer) gameSummary.get("correctWordCount");
+ int totalWordCount = (Integer) gameSummary.get("totalWordCount");
+ double errorWordPercentage = (Double) gameSummary.get("errorWordPercentage");
+ double correctWordPercentage = (Double) gameSummary.get("correctWordPercentage");
+ ArrayList errorWords = (ArrayList) gameSummary.get("errorWords");
+
+ GameRecord newGameRecord = new GameRecord(
+ gameMode, timeElapsed, errorWordCount, correctWordCount, totalWordCount,
+ errorWordPercentage, correctWordPercentage, wpm, errorWords
+ );
+ return newGameRecord;
+ }
+
+ private void addRecordToArray(GameRecord gameRecord) {
+ String gameMode = gameRecord.getGameMode();
+ if (gameMode.equals("Word-limited")) {
+ wordLimitedGameRecords.add(gameRecord);
+ } else {
+ timeLimitedGameRecords.add(gameRecord);
+ }
+ }
+
+
+ public ArrayList getGameRecords(String gameMode, int numberOfRecords) {
+
+ assert ((gameMode.equals("Time-limited")) || (gameMode.equals("Word-limited")));
+ if (gameMode.equals("Time-limited")) {
+ if (numberOfRecords > timeLimitedGameRecords.size()) {
+ return null;
+ }
+ return new ArrayList<>(timeLimitedGameRecords.subList(
+ timeLimitedGameRecords.size() - numberOfRecords,
+ timeLimitedGameRecords.size()
+ ));
+ } else {
+ if (numberOfRecords > wordLimitedGameRecords.size()) {
+ return null;
+ }
+ return new ArrayList<>(wordLimitedGameRecords.subList(
+ wordLimitedGameRecords.size() - numberOfRecords,
+ wordLimitedGameRecords.size()
+ ));
+ }
+ }
+
+ private void writeRecordToFile(GameRecord gameRecord) {
+ String gameMode = gameRecord.getGameMode();
+ if (gameMode.equals("Word-limited")) {
+ Storage.writeGameRecords(wordLimitedGameRecords, "Word-limited");
+ } else {
+ Storage.writeGameRecords(timeLimitedGameRecords, "Time-limited");
+
+ }
+ }
+
+ public int getNumberOfGameRecords(String gameMode) {
+ if (gameMode.equals("Word-limited")) {
+ return wordLimitedGameRecords.size();
+ } else {
+ return timeLimitedGameRecords.size();
+ }
+ }
+
+ public void clearAllRecords(String gameMode) {
+ assert (gameMode.equals("Time-limited") || gameMode.equals("Word-limited") || gameMode.equals("all"));
+ if (gameMode.equals("Word-limited")) {
+ wordLimitedGameRecords.clear();
+ Storage.writeGameRecords(wordLimitedGameRecords, "Word-limited");
+ } else if (gameMode.equals("Time-limited")) {
+ timeLimitedGameRecords.clear();
+ Storage.writeGameRecords(timeLimitedGameRecords, "Time-limited");
+ } else {
+ wordLimitedGameRecords.clear();
+ timeLimitedGameRecords.clear();
+ Storage.writeGameRecords(wordLimitedGameRecords, "Word-limited");
+ Storage.writeGameRecords(timeLimitedGameRecords, "Time-limited");
+ }
+ }
+}
diff --git a/src/main/java/seedu/typists/game/SummaryManager.java b/src/main/java/seedu/typists/game/SummaryManager.java
new file mode 100644
index 0000000000..ea4c40aadd
--- /dev/null
+++ b/src/main/java/seedu/typists/game/SummaryManager.java
@@ -0,0 +1,141 @@
+package seedu.typists.game;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+
+
+public class SummaryManager {
+
+ public static HashMap generateSummary(
+ ArrayList expectedInput, ArrayList actualInput, double timeElapsed, String gameMode) {
+
+ assert timeElapsed >= 0;
+ assert expectedInput.size() > 0;
+ assert actualInput != null;
+ assert ((gameMode.equals("Word-limited")) || (gameMode.equals("Time-limited")));
+
+ HashMap summary = new HashMap<>();
+ summary.put("timeElapsed", timeElapsed);
+ summary.put("gameMode", gameMode);
+ summary.put("errorWordCount", calculateErrorWordCount(actualInput, expectedInput));
+ summary.put("correctWordCount", calculateCorrectWordCount(actualInput, expectedInput));
+ summary.put("totalWordCount", calculateTotalWordCount(expectedInput));
+ summary.put("errorWordPercentage", calculateErrorWordsPercentage(actualInput, expectedInput));
+ summary.put("correctWordPercentage", calculateCorrectWordsPercentage(actualInput, expectedInput));
+ summary.put("wordsPerMinute", calculateWpm(timeElapsed, actualInput, expectedInput));
+ summary.put("errorWords", getErrorWords(actualInput, expectedInput));
+ return summary;
+ }
+
+ private static ArrayList getErrorWords(
+ ArrayList actualInput, ArrayList expectedInput) {
+
+ ArrayList errorWords = new ArrayList<>();
+
+ if (actualInput.isEmpty()) {
+ ArrayList mergedExpectedInput = mergeArrays(expectedInput);
+ return mergedExpectedInput;
+ }
+
+ int totalExpectedLines = expectedInput.size();
+ int outerLoopCount;
+ for (outerLoopCount = 0; outerLoopCount < totalExpectedLines; outerLoopCount++) {
+
+ if (actualInput.size() == outerLoopCount) {
+ ArrayList remainingLines = new ArrayList<>(
+ expectedInput.subList(outerLoopCount, totalExpectedLines)
+ );
+ ArrayList mergedRemainingLines = mergeArrays(remainingLines);
+ errorWords.addAll(mergedRemainingLines);
+ break;
+ }
+
+ ArrayList expectedLine = new ArrayList<>(Arrays.asList(expectedInput.get(outerLoopCount)));
+ assert expectedLine.size() > 0;
+ ArrayList actualLine = new ArrayList<>(Arrays.asList(actualInput.get(outerLoopCount)));
+
+ int innerLoopCount = 0;
+ for (String expectedWord : expectedLine) {
+
+ if (actualLine.isEmpty()) {
+ ArrayList remainingWords = new ArrayList<>(
+ expectedLine.subList(innerLoopCount, expectedLine.size())
+ );
+ errorWords.addAll(remainingWords);
+ break;
+ }
+
+ int result = actualLine.indexOf(expectedWord);
+
+ if (result == -1) {
+ String errorWord = expectedWord;
+ errorWords.add(errorWord);
+ } else {
+ actualLine.remove(result);
+ }
+
+ innerLoopCount++;
+ }
+
+ }
+ return errorWords;
+ }
+
+ private static int calculateCorrectWordCount(ArrayList actualInput, ArrayList expectedInput) {
+ int errorWordCount = calculateErrorWordCount(actualInput, expectedInput);
+ assert errorWordCount >= 0;
+ int totalWordCount = calculateTotalWordCount(expectedInput);
+ assert totalWordCount >= errorWordCount;
+ int correctWordCount = totalWordCount - errorWordCount;
+ return correctWordCount;
+
+ }
+
+ private static Double calculateCorrectWordsPercentage(
+ ArrayList actualInput, ArrayList expectedInput) {
+ int correctWordCount = calculateCorrectWordCount(actualInput, expectedInput);
+ int totalWordCount = calculateTotalWordCount(expectedInput);
+ double correctWordsPercentage = ((double) correctWordCount / (double) totalWordCount) * 100;
+ return correctWordsPercentage;
+ }
+
+ private static Double calculateWpm(
+ Double timeElapsed, ArrayList actualInput, ArrayList expectedInput) {
+ int correctWordCount = calculateCorrectWordCount(actualInput, expectedInput);
+ double wordPerMinute = ((double) correctWordCount / timeElapsed) * 60;
+ return wordPerMinute;
+ }
+
+
+ private static int calculateErrorWordCount(ArrayList actualInput, ArrayList expectedInput) {
+ int errorWordCount = getErrorWords(actualInput, expectedInput).size();
+ return errorWordCount;
+ }
+
+ private static Double calculateErrorWordsPercentage(
+ ArrayList actualInput, ArrayList expectedInput) {
+ int errorWordCount = calculateErrorWordCount(actualInput, expectedInput);
+ int totalWordCount = calculateTotalWordCount(expectedInput);
+ double errorWordsPercentage = ((double) errorWordCount / (double) totalWordCount) * 100;
+ return errorWordsPercentage;
+ }
+
+ private static int calculateTotalWordCount(ArrayList expectedInput) {
+ ArrayList mergedExpectedInput = mergeArrays(expectedInput);
+ int totalWordCount = mergedExpectedInput.size();
+ return totalWordCount;
+ }
+
+ private static ArrayList mergeArrays(ArrayList arrayToMerge) {
+ ArrayList mergedArr = new ArrayList<>();
+
+ for (String[] array : arrayToMerge) {
+ ArrayList tempArr = new ArrayList<>(Arrays.asList(array));
+ mergedArr.addAll(tempArr);
+ }
+
+ return mergedArr;
+ }
+
+}
diff --git a/src/main/java/seedu/typists/game/TimeLimitGame.java b/src/main/java/seedu/typists/game/TimeLimitGame.java
new file mode 100644
index 0000000000..62aa825c3c
--- /dev/null
+++ b/src/main/java/seedu/typists/game/TimeLimitGame.java
@@ -0,0 +1,105 @@
+package seedu.typists.game;
+
+import seedu.typists.common.exception.ExceedRangeException;
+import seedu.typists.common.exception.InvalidCommandException;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Scanner;
+
+import static seedu.typists.common.Messages.MESSAGE_TIME_GAME_END;
+import static seedu.typists.common.Utils.getDisplayLines;
+import static seedu.typists.common.Utils.splitStringIntoWordList;
+import static seedu.typists.common.Utils.getWordLineFromStringArray;
+import static seedu.typists.common.Utils.isValidTime;
+
+
+public class TimeLimitGame extends Game {
+ protected final ArrayList wordLists;
+ protected double gameTime;
+ protected int currentRow;
+
+ public TimeLimitGame(String targetWordSet, int wordsPerLine, boolean isReady) {
+ super(wordsPerLine, isReady);
+ assert targetWordSet != null;
+ this.wordLists = splitStringIntoWordList(targetWordSet);
+ this.currentRow = 1;
+ this.limit = getTimeLimit();
+ }
+
+ public TimeLimitGame(String targetWordSet, int wordsPerLine, int timeInSeconds, boolean isReady) {
+ super(wordsPerLine, timeInSeconds, isReady);
+ assert targetWordSet != null;
+ this.wordLists = splitStringIntoWordList(targetWordSet);
+ this.currentRow = 1;
+ }
+
+ public int getTimeLimit() {
+ ui.printScreen("Enter how long you want the game to run: ");
+ try {
+ int n = Integer.parseInt(ui.readCommand());
+ return isValidTime(n);
+ } catch (NumberFormatException e) {
+ ui.printScreen("Invalid Number!");
+ } catch (InvalidCommandException e) {
+ //repeat getTimeLimit
+ }
+ return getTimeLimit();
+ }
+
+ @Override
+ public void displayLines(int row) {
+ try {
+ String[] display = getDisplayLines(wordLists, wordsPerLine, currentRow);
+ ui.printLine(display);
+ displayedLines.add(display);
+ } catch (ExceedRangeException e) {
+ currentRow = 1;
+ displayLines(currentRow);
+ }
+ }
+
+ public void runGame() {
+ Scanner in = new Scanner(System.in);
+ List inputs = new ArrayList<>();
+
+ //game begins
+ long beginTime = getTimeNow();
+ boolean timeOver = false;
+
+ while (!timeOver) {
+ long currTime = getTimeNow() - beginTime;
+ if (currTime >= limit * 1000L) {
+ gameTime = (double) currTime / 1000;
+ timeOver = true;
+ } else {
+ displayLines(currentRow);
+ inputs.add(in.nextLine());
+ currentRow++;
+ }
+ }
+
+ updateUserLines(inputs);
+ endGame();
+ }
+
+ public void endGame() {
+ ui.printEnd(MESSAGE_TIME_GAME_END);
+ double overshoot = gameTime - limit;
+ assert overshoot >= 0;
+ if (overshoot > 0) {
+ ui.printOvershoot(overshoot);
+ }
+ }
+
+ public void gameSummary() {
+ HashMap summary = handleSummary(displayedLines, userLines, gameTime, "Time-limited");
+ handleStorage(summary);
+ }
+
+ public void updateUserLines(List stringArray) {
+ userLines = getWordLineFromStringArray(stringArray);
+ }
+}
+
diff --git a/src/main/java/seedu/typists/game/WordLimitGame.java b/src/main/java/seedu/typists/game/WordLimitGame.java
new file mode 100644
index 0000000000..1408fdddd0
--- /dev/null
+++ b/src/main/java/seedu/typists/game/WordLimitGame.java
@@ -0,0 +1,118 @@
+package seedu.typists.game;
+
+import seedu.typists.common.exception.ExceedRangeException;
+import seedu.typists.common.exception.InvalidCommandException;
+import seedu.typists.common.exception.InvalidStringInputException;
+import seedu.typists.ui.TextUi;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import static seedu.typists.common.Utils.getDisplayLinesWithoutNull;
+import static seedu.typists.common.Utils.getWordLineFromStringArray;
+import static seedu.typists.common.StringParser.splitString;
+
+public class WordLimitGame extends Game {
+ private ArrayList eachWord;
+ protected ArrayList wordLines;
+ private final String content1;
+ private long beginTime;
+ private String[] displayed;
+
+ public WordLimitGame(String targetWordSet, int wordsPerLine, boolean isReady) {
+ super(wordsPerLine, isReady);
+ this.eachWord = new ArrayList<>(100);
+ this.content1 = targetWordSet;
+ this.limit = getWordLimit();
+ }
+
+ public WordLimitGame(String targetWordSet, int wordsPerLine, int wordLimit, boolean isReady) {
+ super(wordsPerLine, wordLimit, isReady);
+ this.eachWord = new ArrayList<>(100);
+ this.content1 = targetWordSet;
+ }
+
+ @Override
+ public void displayLines(int row) {
+ displayed = new String[0];
+ try {
+ displayed = getDisplayLinesWithoutNull(eachWord,wordsPerLine,row);
+ } catch (ExceedRangeException e) {
+ e.printStackTrace();
+ }
+ ui.printLine(displayed);
+ }
+
+ public int getWordLimit() {
+ ui.printScreen("Enter how many words you want the game to run: ");
+ try {
+ int n = Integer.parseInt(ui.readCommand());
+ return isValidWord(n);
+ } catch (NumberFormatException e) {
+ new TextUi().printScreen("Length should be a number!");
+ //repeat getWordLimit
+ } catch (InvalidCommandException e) {
+ new TextUi().printScreen(e.getMessage());
+ //repeat getWordLimit
+ }
+ return getWordLimit();
+ }
+
+ public static int isValidWord(int n) throws InvalidCommandException {
+ if (n <= 0) {
+ throw new InvalidCommandException("Length should be positive!");
+ }
+ return n;
+ }
+
+
+ public void trimContent(int wordLimit) {
+ try {
+ eachWord = splitString(content1, " ");
+ } catch (InvalidStringInputException e) {
+ e.printStackTrace();
+ }
+ eachWord = new ArrayList<>(eachWord.subList(0, wordLimit));
+ }
+
+ public void runGame() {
+ assert limit > 0 : "limit should be greater than 0";
+ trimContent(limit);
+ beginTime = getTimeNow();
+ List inputs = new ArrayList<>();
+ int row = 0;// for method: getDisplayLines()
+ boolean isExit = false;
+ while (!isExit) {
+ row++;
+ assert row > 0 : "row is always a positive integer.";
+ //display a single line
+ displayLines(row);
+ //read user input
+ String fullCommand = ui.readCommand();
+ if (fullCommand.equals("Exit")) {
+ break;
+ }
+ //only add the line into displayedLines when the Command is not Exit
+ displayedLines.add(displayed);
+
+ //update for summary
+ inputs.add(fullCommand);
+ updateUserLines(inputs);
+
+ if ((wordsPerLine * (row)) > eachWord.size()) {
+ isExit = true;
+ }
+ }
+ }
+
+ public void gameSummary() {
+ gameTime = getDuration(beginTime, getTimeNow());
+ HashMap summary = handleSummary(displayedLines, userLines, gameTime, "Word-limited");
+ handleStorage(summary);
+ }
+
+ public void updateUserLines(List stringArray) {
+ userLines = getWordLineFromStringArray(stringArray);
+ }
+}
diff --git a/src/main/java/seedu/typists/storage/FileParser.java b/src/main/java/seedu/typists/storage/FileParser.java
new file mode 100644
index 0000000000..56d673539c
--- /dev/null
+++ b/src/main/java/seedu/typists/storage/FileParser.java
@@ -0,0 +1,119 @@
+package seedu.typists.storage;
+
+import seedu.typists.game.GameRecord;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class FileParser {
+
+ public static GameRecord convertStringToGameRecord(String gameRecordString) {
+ try {
+ String[] gameRecordStats = gameRecordString.split("\\|");
+ assert gameRecordStats.length >= 8;
+ String gameMode = getGameMode(gameRecordStats);
+ double timeElapsed = getTimeElapsed(gameRecordStats);
+ int errorWordCount = getErrorWordCount(gameRecordStats);
+ int correctWordCount = getCorrectWordCount(gameRecordStats);
+ int totalWordCount = getTotalWordCount(gameRecordStats);
+ double errorWordPercentage = getErrorWordPercentage(gameRecordStats);
+ double correctWordPercentage = getCorrectWordPercentage(gameRecordStats);
+ double wpm = getWpm(gameRecordStats);
+ ArrayList errorWords = getErrorWords(gameRecordStats);
+
+ GameRecord gameRecord = new GameRecord(
+ gameMode,
+ timeElapsed,
+ errorWordCount,
+ correctWordCount,
+ totalWordCount,
+ errorWordPercentage,
+ correctWordPercentage,
+ wpm,
+ errorWords
+ );
+ return gameRecord;
+ } catch (Exception e) {
+ System.out.println("OOPS! Looks like you messed around with a record file.\n"
+ + "A brand new record file will be created!\n");
+ return null;
+ }
+ }
+
+ private static String getGameMode(String[] gameRecordStats) throws Exception {
+ try {
+ return gameRecordStats[0];
+ } catch (Exception e) {
+ throw new Exception();
+ }
+ }
+
+ private static double getTimeElapsed(String[] gameRecordStats) throws Exception {
+ try {
+ return Double.parseDouble(gameRecordStats[1]);
+ } catch (Exception e) {
+ throw new Exception();
+ }
+
+ }
+
+ private static int getErrorWordCount(String[] errorWordCountString) throws Exception {
+ try {
+ return Integer.parseInt(errorWordCountString[2]);
+ } catch (Exception e) {
+ throw new Exception();
+ }
+ }
+
+ private static int getCorrectWordCount(String[] gameRecordStats) throws Exception {
+ try {
+ return Integer.parseInt(gameRecordStats[3]);
+ } catch (Exception e) {
+ throw new Exception();
+ }
+ }
+
+ private static int getTotalWordCount(String[] gameRecordStats) throws Exception {
+ try {
+ return Integer.parseInt(gameRecordStats[4]);
+ } catch (Exception e) {
+ throw new Exception();
+ }
+ }
+
+ private static double getErrorWordPercentage(String[] gameRecordStats) throws Exception {
+ try {
+ return Double.parseDouble(gameRecordStats[5]);
+ } catch (Exception e) {
+ throw new Exception();
+ }
+ }
+
+ private static double getCorrectWordPercentage(String[] gameRecordStats) throws Exception {
+ try {
+ return Double.parseDouble(gameRecordStats[6]);
+ } catch (Exception e) {
+ throw new Exception();
+ }
+ }
+
+ private static double getWpm(String[] wpmString) throws Exception {
+ try {
+ return Double.parseDouble(wpmString[7]);
+ } catch (Exception e) {
+ throw new Exception();
+ }
+ }
+
+ private static ArrayList getErrorWords(String[] gameRecordStats) throws Exception {
+ try {
+ if (gameRecordStats[8].trim().equals("[]")) {
+ return new ArrayList<>();
+ }
+ String[] errorWords = gameRecordStats[8].substring(1, gameRecordStats[8].length() - 1).split(",");
+ return new ArrayList<>(Arrays.asList(errorWords));
+ } catch (Exception e) {
+ throw new Exception();
+ }
+ }
+}
diff --git a/src/main/java/seedu/typists/storage/Storage.java b/src/main/java/seedu/typists/storage/Storage.java
new file mode 100644
index 0000000000..f011985b53
--- /dev/null
+++ b/src/main/java/seedu/typists/storage/Storage.java
@@ -0,0 +1,141 @@
+package seedu.typists.storage;
+
+
+import seedu.typists.game.GameRecord;
+import seedu.typists.ui.TextUi;
+
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileWriter;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Locale;
+import java.util.Scanner;
+
+import java.util.logging.Logger;
+import java.util.logging.LogManager;
+import java.util.logging.Level;
+import java.util.logging.SimpleFormatter;
+import java.util.logging.FileHandler;
+import java.util.logging.ConsoleHandler;
+
+public class Storage {
+
+ private static final Logger LOGGER = Logger.getLogger(TextUi.class.getName());
+
+ public static ArrayList readGameRecords(String gameMode) {
+ ArrayList gameRecordsStringArrayList = readFile(gameMode);
+ ArrayList gameRecords = convertToGameRecords(gameRecordsStringArrayList);
+ return gameRecords;
+ }
+
+ public static void writeGameRecords(ArrayList gameRecords, String gameMode) {
+ assert gameRecords != null;
+ ArrayList gameRecordsStringArrayList = convertToGameRecordsStringArrayList(gameRecords);
+ writeToFile(gameRecordsStringArrayList, gameMode);
+ }
+
+ private static ArrayList readFile(String gameMode) {
+ ArrayList fileLines = new ArrayList<>();
+ String filename = getFileName(gameMode);
+ try {
+ File gameRecordsFile = new File(filename);
+ Scanner fc = new Scanner(gameRecordsFile);
+ while (fc.hasNextLine()) {
+ String fileLine = fc.nextLine();
+ fileLines.add(fileLine);
+ }
+ } catch (NullPointerException e) {
+ LOGGER.log(Level.SEVERE, "Caught NullPointerException when creating file", e);
+ } catch (FileNotFoundException e) {
+ LOGGER.log(Level.INFO, "create new file with filename: " + filename);
+ }
+ return fileLines;
+ }
+
+ private static void writeToFile(ArrayList gameRecordsStringArrayList, String gameMode) {
+ String filename = getFileName(gameMode);
+ try {
+ BufferedWriter bw = new BufferedWriter(new FileWriter(filename));
+ for (String gameRecordString : gameRecordsStringArrayList) {
+ bw.write(gameRecordString);
+ bw.newLine();
+ }
+ bw.flush();
+ bw.close();
+ } catch (IOException e) {
+ LOGGER.log(Level.SEVERE, "Caught FileNotFoundException when creating fileWriter", e);
+ System.out.print("Failed to write to file.\n");
+ System.out.print("File named '" + filename + "' could not be opened for some reasons.\n"
+ + "Ensure that there are no directories named '"
+ + filename
+ + "' in the directory where the .jar file resides.\n");
+ }
+
+ }
+
+ private static ArrayList convertToGameRecords(ArrayList gameRecordsStringArrayList) {
+ ArrayList gameRecords = new ArrayList<>();
+ if (gameRecordsStringArrayList.isEmpty()) {
+ return gameRecords;
+ } else {
+ for (String gameRecordString : gameRecordsStringArrayList) {
+ GameRecord gameRecord = FileParser.convertStringToGameRecord(gameRecordString);
+ if (gameRecord == null) {
+ gameRecords = new ArrayList<>();
+ return gameRecords;
+ }
+ gameRecords.add(gameRecord);
+ }
+ return gameRecords;
+ }
+ }
+
+ private static ArrayList convertToGameRecordsStringArrayList(ArrayList gameRecords) {
+ ArrayList gameRecordsStringArrayList = new ArrayList<>();
+ if (gameRecords.isEmpty()) {
+ return gameRecordsStringArrayList;
+ }
+ for (GameRecord gameRecord : gameRecords) {
+ String gameRecordString = convertGameRecordToString(gameRecord);
+ gameRecordsStringArrayList.add(gameRecordString);
+ }
+ return gameRecordsStringArrayList;
+ }
+
+ private static String convertGameRecordToString(GameRecord gameRecord) {
+ return gameRecord.getStringFormat();
+ }
+
+
+ private static String getFileName(String gameMode) {
+ assert ((gameMode.equals("Time-limited")) || (gameMode.equals("Word-limited")));
+ String filename = gameMode.toLowerCase(Locale.ROOT) + "_records.txt";
+ return filename;
+ }
+
+ public static void setUpLog() {
+ LogManager.getLogManager().reset();
+ LOGGER.setLevel(Level.ALL);
+
+ ConsoleHandler ch = new ConsoleHandler();
+ ch.setLevel(Level.SEVERE);
+ LOGGER.addHandler(ch);
+
+ try {
+ FileHandler fh = new FileHandler(TextUi.class.getName() + ".log");
+ fh.setFormatter(new SimpleFormatter());
+ fh.setLevel(Level.FINE);
+ LOGGER.addHandler(fh);
+ } catch (IOException e) {
+ LOGGER.log(Level.SEVERE, "File logger failed to set up\n", e);
+ }
+
+ LOGGER.info("Set up log in Storage");
+ }
+
+
+}
diff --git a/src/main/java/seedu/typists/ui/ClearCommandUi.java b/src/main/java/seedu/typists/ui/ClearCommandUi.java
new file mode 100644
index 0000000000..667980eaaf
--- /dev/null
+++ b/src/main/java/seedu/typists/ui/ClearCommandUi.java
@@ -0,0 +1,24 @@
+package seedu.typists.ui;
+
+import static seedu.typists.common.Messages.CLEAR_RECORDS;
+
+public class ClearCommandUi {
+
+ public static void displayHelp() {
+ System.out.print("clear deletes all records for both games,"
+ + " or either of the game types, depending on your choice.\n");
+ System.out.print("The format of the command is 'clear [-g GAME_MODE]'\n");
+ System.out.print("The arguments in the square brackets are optional.\n"
+ + "GAME_MODE specifies which game mode's record(s) to clear.\n"
+ + "If not inputted, the default is 'all' which means "
+ + "records from both the time- and word-limited will be cleared.\n");
+ System.out.print("The possible values for GAME_MODE is 'all', 'time', and, 'word'.\n");
+ System.out.print("'word' clears all game records for the word-limited mode.\n"
+ + "'time' clears all game records for the time-limited mode.\n");
+ }
+
+ public static void displaySuccess(String gameMode) {
+ System.out.print(CLEAR_RECORDS + "\n");
+ System.out.println("Successfully cleared " + gameMode + " game records.\n");
+ }
+}
diff --git a/src/main/java/seedu/typists/ui/GameUi.java b/src/main/java/seedu/typists/ui/GameUi.java
new file mode 100644
index 0000000000..29ecc52572
--- /dev/null
+++ b/src/main/java/seedu/typists/ui/GameUi.java
@@ -0,0 +1,39 @@
+package seedu.typists.ui;
+
+import java.util.Scanner;
+
+public class GameUi extends TextUi {
+
+ public void printEnd(String endMessage) {
+ printScreen(endMessage);
+ }
+
+ public void readyToStartTimer() {
+ Scanner in = new Scanner(System.in);
+ String command = "";
+ while (!command.equals("start")) {
+ printScreen("Timer will start once you entered \"start\": ");
+ command = in.nextLine();
+ }
+ }
+
+ public void printOvershoot(double overshoot) {
+ printScreen(
+ "While on your last sentence, "
+ + "you exceeds the game by "
+ + String.format("%.2f", overshoot)
+ + " seconds."
+ );
+ }
+
+ public void printGameMode1Progress(int a, int b) throws InterruptedException {
+ viewAnimateLeft("Your progress:" + String.valueOf(a) + "/" + String.valueOf(b));
+ }
+
+ public void printResetContent(int n) {
+ printScreen(
+ "Please reset your content to more than "
+ + n + " words."
+ );
+ }
+}
diff --git a/src/main/java/seedu/typists/ui/HistoryCommandUi.java b/src/main/java/seedu/typists/ui/HistoryCommandUi.java
new file mode 100644
index 0000000000..e2b4d4c179
--- /dev/null
+++ b/src/main/java/seedu/typists/ui/HistoryCommandUi.java
@@ -0,0 +1,42 @@
+package seedu.typists.ui;
+
+import seedu.typists.game.GameRecord;
+
+import java.util.ArrayList;
+
+import static seedu.typists.common.Messages.HISTORY;
+
+public class HistoryCommandUi {
+ private static final String SUMMARY_SEPARATOR =
+ "==================================================================";
+
+ public static void displayGameRecords(ArrayList gameRecords, int numberOfGameRecords) {
+ if (gameRecords == null) {
+ System.out.print("The number of game records you requested to view is more than what you have.\n"
+ + "Number of game records you have is " + numberOfGameRecords + ".\n");
+ return;
+ }
+ System.out.print(HISTORY + "\n");
+ if (gameRecords.size() == 0) {
+ System.out.print("No records to display.\n");
+ }
+ for (GameRecord gameRecord : gameRecords) {
+ SummaryUi.displaySummary(gameRecord);
+ System.out.print(SUMMARY_SEPARATOR + "\n");
+ }
+ }
+
+ public static void displayHelp() {
+ //do after user guide
+ System.out.print("history shows the game records for your past games.\n");
+ System.out.print("Command format: 'history -g GAME_MODE [-n NUMBER_OF_RECORDS]'\n");
+ System.out.print("Arguments in the square brackets are optional.\n");
+ System.out.print("GAME_MODE specifies which game mode records you want to view.\n"
+ + "The possible values for GAME_MODE are 'time' and 'word'.\n");
+ System.out.print("NUMBER_OF_RECORDS specifies the number of past records you want to view.\n"
+ + "Only positive integers are allowed.\n"
+ + "If not inputted, the default is the total number of game records "
+ + "you have for the selected game mode.\n");
+
+ }
+}
diff --git a/src/main/java/seedu/typists/ui/SummaryUi.java b/src/main/java/seedu/typists/ui/SummaryUi.java
new file mode 100644
index 0000000000..c5c450e0af
--- /dev/null
+++ b/src/main/java/seedu/typists/ui/SummaryUi.java
@@ -0,0 +1,140 @@
+package seedu.typists.ui;
+
+import seedu.typists.game.GameRecord;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.logging.Logger;
+import java.util.logging.LogManager;
+import java.util.logging.Level;
+import java.util.logging.SimpleFormatter;
+import java.util.logging.FileHandler;
+import java.util.logging.ConsoleHandler;
+
+import static seedu.typists.common.Messages.SUMMARY;
+
+public class SummaryUi extends TextUi {
+
+ private static final Logger LOGGER = Logger.getLogger(TextUi.class.getName());
+
+ public static void displaySummary(HashMap summary) {
+ assert summary.containsKey("timeElapsed");
+ assert summary.containsKey("gameMode");
+ assert summary.containsKey("wordsPerMinute");
+ assert summary.containsKey("errorWordCount");
+ assert summary.containsKey("correctWordCount");
+ assert summary.containsKey("totalWordCount");
+ assert summary.containsKey("errorWordPercentage");
+ assert summary.containsKey("correctWordPercentage");
+ assert summary.containsKey("errorWords");
+
+
+ assert summary.get("timeElapsed") instanceof Double;
+ assert summary.get("gameMode") instanceof String;
+ assert summary.get("wordsPerMinute") instanceof Double;
+ printHeader();
+ printOverview(
+ (Double) summary.get("timeElapsed"),
+ (String) summary.get("gameMode"),
+ (Double) summary.get("wordsPerMinute"));
+ assert summary.get("errorWordCount") instanceof Integer;
+ assert summary.get("errorWordPercentage") instanceof Double;
+ printErrorStatistics(
+ (Integer) summary.get("errorWordCount"),
+ (Double) summary.get("errorWordPercentage"),
+ (Integer) summary.get("totalWordCount"));
+ assert summary.get("correctWordCount") instanceof Integer;
+ assert summary.get("correctWordPercentage") instanceof Double;
+ printSuccessStatistics(
+ (Integer) summary.get("correctWordCount"),
+ (Double) summary.get("correctWordPercentage"),
+ (Integer) summary.get("totalWordCount"));
+ assert summary.get("errorWords") instanceof List;
+ printErrorWords((ArrayList) summary.get("errorWords"));
+ }
+
+ public static void displaySummary(GameRecord gameRecord) {
+ printOverview(gameRecord.getTimeElapsed(),
+ gameRecord.getGameMode(),
+ gameRecord.getWpm());
+ printErrorStatistics(gameRecord.getErrorWordCount(),
+ gameRecord.getErrorWordPercentage(),
+ gameRecord.getTotalWordCount());
+ printSuccessStatistics(gameRecord.getCorrectWordCount(),
+ gameRecord.getCorrectWordPercentage(),
+ gameRecord.getTotalWordCount());
+ printErrorWords(gameRecord.getErrorWords());
+ }
+
+ private static void printErrorStatistics(int errorWordCount, double errorWordPercentage, int totalWordCount) {
+ System.out.print("Number of Wrong Words: "
+ + errorWordCount
+ + "/"
+ + totalWordCount
+ + "|" + String.format("%.2f", errorWordPercentage) + "%\n");
+ }
+
+ private static void printSuccessStatistics(int correctWordCount, double correctWordPercentage, int totalWordCount) {
+ System.out.print("Number of Correct Words: "
+ + correctWordCount
+ + "/"
+ + totalWordCount
+ + "|" + String.format("%.2f", correctWordPercentage) + "%\n");
+ }
+
+
+ static void printErrorWords(ArrayList errorWords) {
+ setUpLog();
+ assert errorWords != null;
+ System.out.print("Mistakes: ");
+ if (errorWords.size() == 0) {
+ System.out.print("No words typed wrongly.\n");
+ return;
+ }
+ for (int i = 0; i < errorWords.size(); i++) {
+ assert errorWords != null;
+ if (i % 8 == 0) {
+ System.out.print("\n");
+ }
+ System.out.print(errorWords.get(i).trim());
+ if (i != (errorWords.size() - 1)) {
+ System.out.print("|");
+ }
+ }
+ System.out.print("\n");
+ }
+
+ private static void printOverview(double timeElapsed, String gameMode, double wpm) {
+ System.out.print("Game Mode: " + gameMode + '\n');
+ System.out.print("WPM: " + String.format("%.2f", wpm) + '\n');
+ System.out.print("Total Time taken for the game: " + String.format("%.2f", timeElapsed) + " seconds\n");
+ }
+
+ private static void printHeader() {
+ System.out.print(SUMMARY + '\n');
+ }
+
+ public static void setUpLog() {
+ LogManager.getLogManager().reset();
+ LOGGER.setLevel(Level.ALL);
+
+ ConsoleHandler ch = new ConsoleHandler();
+ ch.setLevel(Level.SEVERE);
+ LOGGER.addHandler(ch);
+
+ try {
+ FileHandler fh = new FileHandler(TextUi.class.getName() + ".log");
+ fh.setFormatter(new SimpleFormatter());
+ fh.setLevel(Level.FINE);
+ LOGGER.addHandler(fh);
+ } catch (IOException e) {
+ LOGGER.log(Level.SEVERE, "File logger failed to set up\n", e);
+ }
+
+ LOGGER.info("Set up log in SummaryUi");
+ }
+
+
+}
diff --git a/src/main/java/seedu/typists/ui/TextUi.java b/src/main/java/seedu/typists/ui/TextUi.java
new file mode 100644
index 0000000000..e2fbd49f84
--- /dev/null
+++ b/src/main/java/seedu/typists/ui/TextUi.java
@@ -0,0 +1,122 @@
+package seedu.typists.ui;
+
+import seedu.typists.content.Animation;
+
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+
+import static java.lang.System.lineSeparator;
+import static java.lang.System.out;
+import static seedu.typists.common.Messages.CLOCK;
+import static seedu.typists.common.Messages.KEYBOARD;
+import static seedu.typists.common.Messages.LETTER;
+import static seedu.typists.common.Messages.LOGO;
+import static seedu.typists.common.Messages.MESSAGE_ACKNOWLEDGE;
+import static seedu.typists.common.Messages.MESSAGE_MAN;
+import static seedu.typists.common.Messages.MESSAGE_WELCOME;
+
+
+import java.util.Scanner;
+
+
+/**
+ * Text UI of the application.
+ */
+public class TextUi {
+ private final SimpleDateFormat timeFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ private static final String DIVIDER = "===========================================================";
+ private static final String LINE_PREFIX = " | ";
+ private static final String LS = lineSeparator();
+
+ /** Get current timestamp.
+ * Unused because it interferes with the EXPECTED.TXT in runtest
+ * @return current time in yyyy-mm-dd hh:mm:ss format
+ */
+ public String getTimeStamp() {
+ Timestamp timestamp = new Timestamp(System.currentTimeMillis());
+ return timeFormatter.format(timestamp);
+ }
+
+ //solution below adapted from https://github.com/se-edu/addressbook-level2/
+ public void showWelcomeMessage(String version) {
+ getTimeStamp();
+ printScreen(
+ version,
+ //getTimeStamp(),
+ DIVIDER,
+ LOGO,
+ MESSAGE_WELCOME,
+ MESSAGE_ACKNOWLEDGE,
+ MESSAGE_MAN,
+ DIVIDER
+ );
+ }
+
+ public void printScreen(String... message) {
+ for (String m : message) {
+ out.println(LINE_PREFIX + m.replace("\n", LS + LINE_PREFIX));
+ }
+ }
+
+ public void printLine(String... message) {
+ out.print(LINE_PREFIX);
+ for (String m : message) {
+ out.print(m + " ");
+ }
+ out.print("\n");
+ }
+
+ public String readCommand() {
+ Scanner sc = new Scanner(System.in);
+ return sc.nextLine();
+ }
+
+ public void showBye() {
+ printScreen("Thanks for using Typist! See you next time!");
+ }
+
+
+ public void printKeyboard() {
+ out.println(KEYBOARD);
+ }
+
+ public void printLetter() {
+ out.println(LETTER);
+ }
+
+ public void printClock() {
+ out.println(CLOCK);
+ }
+
+ public void printBookSelection() {
+ printScreen("Input '0' to go back.\n"
+ + "Content list:\n"
+ + "1. A Confederacy of Dunces\n"
+ + "2. Moby Dick\n"
+ + "3. A Farewell to Arms\n"
+ + "4. A Tale of Two Cities\n"
+ + "5. On the Road\n"
+ + "6. Bright on Rock\n"
+ + "7. Pick Up\n"
+ + "8. Pride and Prejudice\n"
+ + "9. White Fang\n"
+ + "10. American Psycho\n"
+ + "11. Notes from Underground\n"
+ + "12. The Haunting of Hill House\n"
+ + "13. Metamorphosis\n"
+ + "14. Invisible Man\n"
+ + "15. The Adventures of Huckleberry Finn");
+ }
+
+ public void viewAnimateLeft(String string) throws InterruptedException {
+ Animation animation = new Animation();
+ animation.resetAnimLeft();
+ int k = 0;
+ while (k < 6) {
+ animation.animateLeft(string);
+ Thread.sleep(300);
+ k++;
+ }
+ System.out.println("");
+ }
+}
diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/DukeTest.java
similarity index 90%
rename from src/test/java/seedu/duke/DukeTest.java
rename to src/test/java/seedu/DukeTest.java
index 2dda5fd651..c222fff234 100644
--- a/src/test/java/seedu/duke/DukeTest.java
+++ b/src/test/java/seedu/DukeTest.java
@@ -1,10 +1,11 @@
-package seedu.duke;
+package seedu;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
class DukeTest {
+
@Test
public void sampleTest() {
assertTrue(true);
diff --git a/src/test/java/seedu/StringParserTest.java b/src/test/java/seedu/StringParserTest.java
new file mode 100644
index 0000000000..7298fa82b1
--- /dev/null
+++ b/src/test/java/seedu/StringParserTest.java
@@ -0,0 +1,36 @@
+package seedu;
+
+import org.junit.jupiter.api.Test;
+import seedu.typists.common.exception.InvalidStringInputException;
+import seedu.typists.common.StringParser;
+
+import java.util.ArrayList;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class StringParserTest {
+
+ // methodBeingTested_inputGiven_expectedOutcome
+ // Input, one separator, 2 parts -- Happy path
+ @Test
+ void splitString_oneSeparator_expectTwoParts() throws InvalidStringInputException {
+ String inputString = "Hello|world";
+ StringParser sp = new StringParser();
+ String separator = "\\|";
+ ArrayList stringParts = sp.splitString(inputString,separator);
+ assertEquals(2,stringParts.size());
+ }
+
+ //test failure case
+ // method being tested, NullInput, expect exception
+ @Test
+ void splitString_nullInput_expectException() {
+ String inputString = null;
+ StringParser sp = new StringParser();
+ String separator = "\\|";
+ //ArrayList stringParts = sp.splitString(inputString,separator);
+ assertThrows(InvalidStringInputException.class, () -> sp.splitString(inputString,separator));
+ //assertEquals(0,stringParts.size());
+ }
+}
diff --git a/src/test/java/seedu/TimeLimitGameTest.java b/src/test/java/seedu/TimeLimitGameTest.java
new file mode 100644
index 0000000000..daf0517436
--- /dev/null
+++ b/src/test/java/seedu/TimeLimitGameTest.java
@@ -0,0 +1,17 @@
+package seedu;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TimeLimitGameTest {
+ private static final String TEXT_TXT = "this is test text";
+ //name: whatIsBeingTested_descriptionOfTestInputs_expectedOutcome
+
+ @Test
+ public void wordLines_sampleTxt_correct() {
+ //TimeLimitGame g = new TimeLimitGame(TEXT_TXT, 15);
+ //should get an error cause the text is not enough words
+ //need update
+ }
+}
diff --git a/src/test/java/seedu/UtilsTest.java b/src/test/java/seedu/UtilsTest.java
new file mode 100644
index 0000000000..8a36b0f1f3
--- /dev/null
+++ b/src/test/java/seedu/UtilsTest.java
@@ -0,0 +1,54 @@
+package seedu;
+
+import org.junit.jupiter.api.Test;
+import seedu.typists.common.exception.ExceedRangeException;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static seedu.typists.common.Utils.splitStringIntoWordList;
+import static seedu.typists.common.Utils.getDisplayLinesWithoutNull;
+
+public class UtilsTest {
+ private static final String SAMPLE = "this is test text here";
+ private static final ArrayList places = new ArrayList<>(Arrays.asList(
+ "this", "is", "test", "text", "here","this1", "is1", "test1", "text1", "here1"));
+
+ //name: whatIsBeingTested_descriptionOfTestInputs_expectedOutcome
+
+ @Test
+ public void splitStringIntoWordList_sampleTxt_success() {
+ ArrayList result = new ArrayList<>(Arrays.asList("this", "is", "test", "text", "here"));
+ assertEquals(result, splitStringIntoWordList(SAMPLE));
+ assertEquals(result.size(), 5);
+ }
+
+ @Test
+ public void getDisplayLines_inRange_success() throws ExceedRangeException {
+ String[] displayed = getDisplayLinesWithoutNull(places, 3, 3);
+ String[] result = { "is1", "test1", "text1"};
+ for (int i = 0; i < displayed.length; i++) {
+ assertEquals(displayed[i], result[i]);
+ }
+ }
+
+ @Test
+ public void getDisplayLines_outRange_success() throws ExceedRangeException {
+ String[] displayed = getDisplayLinesWithoutNull(places, 6, 2);
+ String[] result = { "is1", "test1", "text1", "here1"};
+ for (int i = 0; i < displayed.length; i++) {
+ assertEquals(displayed[i], result[i]);
+ }
+ }
+ // this test is commented out
+ // because it will fail due to array address allocation
+ // but the result strings are identical
+ // @Test
+ //public void getWordLine_sampleTxt_success() {
+ // String[] a = new String[]{"this", "is"};
+ // String[] b = new String[]{"test", "text"};
+ // ArrayList result = new ArrayList<>(Arrays.asList(a,b));
+ // assertEquals(result, getWordLine(SAMPLE, 2));
+ // }
+}
diff --git a/src/test/java/seedu/WikiImportTest.java b/src/test/java/seedu/WikiImportTest.java
new file mode 100644
index 0000000000..5683fc91dc
--- /dev/null
+++ b/src/test/java/seedu/WikiImportTest.java
@@ -0,0 +1,30 @@
+package seedu;
+
+import org.junit.jupiter.api.Test;
+import seedu.typists.content.WikiImport;
+import seedu.typists.common.exception.InvalidArticleException;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class WikiImportTest {
+
+ // methodBeingTested_inputGiven_expectedOutcome
+ // Input, GitHub, relevant article (longer than 0)
+ @Test
+ void getArticle_GitHub_expectArticle() throws InvalidArticleException {
+ String article = "GitHub";
+ WikiImport wiki = new WikiImport();
+ String content = wiki.getArticle(article);
+ assertTrue(content.length() > 0);
+ }
+
+ // test failure case
+ // method being tested, Githob, expect exception
+ @Test
+ void getArticle_Githob_expectException() {
+ String article = "Githob";
+ WikiImport wiki = new WikiImport();
+ assertThrows(InvalidArticleException.class, () -> wiki.getArticle(article));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/seedu/typists/game/SummaryManagerTest.java b/src/test/java/seedu/typists/game/SummaryManagerTest.java
new file mode 100644
index 0000000000..8053083fac
--- /dev/null
+++ b/src/test/java/seedu/typists/game/SummaryManagerTest.java
@@ -0,0 +1,97 @@
+package seedu.typists.game;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class SummaryManagerTest {
+
+ @Test
+ void generateSummary_0ErrorWords() {
+ ArrayList expectedInput = new ArrayList<>();
+ expectedInput.add(new String[]{"one", "two", "three", "four"});
+ expectedInput.add(new String[]{"five", "six", "seven", "eight", "nine", "ten"});
+ int totalWordCount = expectedInput.get(0).length + expectedInput.get(1).length;
+ int correctWordCount = totalWordCount;
+ int errorWordCount = 0;
+ ArrayList errorWords = new ArrayList<>();
+ ArrayList actualInput = new ArrayList<>();
+ actualInput.addAll(expectedInput);
+ double timeElapsed = 10.9021;
+ String gameMode = "Time-limited";
+ HashMap actual = SummaryManager.generateSummary(
+ expectedInput, actualInput, timeElapsed, gameMode
+ );
+ HashMap expected = initializeExpectedOutput(
+ timeElapsed, gameMode, errorWordCount, correctWordCount, totalWordCount, errorWords
+ );
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ void generateSummary_oneLineAllErrorWords() {
+ ArrayList expectedInput = new ArrayList<>();
+ expectedInput.add(new String[]{"one", "two", "three", "four"});
+ expectedInput.add(new String[]{"five", "six", "seven", "eight", "nine", "ten"});
+ ArrayList actualInput = new ArrayList<>();
+ actualInput.add(expectedInput.get(0));
+ actualInput.add(expectedInput.get(0));
+ double timeElapsed = 10.9021;
+ String gameMode = "Time-limited";
+ int totalWordCount = expectedInput.get(0).length + expectedInput.get(1).length;
+ int errorWordCount = expectedInput.get(1).length;
+ int correctWordCount = totalWordCount - errorWordCount;
+ ArrayList errorWords = new ArrayList<>(Arrays.asList(expectedInput.get(1)));
+ HashMap actual = SummaryManager.generateSummary(
+ expectedInput, actualInput, timeElapsed, gameMode
+ );
+ HashMap expected = initializeExpectedOutput(
+ timeElapsed, gameMode, errorWordCount, correctWordCount, totalWordCount, errorWords
+ );
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ void generateSummary_noUserInput() {
+ ArrayList expectedInput = new ArrayList<>();
+ expectedInput.add(new String[]{"one", "two", "three", "four"});
+ expectedInput.add(new String[]{"five", "six", "seven", "eight", "nine", "ten"});
+ ArrayList actualInput = new ArrayList<>();
+ double timeElapsed = 10.9021;
+ String gameMode = "Time-limited";
+ int totalWordCount = expectedInput.get(0).length + expectedInput.get(1).length;
+ int errorWordCount = totalWordCount;
+ int correctWordCount = 0;
+ ArrayList errorWords = new ArrayList<>(Arrays.asList(expectedInput.get(0)));
+ errorWords.addAll(Arrays.asList(expectedInput.get(1)));
+ HashMap actual = SummaryManager.generateSummary(
+ expectedInput, actualInput, timeElapsed, gameMode
+ );
+ HashMap expected = initializeExpectedOutput(
+ timeElapsed, gameMode, errorWordCount, correctWordCount, totalWordCount, errorWords
+ );
+ assertEquals(expected, actual);
+
+ }
+
+ HashMap initializeExpectedOutput(
+ double timeElapsed, String gameMode, int errorWordCount,
+ int correctWordCount, int totalWordCount, ArrayList errorWords
+ ) {
+ HashMap expected = new HashMap<>();
+ expected.put("timeElapsed", timeElapsed);
+ expected.put("gameMode", gameMode);
+ expected.put("wordsPerMinute", (correctWordCount / timeElapsed) * 60);
+ expected.put("errorWordCount", errorWordCount);
+ expected.put("correctWordCount", correctWordCount);
+ expected.put("totalWordCount", totalWordCount);
+ expected.put("errorWordPercentage", ((double) errorWordCount / totalWordCount) * 100);
+ expected.put("correctWordPercentage", ((double) correctWordCount / totalWordCount) * 100);
+ expected.put("errorWords", errorWords);
+ return expected;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/seedu/typists/ui/SummaryUiTest.java b/src/test/java/seedu/typists/ui/SummaryUiTest.java
new file mode 100644
index 0000000000..a186fdd60e
--- /dev/null
+++ b/src/test/java/seedu/typists/ui/SummaryUiTest.java
@@ -0,0 +1,143 @@
+package seedu.typists.ui;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static seedu.typists.common.Messages.SUMMARY;
+
+class SummaryUiTest {
+
+ //This code (lines 15 to 38) was referenced from https://www.baeldung.com/java-testing-system-out-println
+ private final PrintStream standardOut = System.out;
+ private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream();
+
+ @BeforeEach
+ public void setUp() {
+ System.setOut(new PrintStream(outputStreamCaptor));
+ }
+
+ @Test
+ void displaySummary_0ErrorWords() {
+ double timeElapsed = 10.03221;
+ String gameMode = "Word-limited";
+ int errorWordCount = 0;
+ int correctWordCount = 10;
+ int totalWordCount = 0;
+ ArrayList errorWords = new ArrayList<>();
+ HashMap summary = initializeSummary(
+ timeElapsed, gameMode, errorWordCount, correctWordCount, totalWordCount, errorWords
+ );
+ String expected = SUMMARY + '\n'
+ + "Game Mode: " + gameMode + '\n'
+ + "WPM: " + String.format("%.2f", (Double) summary.get("wordsPerMinute")) + "\n"
+ + "Total Time taken for the game: " + String.format("%.2f", timeElapsed) + " seconds\n"
+ + "Number of Wrong Words: " + errorWordCount + "/" + totalWordCount
+ + "|" + String.format("%.2f", summary.get("errorWordPercentage")) + "%\n"
+ + "Number of Correct Words: "
+ + correctWordCount
+ + "/"
+ + totalWordCount
+ + "|" + String.format("%.2f", summary.get("correctWordPercentage")) + "%\n"
+ + "Mistakes: No words typed wrongly.\n";
+
+ SummaryUi.displaySummary(summary);
+ String actual = outputStreamCaptor.toString();
+ assertEquals(expected, actual);
+
+ }
+
+ @Test
+ void displaySummary_noUserInput() {
+ double timeElapsed = 10.03221;
+ String gameMode = "Word-limited";
+ int errorWordCount = 10;
+ int correctWordCount = 0;
+ int totalWordCount = 10;
+ ArrayList errorWords = new ArrayList<>(
+ Arrays.asList(
+ new String[]{"one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"})
+ );
+ HashMap summary = initializeSummary(
+ timeElapsed, gameMode, errorWordCount, correctWordCount, totalWordCount, errorWords
+ );
+ String expected = SUMMARY + '\n'
+ + "Game Mode: " + gameMode + '\n'
+ + "WPM: " + String.format("%.2f", (Double) summary.get("wordsPerMinute")) + "\n"
+ + "Total Time taken for the game: " + String.format("%.2f", timeElapsed) + " seconds\n"
+ + "Number of Wrong Words: " + errorWordCount + "/" + totalWordCount
+ + "|" + String.format("%.2f", summary.get("errorWordPercentage")) + "%\n"
+ + "Number of Correct Words: "
+ + correctWordCount
+ + "/"
+ + totalWordCount
+ + "|" + String.format("%.2f", summary.get("correctWordPercentage")) + "%\n"
+ + "Mistakes: \n"
+ + "one|two|three|four|five|six|seven|eight|\n"
+ + "nine|ten\n";
+
+ SummaryUi.displaySummary(summary);
+ String actual = outputStreamCaptor.toString();
+ assertEquals(expected, actual);
+ }
+
+
+ @Test
+ void printErrorWords_multipleOfEight() {
+ ArrayList errorWords = new ArrayList<>(Arrays.asList(
+ "foo1", "foo2", "foo3", "foo4", "foo5", "foo6", "foo7", "foo8",
+ "bar1", "bar2", "bar3", "bar4", "bar5", "bar6", "bar7", "bar8"
+ ));
+ String expected = "Mistakes: \n"
+ + "foo1|foo2|foo3|foo4|foo5|foo6|foo7|foo8|\n"
+ + "bar1|bar2|bar3|bar4|bar5|bar6|bar7|bar8\n";
+ SummaryUi.printErrorWords(errorWords);
+ String actual = outputStreamCaptor.toString();
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ void printErrorWords_notMultipleOfEight() {
+ ArrayList errorWords = new ArrayList<>(Arrays.asList(
+ "foo1", "foo2", "foo3", "foo4", "foo5", "foo6", "foo7", "foo8",
+ "bar1", "bar2", "bar3", "bar4", "bar5", "bar6", "bar7"
+ ));
+ String expected = "Mistakes: \n"
+ + "foo1|foo2|foo3|foo4|foo5|foo6|foo7|foo8|\n"
+ + "bar1|bar2|bar3|bar4|bar5|bar6|bar7\n";
+ SummaryUi.printErrorWords(errorWords);
+ String actual = outputStreamCaptor.toString();
+ assertEquals(expected, actual);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ System.setOut(standardOut);
+ }
+
+ HashMap initializeSummary(
+ double timeElapsed, String gameMode, int errorWordCount,
+ int correctWordCount, int totalWordCount, ArrayList errorWords
+ ) {
+ HashMap expected = new HashMap<>();
+ expected.put("timeElapsed", timeElapsed);
+ expected.put("gameMode", gameMode);
+ expected.put("wordsPerMinute", (correctWordCount / timeElapsed) * 60);
+ expected.put("errorWordCount", errorWordCount);
+ expected.put("correctWordCount", correctWordCount);
+ expected.put("totalWordCount", totalWordCount);
+ expected.put("errorWordPercentage", ((double) errorWordCount / totalWordCount) * 100);
+ expected.put("correctWordPercentage", ((double) correctWordCount / totalWordCount) * 100);
+ expected.put("errorWords", errorWords);
+ return expected;
+
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/seedu/typists/ui/TextUiTest.java b/src/test/java/seedu/typists/ui/TextUiTest.java
new file mode 100644
index 0000000000..7909b54573
--- /dev/null
+++ b/src/test/java/seedu/typists/ui/TextUiTest.java
@@ -0,0 +1,83 @@
+package seedu.typists.ui;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static seedu.typists.common.Messages.SUMMARY;
+
+class TextUiTest {
+ //This code (lines 15 to 38) was referenced from https://www.baeldung.com/java-testing-system-out-println
+ /*private final PrintStream standardOut = System.out;
+ private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream();
+
+ @BeforeEach
+ public void setUp() {
+ System.setOut(new PrintStream(outputStreamCaptor));
+ }
+
+ @Test
+ void showSummary() {
+ TextUi textUi = new TextUi();
+ String expected = SUMMARY + '\n'
+ + "Number of Wrong Words: 5/10\n"
+ + "Error Percentage of Wrong Words: 50.00%\n"
+ + "Wrong Words:\n"
+ + "foo|bar\n"
+ + "WPM: 40.08\n"
+ + "Total Time taken for the game: 10.07 seconds\n";
+
+ textUi.showSummary(5, 50.00000, java.util.Arrays.asList("foo", "bar"), 40.08, 10, 10.07);
+ String actual = outputStreamCaptor.toString();
+ assertEquals(expected, actual);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ System.setOut(standardOut);
+ }
+
+ @Test
+ void printErrorWords_noErrorWords_noExceptionThrown() {
+ List errorWords = null;
+ String expected = "No words typed wrongly.\n";
+ TextUi textUi = new TextUi();
+ textUi.printErrorWords(errorWords);
+ String actual = outputStreamCaptor.toString();
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ void printErrorWords_multipleOfEight() {
+ List errorWords = Arrays.asList(
+ "foo1", "foo2", "foo3", "foo4", "foo5", "foo6", "foo7", "foo8",
+ "bar1", "bar2", "bar3", "bar4", "bar5", "bar6", "bar7", "bar8"
+ );
+ String expected = "foo1|foo2|foo3|foo4|foo5|foo6|foo7|foo8|\n"
+ + "bar1|bar2|bar3|bar4|bar5|bar6|bar7|bar8\n";
+ TextUi textUi = new TextUi();
+ textUi.printErrorWords(errorWords);
+ String actual = outputStreamCaptor.toString();
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ void printErrorWords_notMultipleOfEight() {
+ List errorWords = Arrays.asList(
+ "foo1", "foo2", "foo3", "foo4", "foo5", "foo6", "foo7", "foo8",
+ "bar1", "bar2", "bar3", "bar4", "bar5", "bar6", "bar7"
+ );
+ String expected = "foo1|foo2|foo3|foo4|foo5|foo6|foo7|foo8|\n"
+ + "bar1|bar2|bar3|bar4|bar5|bar6|bar7\n";
+ TextUi textUi = new TextUi();
+ textUi.printErrorWords(errorWords);
+ String actual = outputStreamCaptor.toString();
+ assertEquals(expected, actual);
+ }*/
+}
\ No newline at end of file
diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT
index 892cb6cae7..71a5a48d31 100644
--- a/text-ui-test/EXPECTED.TXT
+++ b/text-ui-test/EXPECTED.TXT
@@ -1,9 +1,14 @@
-Hello from
- ____ _
-| _ \ _ _| | _____
-| | | | | | | |/ / _ \
-| |_| | |_| | < __/
-|____/ \__,_|_|\_\___|
-
-What is your name?
-Hello James Gosling
+ | Typist - Version 2.1
+ | ===========================================================
+ | ______ _ __
+ | /_ __/_ ______ (_)____/ /_
+ | / / / / / / __ \/ / ___/ __/
+ | / / / /_/ / /_/ / (__ ) /_
+ | /_/ \__, / .___/_/____/\__/
+ | /____/_/
+ | Welcome to Typist -- the ultimate cli typing game.
+ | Brought to you by -- AY2122S1-CS2113-T13-4.
+ | man: check man page to get started!
+ | ===========================================================
+Updating game records...
+ | Thanks for using Typist! See you next time!
diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt
index f6ec2e9f95..0abaeaa993 100644
--- a/text-ui-test/input.txt
+++ b/text-ui-test/input.txt
@@ -1 +1 @@
-James Gosling
\ No newline at end of file
+bye
\ No newline at end of file
diff --git a/text-ui-test/time-limited_records.txt b/text-ui-test/time-limited_records.txt
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/text-ui-test/word-limited_records.txt b/text-ui-test/word-limited_records.txt
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/time-limited_records.txt b/time-limited_records.txt
new file mode 100644
index 0000000000..6d37bc7f09
--- /dev/null
+++ b/time-limited_records.txt
@@ -0,0 +1,3 @@
+Time-limited|32.119|24|16|40|60.0|40.0|29.888850835953797|[Lorem, Ipsum, simply, text, the, printing, and, typesetting, industry., Lorem, Ipsum, been, industry's, standard, dummy, text, 1500s, , when, an, unknown, printer, took, type, scrambled]
+Time-limited|37.157|7|33|40|17.5|82.5|53.28740210458326|[fleshy, balloon, head., The, earflaps, , hair, grew]
+Time-limited|36.115|21|9|30|70.0|30.0|14.952235913055516|[the, typesetting, industry., Lorem, Ipsum, has, been, the, industry's, standard, dummy, text, ever, since, the, 1500s, , when, an, unknown, printer, took]
diff --git a/word-limited_records.txt b/word-limited_records.txt
new file mode 100644
index 0000000000..8a947c06b9
--- /dev/null
+++ b/word-limited_records.txt
@@ -0,0 +1 @@
+Word-limited|12.04|3|8|11|27.27272727272727|72.72727272727273|39.8671096345515|[the, printing, typesetting]