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 --------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | 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 + +![](diagrams/archdiagram.png) + +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: + +![](images/command.png) + +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: + +![](images/WordLimitMode.png) + +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. + +![](images/Content.png) + +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. + +![Alt text](diagrams/ViewStatistics-1.drawio.svg) + +Step 4. ViewCommand calls the retrieveStatistics() method of StatisticsManager. +![Alt text](diagrams/ViewStatistics-2.drawio.svg) + +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. + +![Alt text](diagrams/ViewStatistics-3.drawio.svg) +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. +![Alt test](diagrams/ViewStatistics-4.drawio.svg) + + +The following sequence diagram shows how the above scenario is executed. +![Alt test](diagrams/ViewStatistics-5.drawio.svg) + + + +#### 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 @@ + + + +
:ViewCommand
:ViewCommand
Viewer does not support full SVG 1.1
\ 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 @@ + + + +
uses
uses
:StatisitcsManager
:StatisitcsManag...
:ViewCommand
:ViewCommand
Viewer does not support full SVG 1.1
\ 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 @@ + + + +
:ViewCommand
:ViewCommand
Uses
Uses
:StatisitcsManager
:StatisitcsManag...
:GameRecordsManager
:GameRecordsMana...
wordLimitedGameRecords
:GameRecord
wordLimitedGameR...
timeLimitedGameRecords
:GameRecord
timeLimitedGameR...
:Storage
:Storage
Creates
Creates
uses
uses
timeLimitedGameRecords
:GameRecord
timeLimitedGameR...
timeLimitedGameRecords
:GameRecord
timeLimitedGameR...
timeLimitedGameRecords
:GameRecord
timeLimitedGameR...
wordLimitedGameRecords
:GameRecord
wordLimitedGameR...
Viewer does not support full SVG 1.1
\ 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 @@ + + + +
:StatisitcsManager
:StatisitcsManag...
:ViewCommand
:ViewCommand
:GameRecordsManager
:GameRecordsMana...
wordLimitedGameRecords
:GameRecord
wordLimitedGameR...
timeLimitedGameRecords
:GameRecord
timeLimitedGameR...
:Storage
:Storage
Creates
Creates
Uses
Uses
:ViewCommandUi
:ViewCommandUi
Uses
Uses
uses
uses
timeLimitedGameRecords
:GameRecord
timeLimitedGameR...
timeLimitedGameRecords
:GameRecord
timeLimitedGameR...
timeLimitedGameRecords
:GameRecord
timeLimitedGameR...
wordLimitedGameRecords
:GameRecord
wordLimitedGameR...
Viewer does not support full SVG 1.1
\ 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 @@ + + + +:ViewCommand<<class>>StatisticsManagerretrieveStatistics("best", "Time-limited", 4)
calculateBestStatistics(gameRecords)
calculateBestStatistics(gameRecords)
bestStatistics
bestStatistics
bestStatistics:GameRecordsManager()gameRecordsnew GameRecordsManager()<<class>>StoragetimeLimitedGameRecords = readGameRecords("Time-limited")wordLimitedGameRecords = readGameRecords("Word-limited")getGameRecords("Time-limited", 4)<<class>>ViewCommandUidisplayStatistics(bestStatistics)executeCommand("best", "Time-limited", 4)
Viewer does not support full SVG 1.1
\ 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]