diff --git a/.gitignore b/.gitignore deleted file mode 100644 index a6e9064b2c0786196ae3e29e93c0ddbcbdfad705..0000000000000000000000000000000000000000 --- a/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -.idea -.gitignore -out -sp21-cs242-assignment1.iml \ No newline at end of file diff --git a/README.md b/README.md index f64b939130a3903393c5769ba9cd092c1daca560..71d6344847448bd97eacc2239f12558edab5c0d4 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,32 @@ -# sp21-cs242-assignment1.1 (UNO) +# sp21-cs242-assignment1.2 (UNO) Table of Contents ----------------- * [Introduction](#Introduction) +* [MVC Design](#MVC_Design) * [Environment](#Environment) ## Introdution -This is the first part of the UNO MP (CS242@illinois). +This is the completet version of the UNO MP (CS242@illinois). It currently contains three packages: UNO, Test, and GUI. -UNO package provides the implementation of basic game logics for a **single round** of UNO (deck managing, validating play of all 108 cards in various context). For specification of functioning, please refer to [the requirement webpage](https://wiki.illinois.edu/wiki/pages/viewpage.action?pageId=528356500). You can also find a doxygen configuration file in the doxygen directory. Run "doxygen Doxyfile" for auto-generated documents. +1. UNO package provides the implementation of basic game logics for a **single round** of UNO (deck managing, validating play of all 108 cards in various context). For specification of functioning, please refer to [the requirement webpage](https://wiki.illinois.edu/wiki/pages/viewpage.action?pageId=528356500). You can also find a doxygen configuration file in the doxygen directory. Run "doxygen Doxyfile" for auto-generated documents. UNO package also provides two AI player families - a primitive AI that plays randomly and a more strategic AI. The stretegic AI has ~58% winning rate against primitive AI when tested in 100000 complete games. Finally, this UNO package support two extra rule - addition of two cards and subtraction of two cards. + +2. Test package provides comprehensive JUnit tests for testing the functionality of UNO, including the ruleController, player, card manager, AI. + +3. In GUI package, fivestatic, non-interacive GUI (JFrames), including (i) welcome page (ii) player number input page (iii) game stage page (iv) choose color page (v) ending page. It can now support integration of AI into the game using GUI. + + +### MVC Design + +The overall interactive control is implemented following MVC pattern. Specifically, the **UNO.ruleController & UNO.Player** classes serve as the **Model** that stores the game state data (e.g. allowed color, symbol, previous played card, previous player action, etc.) and player hand cards, **UNO.Game** serves as the **Controller**, and **GUI package** serves as the **Viewer**. + +Viewer will never directly interact with Model (you will not find anything about RuleController with Ctrl+F in the Viewer classes), and will always interact with the controller (Game class). -Test package provides comprehensive tests for testing the functionality of UNO. -In GUI package, fivestatic, non-interacive GUI (JFrames), including (i) welcome page (ii) player number input page (iii) game stage page (iv) choose color page (v) ending page. Environment ----------- diff --git a/image/manualTest/1614019925773.png b/image/manualTest/1614019925773.png new file mode 100644 index 0000000000000000000000000000000000000000..3744b325b77bd50362f819d28569a22932ae9c39 Binary files /dev/null and b/image/manualTest/1614019925773.png differ diff --git a/image/manualTest/1614020029698.png b/image/manualTest/1614020029698.png new file mode 100644 index 0000000000000000000000000000000000000000..59eb3ffc1c1489b1af41b0afc5bf92fdce8b370b Binary files /dev/null and b/image/manualTest/1614020029698.png differ diff --git a/image/manualTest/1614020088098.png b/image/manualTest/1614020088098.png new file mode 100644 index 0000000000000000000000000000000000000000..c445d09365dec31d725141ff7af145325864629e Binary files /dev/null and b/image/manualTest/1614020088098.png differ diff --git a/image/manualTest/1614020173560.png b/image/manualTest/1614020173560.png new file mode 100644 index 0000000000000000000000000000000000000000..5a6339e84d3dd41669e216c2d8760c584dd77045 Binary files /dev/null and b/image/manualTest/1614020173560.png differ diff --git a/image/manualTest/1614020363258.png b/image/manualTest/1614020363258.png new file mode 100644 index 0000000000000000000000000000000000000000..7af0d04788989717e14ef3b1be07dea80dedb371 Binary files /dev/null and b/image/manualTest/1614020363258.png differ diff --git a/image/manualTest/1614020568896.png b/image/manualTest/1614020568896.png new file mode 100644 index 0000000000000000000000000000000000000000..8227a7063e514be0f1417045b31887bf4683c1ac Binary files /dev/null and b/image/manualTest/1614020568896.png differ diff --git a/image/manualTest/1614020622245.png b/image/manualTest/1614020622245.png new file mode 100644 index 0000000000000000000000000000000000000000..a67dc94bb0eef5f80c8e1101b4392e4f4617bc0b Binary files /dev/null and b/image/manualTest/1614020622245.png differ diff --git a/image/manualTest/1614020735002.png b/image/manualTest/1614020735002.png new file mode 100644 index 0000000000000000000000000000000000000000..ddbeb891cf8291b6ee77fd1e4f23a3980f9b35e6 Binary files /dev/null and b/image/manualTest/1614020735002.png differ diff --git a/image/manualTest/1614020892335.png b/image/manualTest/1614020892335.png new file mode 100644 index 0000000000000000000000000000000000000000..54a90f94f97f3e9d03aca90e8d5f3fbce71adc66 Binary files /dev/null and b/image/manualTest/1614020892335.png differ diff --git a/image/manualTest/1614020943106.png b/image/manualTest/1614020943106.png new file mode 100644 index 0000000000000000000000000000000000000000..211363c03b710c0c969c1b7285d24dd34ba61c08 Binary files /dev/null and b/image/manualTest/1614020943106.png differ diff --git a/image/manualTest/1614021063906.png b/image/manualTest/1614021063906.png new file mode 100644 index 0000000000000000000000000000000000000000..961ef90c398f2d0f294edcbb85bdcfbddee6bfea Binary files /dev/null and b/image/manualTest/1614021063906.png differ diff --git a/image/manualTest/1614021141517.png b/image/manualTest/1614021141517.png new file mode 100644 index 0000000000000000000000000000000000000000..37becb477fb67c6366ccab5303d98c2504b8b93e Binary files /dev/null and b/image/manualTest/1614021141517.png differ diff --git a/image/manualTest/1614021327580.png b/image/manualTest/1614021327580.png new file mode 100644 index 0000000000000000000000000000000000000000..f6b3025bd5efa95a9b460e6a2a4802f16253c7c3 Binary files /dev/null and b/image/manualTest/1614021327580.png differ diff --git a/image/manualTest/1614021501116.png b/image/manualTest/1614021501116.png new file mode 100644 index 0000000000000000000000000000000000000000..c129a0b75950f677c792de165bc7716bb54c7d77 Binary files /dev/null and b/image/manualTest/1614021501116.png differ diff --git a/image/manualTest/1614025546309.png b/image/manualTest/1614025546309.png new file mode 100644 index 0000000000000000000000000000000000000000..542a166d412787da299d4b78a64a976ccf19d30c Binary files /dev/null and b/image/manualTest/1614025546309.png differ diff --git a/image/manualTest/1614025997954.png b/image/manualTest/1614025997954.png new file mode 100644 index 0000000000000000000000000000000000000000..0d663ae3078b3de28b43604cd788981e874167e8 Binary files /dev/null and b/image/manualTest/1614025997954.png differ diff --git a/image/manualTest/1614026859710.png b/image/manualTest/1614026859710.png new file mode 100644 index 0000000000000000000000000000000000000000..da946bf2b61ebad67240ee81619670a3f03189e3 Binary files /dev/null and b/image/manualTest/1614026859710.png differ diff --git a/image/manualTest/1614026933385.png b/image/manualTest/1614026933385.png new file mode 100644 index 0000000000000000000000000000000000000000..81e0d9cc001d02b07d4330dc7597678de32e5481 Binary files /dev/null and b/image/manualTest/1614026933385.png differ diff --git a/image/manualTest/1614027296283.png b/image/manualTest/1614027296283.png new file mode 100644 index 0000000000000000000000000000000000000000..75d57d11031b93884bb17baa83e080a4a42b2b43 Binary files /dev/null and b/image/manualTest/1614027296283.png differ diff --git a/image/manualTest/1614027730091.png b/image/manualTest/1614027730091.png new file mode 100644 index 0000000000000000000000000000000000000000..07238b9627b3d248de71eb9141c840b7342bd698 Binary files /dev/null and b/image/manualTest/1614027730091.png differ diff --git a/image/manualTest/1614027935342.png b/image/manualTest/1614027935342.png new file mode 100644 index 0000000000000000000000000000000000000000..75abfa1177b7c33fd15a67a4684dbcd3f84f2a4e Binary files /dev/null and b/image/manualTest/1614027935342.png differ diff --git a/image/manualTest/1614028019702.png b/image/manualTest/1614028019702.png new file mode 100644 index 0000000000000000000000000000000000000000..6d0304d73bc2436988688540d5fc7e080457ed01 Binary files /dev/null and b/image/manualTest/1614028019702.png differ diff --git a/image/manualTest/1614028171541.png b/image/manualTest/1614028171541.png new file mode 100644 index 0000000000000000000000000000000000000000..1432b7086dd7962f8c0dbf2e5085141624b08917 Binary files /dev/null and b/image/manualTest/1614028171541.png differ diff --git a/image/manualTest/1614028220162.png b/image/manualTest/1614028220162.png new file mode 100644 index 0000000000000000000000000000000000000000..8804337afa80790041df9ba865ea55b99a4761e6 Binary files /dev/null and b/image/manualTest/1614028220162.png differ diff --git a/image/manualTest/1614028327374.png b/image/manualTest/1614028327374.png new file mode 100644 index 0000000000000000000000000000000000000000..dbe9e4bd4c65c750a4bd712cc7585e0720bc2708 Binary files /dev/null and b/image/manualTest/1614028327374.png differ diff --git a/image/manualTest/1614028367088.png b/image/manualTest/1614028367088.png new file mode 100644 index 0000000000000000000000000000000000000000..41ff3915d7040dbe9b9a4c0a9ed95d4c79f4477d Binary files /dev/null and b/image/manualTest/1614028367088.png differ diff --git a/image/manualTest/1614029332992.png b/image/manualTest/1614029332992.png new file mode 100644 index 0000000000000000000000000000000000000000..e6e972aa67e71853cc130dd674c7ba90cc1a5f16 Binary files /dev/null and b/image/manualTest/1614029332992.png differ diff --git a/image/manualTest/1614029810526.png b/image/manualTest/1614029810526.png new file mode 100644 index 0000000000000000000000000000000000000000..d41ba1410104a76e416011e2fc6f92be2a4e1808 Binary files /dev/null and b/image/manualTest/1614029810526.png differ diff --git a/image/manualTest/1614029871863.png b/image/manualTest/1614029871863.png new file mode 100644 index 0000000000000000000000000000000000000000..fa27126a529496a9bce3d5354d7ebb40a30abff3 Binary files /dev/null and b/image/manualTest/1614029871863.png differ diff --git a/image/manualTest/1614031204314.png b/image/manualTest/1614031204314.png new file mode 100644 index 0000000000000000000000000000000000000000..dfc47e02fff0e46667f542e01bb674295e14c3be Binary files /dev/null and b/image/manualTest/1614031204314.png differ diff --git a/image/manualTest/1614031415563.png b/image/manualTest/1614031415563.png new file mode 100644 index 0000000000000000000000000000000000000000..549843daa7891babde439c4f682a899890236d1c Binary files /dev/null and b/image/manualTest/1614031415563.png differ diff --git a/image/manualTest/1614031481187.png b/image/manualTest/1614031481187.png new file mode 100644 index 0000000000000000000000000000000000000000..15dd45fc279344cc58edf3d43089f4d65cfa77e7 Binary files /dev/null and b/image/manualTest/1614031481187.png differ diff --git a/image/manualTest/1614031678760.png b/image/manualTest/1614031678760.png new file mode 100644 index 0000000000000000000000000000000000000000..cc40a646669ce974bcacef1cbc4667537dca41a5 Binary files /dev/null and b/image/manualTest/1614031678760.png differ diff --git a/image/manualTest/1614031719685.png b/image/manualTest/1614031719685.png new file mode 100644 index 0000000000000000000000000000000000000000..e41976d36900ed328458f25a90b1260933903a89 Binary files /dev/null and b/image/manualTest/1614031719685.png differ diff --git a/image/manualTest/1614049476482.png b/image/manualTest/1614049476482.png new file mode 100644 index 0000000000000000000000000000000000000000..33c6fc033db424b05e412a644a2ea87dd97d8601 Binary files /dev/null and b/image/manualTest/1614049476482.png differ diff --git a/image/manualTest/1614049583593.png b/image/manualTest/1614049583593.png new file mode 100644 index 0000000000000000000000000000000000000000..fe6cb5528e167c3a5822b2e75fbf346b1286a823 Binary files /dev/null and b/image/manualTest/1614049583593.png differ diff --git a/image/manualTest/1614049628455.png b/image/manualTest/1614049628455.png new file mode 100644 index 0000000000000000000000000000000000000000..92a6b7b2754758688c6c0591cc294e759aef1a11 Binary files /dev/null and b/image/manualTest/1614049628455.png differ diff --git a/image/manualTest/1614049737710.png b/image/manualTest/1614049737710.png new file mode 100644 index 0000000000000000000000000000000000000000..d85de5b8a2a3bb7e421960f352dece74dec0124e Binary files /dev/null and b/image/manualTest/1614049737710.png differ diff --git a/image/manualTest/1614049756254.png b/image/manualTest/1614049756254.png new file mode 100644 index 0000000000000000000000000000000000000000..e3d4f87bb5825da64a3ca48a320e18018926ff8f Binary files /dev/null and b/image/manualTest/1614049756254.png differ diff --git a/image/manualTest/1614051934205.png b/image/manualTest/1614051934205.png new file mode 100644 index 0000000000000000000000000000000000000000..9827aab6db1aeb246a2e7503524c4eb1c158242e Binary files /dev/null and b/image/manualTest/1614051934205.png differ diff --git a/image/manualTest/1614053062770.png b/image/manualTest/1614053062770.png new file mode 100644 index 0000000000000000000000000000000000000000..571e370db16f59916d7881c01e192cb188dc7ebb Binary files /dev/null and b/image/manualTest/1614053062770.png differ diff --git a/image/manualTest/1614053080382.png b/image/manualTest/1614053080382.png new file mode 100644 index 0000000000000000000000000000000000000000..aab9062d798ef1ca168793d6f9fcf1e185c3fddc Binary files /dev/null and b/image/manualTest/1614053080382.png differ diff --git a/image/manualTest/20210222173408.png b/image/manualTest/20210222173408.png new file mode 100644 index 0000000000000000000000000000000000000000..7e2548e49f95f174424517a6ea2fd2de7552e978 Binary files /dev/null and b/image/manualTest/20210222173408.png differ diff --git a/image/manualTest/20210222174551.png b/image/manualTest/20210222174551.png new file mode 100644 index 0000000000000000000000000000000000000000..f53abc6d98f199bdfa8537be7944f36ee7b325ed Binary files /dev/null and b/image/manualTest/20210222174551.png differ diff --git a/manualTestAssets/chooseColor.png b/image/manualTest/chooseColor.png similarity index 100% rename from manualTestAssets/chooseColor.png rename to image/manualTest/chooseColor.png diff --git a/manualTestAssets/ending.png b/image/manualTest/ending.png similarity index 100% rename from manualTestAssets/ending.png rename to image/manualTest/ending.png diff --git a/manualTestAssets/gameStage.png b/image/manualTest/gameStage.png similarity index 100% rename from manualTestAssets/gameStage.png rename to image/manualTest/gameStage.png diff --git a/image/manualTest/playerNumber.png b/image/manualTest/playerNumber.png new file mode 100644 index 0000000000000000000000000000000000000000..a97bd55ac0e1638ccfe9cc3a723223c660a47bbe Binary files /dev/null and b/image/manualTest/playerNumber.png differ diff --git a/image/manualTest/test_2_1_case1.png b/image/manualTest/test_2_1_case1.png new file mode 100644 index 0000000000000000000000000000000000000000..56ed7208736dc62d46b528b7579ddd1db2e8a07d Binary files /dev/null and b/image/manualTest/test_2_1_case1.png differ diff --git a/manualTestAssets/welcome.png b/image/manualTest/welcome.png similarity index 100% rename from manualTestAssets/welcome.png rename to image/manualTest/welcome.png diff --git a/manualTest.md b/manualTest.md new file mode 100644 index 0000000000000000000000000000000000000000..3e06b20ee907433c7dee5276948aebe4be5c5023 --- /dev/null +++ b/manualTest.md @@ -0,0 +1,283 @@ +## Manual Test Plan for sp21-CS242-assignment1 + +### Table of Contents + +* [Environment Setup](#Environment) +* [Test-WelcomePage](#Test-WelcomePage) +* [Test-PlayerNumberPage](#Test-PlayerNumberPage) +* [Test-GameStagePage](#Test-GameStagePage) +* [Test-ChooseColorPage](#Test-ChooseColorPage) +* [Test-EndingPage](#Test-EndingPage) +* [Comments](#Comments) + +### Environment Setup + +- Java JDK - 15.0.2 (swing, awt, lang, util) +- JUnit - 4.13.1 +- Junit-jupyter - 5.4.2 +- junit-platform - 1.4.2 +- IntelliJ - education - 2020.3 +- Windows 10 + +Notice these are only environments where this software got developed and is guaranteed to run. They are not meant to be hard requirements. + +### Test-WelcomePage + + + +**@ Test 1.1 - Button "Game Start"** + +When user clicks this button, the window should navigate to the [PlayerNumberPage](!PlayerNumberPage). + +Test result 1.1: (Successfully navigate to the next page) + + + +**@ Test 1.2 - Button "Exit"** + +When user clicks this button, any window should be closed (process exit). + +Test result 1.2: Successfully terminate the whole process. + + +### Test-PlayerNumberPage + + + +**@ Test 2.1 - Illegal Input & Click Start** + +The legal value of the input box should be ["2","3","4","5",""6","7","8","9"], nothing else. Any other input should be judged as illegal. + +If the user input an illegal string and hit "Game Start", a dialogue should occur (to be implemented in assignment-1.2), and there shouldn't be any nagivations to other pages. + +Test 2.1, Case 1: Character inputs - successfully blocked. + + + +Test 2.1, Case 2 : Empty input - successfully blocked. + + + +Test 2.1, Case 3 : Illegal numbers of players - successfully blocked + + + +**@ Test 2.2 - Legal Input & Click Start** + +Upon input of a legal number & hitting of "Game Start", the frame should be navigated to [GameStagePage](!GameStagePage). + +Test result 2.2 - successfully nagivated to the play stage page. + + + + + +**@Test 2.3 - Button "Exit"** + +Same as Test 1.2. + +Test 2.3 result : successfully exit the whole process. + +### Test-GameStagePage + + + +**@ Test 3.1 "Show"/"Hide" Button** + +- When the cards are shown in the central panel, "Hide Cards" should be on the top right area. Clicking this button should cause all cards in the central panel to be hidden. "Hide Cards" should then be replaced with "Show Cards" in the same position. +- When the cards are not shown in the central panel, "Show Cards" should be on the top right area. Clicking this button should cause all cards in the central panel to be shown. "Show Cards" should then be replaced with "Hide Cards" in the same position. + +Hiden Button is clicked (success): + + + +When Show Button is clicked (success): + + + +**@ Test 3.2 Legal"Play Owned"** + +- *Test 3.2.1 Colored Number* + + - Previous round should be updated to this card. + - Discard Pile goes up by 1. + - Prev player action should be "Play Owned (1)" + - Next player information should be properly loaded (top prompt, cards panel). +- *Test 3.2.2 Colored Skip* + + - Previous round should be updated to this card. + - Discard Pile goes up by 1. + - Next player can only hit "Skip" button. No exception. + - Prev player action should be "Play Owned (1)" + - Next player information should be properly loaded (top prompt, cards panel). +- *Test 3.2.3 Colored Reverse* + + - Previous round should be updated to this card. + - Discard Pile goes up by 1. + - The next player should be the player who has just finished his turn before this player. + - Prev player action should be "Play Owned (1)" + - Next player information should be properly loaded (top prompt, cards panel). +- *Test 3.2.4 Colored Draw2** + + - Previous round should be updated to this card. + - Discard Pile goes up by 1. + - Prev player action should be "Play Owned (1)" + - Stacked Draw should go up by 2. +- *Test 3.2.5 Wild/WildDraw4 * + + - Test 4.1 should be fufilled. +- *Test 3.2.6 User plays two cards* + + - Last Card should be updated to the equivalent card of composition of two chosen cards. + - Prev player action should be "Play Owned (2)". + - Discard Pile goes up by **2**. + +Test3.2.1 Result (Previous red num 5 is played): Every game state is updated correctly. Other cases such as "green 6", "yellow 3", "blue 0" are all tested. + + + +Test 3.2.2 Result (Previously a "blue skip" card is played) - Player cannot draw&play (else the dialogue as shown below appears), but could only "Skip". All game states are updated correctly as stated above. + + + + + +Test 3.2.3 Result (Reverse card is played) - Next round is instead Player 3. The game order is reflected to be changed. Discard pile goes up by 1. + + + + + +Test 3.2.4 Result (Blue Draw 2) - When previous player played a "Draw2". Every game state is properly updated. Player 3 don't have legal cards to play & he cannot choose to click "Draw & Play" button. "Skip" and take the penalty will be his only option. Game states should be updated as stated above. + + + + + +Test 3.2.5 Result (Wild/WildDraw4): + +This functionality needs to be tested together with **Test 4.1**. Please go to that section. + +Test 3.2.6 Result (Play two cards by addition / subtraction): All game stated are updated as stated above! + + + + + +@ **Test 3.3 Illegal "Play owned"** + +- A dialogue (to be implemented in assignment-1.2) prompting selected cards is not legal should occur. +- The current GameStagePage should not be updated in any way. + +Test 3.3 Result: Attempting to play "red 2", "green 3", "green 2 +/- green 3", "red 4 +/-red 2" should all lead to this dialogue. Choosing 0 or more than 2 cards will also trigger this dialogue. In order scenarios, combining number cards with symbol/wild cards are also not legal. **(Comprehensive Manual Test**) + + + +**@ Test 3.4 Legal"Draw & Play"** + +- When the previous player did not played one of "wildDraw4", "draw2", "skip", and current player want to skip his round, he should draw one card and attempt to play it ("Draw&Play"). +- If the card is legal, it should be immediately played and program behavior should be same as Test 3.2, except that Prev Player Action should be "Draw & Play (OK)". +- If the card is illegal, the game continues. Sum of Draw & Discard Pile should go down by 1, and Prev Player Action should be "Draw & Play (FAIL)". If the play fails, Last Card should not be updated. + + + + + + + +**Corner Case of Draw&Play:** Draw&Play meets Wild/WildDraw4 + +The pop-up window shows up as expected, and game states are updated correspondingly. + + + + + + + +**@ Test 3.5 Illegal Draw & Play** + +1. When the previous player played one of "wildDraw4", "draw2", "skip", and current player don't have corresponding cards to defense the stacked penalty, he should not be able to draw one card and play ("Draw&Play"). The only allowed action will be to "Skip" his turn. +2. If the one the three cards is played, a dialogue as shown below should occur to prevent the click on "Draw & Play" as shown below. + + + +**@ Test 3.6 Legal"Skip"** + +"Stacked Draw" should be reset to 0 and the sum of Draw & Discard Pile should go down correspondingly. In the following scenario, the previous player played blue draw2, and player 3 don't have draw2/wildDraw4 to defend, nor could he "Draw & Play". His only option would be to "Skip". When Player 3 skip his round, we can observe that the stacked draw got cleared, the draw pile size goes down by 2. + + + + + +@ **Test 3.7 Illegal Skip** + +1. When the previous player did not play one of "wildDraw4", "draw2", "skip" cards, and current player want to skip his turn, he should be forced to draw a card and attempt to play it. "Skip" button should be an illegal action, and he can only "Draw & Play". +2. A dialogue as shown below should occur to prevent the click on "Skip" when none of the three cards get played. + + + + + +**@ Test 3.8 Empty Draw Pile meets Draw&Play** + +When draw pile is empty, and user request draw & play, the new card should be drawn from discard pile. + + + + + + +**@ Test 3.9 Draw & Discard Piles are both Empty** + +When draw & discard piles are both empty, and player requests to draw a card, it should behave just like the "Skip" button (player won't draw any card). + +Suppose there is a "Draw2", "wildDraw4" previously played and there are pending penalty draw, "Skip" button will only force player to draw whatever many cards left in the draw + discard pile. + + + + + + + +### Test-ChooseColorPage + + + +**@ Test 4.1 "Red", "Blue", "Green", "Yellow" Buttons** + +To make things simple, red button will be demonstrated for example. When click on this button, + +1. this Choose Color Pop-up should close and [GameStagePage](!GameStagePage) should occur. +2. The information on the upper-left panel should be "red sym wild" or "red sym wildDraw4". +3. The [GameStagePage](!GameStagePage) should now be serving on a different player. (Topping prompting should change). +4. The cards demonstrated in the central panel should change. +5. The "Discard Pile" in lower-left panel should go up by 1. +6. Stacked Draw should go up by 4 if the previous play was "red sym wildDraw4". + +Test 4.1 Result: Player plays a wild draw 4, the choose color page occurs. When player click the blue button, every thing is correctly updated as stated above. **Manual tests on all other three color buttons have been done, and they all behave as expected.** + + + + + +**@ Test 4.3 Hitting "X" (close frame)** + +Even if the player clicks "X", the whole process should not exit, and this Pop-up window should appear again. + +Test 4.3 Result (success): Even if players click "X", the page will **immediately restart**. + +**NOTE**: click "X" on all other pages should cause the whole process to exit. + +### Test-EndingPage + + + +**@ Test 5.1 "New Game" Button** + +- The player ID prompted on top should be correct (player of last round). +- Upon clicking this button, the player should be navigated back to [WelcomePage](!WelcomePage). + +Test 5.1 Result : user is nagivated back to the WelcomePage. + + diff --git a/manualTestAssets/playerNumber.png b/manualTestAssets/playerNumber.png deleted file mode 100644 index 2cbddea1a1a3eafc60869d35aad50b4be5862aaf..0000000000000000000000000000000000000000 Binary files a/manualTestAssets/playerNumber.png and /dev/null differ diff --git a/manualTestPlan.md b/manualTestPlan.md deleted file mode 100644 index ebf1d341e3ed322a2bc95212bb1f9e6e967466b7..0000000000000000000000000000000000000000 --- a/manualTestPlan.md +++ /dev/null @@ -1,156 +0,0 @@ -## Manual Test Plan for sp21-CS242-assignment1 - -### Table of Contents - -* [Environment Setup](#Environment) -* [Test-WelcomePage](#Test-WelcomePage) -* [Test-PlayerNumberPage](#Test-PlayerNumberPage) -* [Test-GameStagePage](#Test-GameStagePage) -* [Test-ChooseColorPage](#Test-ChooseColorPage) -* [Test-EndingPage](#Test-EndingPage) -* [Comments](#Comments) - -### Environment Setup - -- Java JDK - 15.0.2 (swing, awt, lang, util) -- JUnit - 4.13.1 -- Junit-jupyter - 5.4.2 -- junit-platform - 1.4.2 -- IntelliJ - education - 2020.3 -- Windows 10 - -Notice these are only environments where this software got developed and is guaranteed to run. They are not meant to be hard requirements. - -### Test-WelcomePage - - - -**@ Test 1.1 - Button "Game Start"** - -When user clicks this button, the window should navigate to the [PlayerNumberPage](!PlayerNumberPage). - -**@ Test 1.2 - Button "Exit"** - -When user clicks this button, any window should be closed (process exit). - -### Test-PlayerNumberPage - - - -**@ Test 2.1 - Illegal Input & Click Start** - -The legal value of the input box should be ["2","3","4","5",""6","7","8","9"], nothing else. Any other input should be judged as illegal. - -If the user input an illegal string and hit "Game Start", a dialogue should occur (to be implemented in assignment-1.2), and there shouldn't be any nagivations to other pages. - -**@ Test 2.2 - Legal Input & Click Start** - -Upon input of a legal number & hitting of "Game Start", the frame should be navigated to [GameStagePage](!GameStagePage). - -**@Test 2.3 - Button "Exit"** - -Same as Test 1.2. - -### Test-GameStagePage - - - -**@ Test 3.1 "Show"/"Hide" Button** - -- When the cards are shown in the central panel, "Hide Cards" should be on the top right area. Clicking this button should cause all cards in the central panel to be hidden. "Hide Cards" should then be replaced with "Show Cards" in the same position. -- When the cards are not shown in the central panel, "Show Cards" should be on the top right area. Clicking this button should cause all cards in the central panel to be shown. "Show Cards" should then be replaced with "Hide Cards" in the same position. - -**@ Test 3.2 "Play Owned" Button & Valid Play** - -- *Test 3.2.1 Colored Number* - - - Previous round should be updated to this card. - - Discard Pile goes up by 1. - - Next player information should be properly loaded (top prompt, cards panel). -- *Test 3.2.2 Colored Skip* - - - Previous round should be updated to this card. - - Discard Pile goes up by 1. - - Next player can only hit "Skip" button. No exception. - - Next player information should be properly loaded (top prompt, cards panel). -- *Test 3.2.3 Colored Reverse* - - - Previous round should be updated to this card. - - Discard Pile goes up by 1. - - The next player should be the player who has just finished his turn before this player. - - Next player information should be properly loaded (top prompt, cards panel). -- *Test 3.2.4 Colored Draw2* - - - Previous round should be updated to this card. - - Discard Pile goes up by 1. - - Stacked Draw should go up by 2. -- *Test 3.2.5 Wild/WildDraw4* - - - The Pop-up window [ChooseColorPage](!ChooseColorPage) should appear. - - All behavior described in Test 4.x should be conformed. - -**@ Test 3.3 "Draw & Play" Button** - -- A dialogue (to be implemented in assignment-1.2) should occur, prompting the player the card he just got. -- If the card is legal, it should be immediately played and program behavior should be same as Test 3.2. -- If the card is illegal, the player will be skipped. Sum of Draw & Discard Pile should go down by 1. - -**@ Test 3.4 "Play 2 - Add" Button & Valid Play** - -- "Previous Card" should be updated to "COLOR NUM (num1+num2)", where num1, num2 are numbers on the two chosen cards. -- Discard Pile goes up by 2. -- Next player information should be properly loaded (top prompt, cards panel). - -**@ Test 3.5 "Play 2 - Minus" Button & Valid Play** - -- "Previous Card" should be updated to "COLOR NUM (bigger - smaller)", where bigger is the bigger number in two chosen cards. -- Discard Pile goes up by 2. -- Next player information should be properly loaded (top prompt, cards panel). - -**@ Test 3.6 "Skip" Button** - -This button should succeed condition-lessly. "Stacked Draw" should be reset to 0 and the sum of Draw & Discard Pile should go down correspondingly. - -**@ Test 3.7 Any Illegal Plays** - -- A dialogue (to be implemented in assignment-1.2) prompting play is illegal should occur. -- The current GameStagePage should not be updated in any way. - -### Test-ChooseColorPage - - - -**@ Test 4.1 "Red" Button** - -When user click this button, [GameStagePage](!GameStagePage) - -1. this Pop-up should close and [GameStagePage](!GameStagePage) should occur. -2. The information on the upper-left panel should be "red sym wild" or "red sym wildDraw4". -3. The [GameStagePage](!GameStagePage) should now be serving on a different player. (Topping prompting should change). -4. The cards demonstrated in the central panel should change. -5. The "Discard Pile" in lower-left panel should go up by 1. -6. Stacked Draw should go up by 4 if the previous play was "red sym wildDraw4". - -**@ Test 4.2 "Blue", "Green", "Yellow" Buttons** - -Everything should be same as Test 4.1 except "previous Round" information in the upper-left panel. - -**@ Test 4.3 Hitting "X" (close frame)** - -Even if the player clicks "X", the whole process should not exit, and this Pop-up window should appear again. - -**NOTE**: click "X" on all other pages should cause the whole process to exit. - -### Test-EndingPage - - - -**@ Test 5.1 "New Game" Button** - -- The player ID prompted on top should be correct (player of last round). -- Upon clicking this button, the player should be navigated back to [WelcomePage](!WelcomePage). - -### Comments - -1. As dialogues should be one or two line issue with Java swing, and GUI-assignment-1.1 is intended to be static, they are not implemented. -2. After clicking bottom action buttons in [GameStagePage](!GameStagePage), user should be able to choose cards. The precise way of choosing them is not decided yet. Here are two plans: (1) using input box, and input card index; (2) making cards as clickable Swing Objects and directly click. I will choose either one to implement in assignment-1.2. diff --git a/src/GUI/ChooseColorPopUp.java b/src/GUI/ChooseColorPopUp.java index 16311761dc6e839731958ca84beb21cabff90e2a..97fa4372fe7262da8ea320e9eca77584bd65667b 100644 --- a/src/GUI/ChooseColorPopUp.java +++ b/src/GUI/ChooseColorPopUp.java @@ -1,16 +1,30 @@ package GUI; +import UNO.Game; import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +/** + * !!!!!!!!!!!!!!!!!!!!!! Viewer IN MVC !!!!!!!!!!!!!!!!!!!!!!! + * Viewer for Scenario where players plays a wild car. + * Prompt the player to choose a color and pass this info to Controller. + * + * MVC Design: This class won't employ any interface provided by Model (Player & ruleController). + * It will only interact with Game (Controller). + */ public class ChooseColorPopUp extends Frames { - private final int W = 540; // Width - private final int H = 360; // Height - private int winnerID; + private final int W = 540; // Width of window + private final int H = 360; // Height of window + private Game gameController; + private JFrame window; - public ChooseColorPopUp() { - winnerID = 1; // should be the correct winner passed by game controller! - - JFrame window = new JFrame("UNO - Choose Color"); + /** + * @param game the game controller in the MVC + */ + public ChooseColorPopUp(Game game) { + gameController = game; + window = new JFrame("UNO - Choose Color"); addButtons(window); addPrompts(window); addBackground(window, W, H); @@ -20,25 +34,79 @@ public class ChooseColorPopUp extends Frames { window.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); } - public void addButtons(JFrame window) { + /** + * All magic numbers are manually adjustable positionable information. + * They are targeted for visual-understanding. + * + * Add red, green, blue, yellow buttons to the page. + * @param window JFrame containing the buttons + */ + public void addButtons(JFrame window) { // red String redButtonPath = "./src/GUI/assets/redButton.png"; - createImageButtonAndAdd(window, redButtonPath, (int) (W * 0.25 - W / 10), (int) (H * 0.55), W / 5, H / 8); + JButton redButton = createImageButton(redButtonPath, (int) (W * 0.25 - W / 10), (int) (H * 0.55), W / 5, H / 8); + window.add(redButton); + redButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gameController.updateGameStateBasedOnPickedColor("red"); + window.dispose(); + window.setVisible(false); + gameController.setColorIsPicked(); // inform controller color picking is done + } + }); // green String greenButtonPath = "./src/GUI/assets/greenButton.png"; - createImageButtonAndAdd(window, greenButtonPath, (int) (W * 0.25 - W / 10), (int) (H * 0.75), W / 5, H / 8); + JButton greenButton = createImageButton(greenButtonPath, (int) (W * 0.25 - W / 10), (int) (H * 0.75), W / 5, H / 8); + window.add(greenButton); + greenButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gameController.updateGameStateBasedOnPickedColor("green"); + window.dispose(); + window.setVisible(false); + gameController.setColorIsPicked(); // inform controller color picking is done + } + }); // blue String blueButtonPath = "./src/GUI/assets/blueButton.png"; - createImageButtonAndAdd(window, blueButtonPath, (int) (W * 0.75 - W / 10), (int) (H * 0.55), W / 5, H / 8); + JButton blueButton = createImageButton(blueButtonPath, (int) (W * 0.75 - W / 10), (int) (H * 0.55), W / 5, H / 8); + window.add(blueButton); + blueButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gameController.updateGameStateBasedOnPickedColor("blue"); + window.dispose(); + window.setVisible(false); + gameController.setColorIsPicked(); // inform controller color picking is done + } + }); // yellow String yellowButtonPath = "./src/GUI/assets/yellowButton.png"; - createImageButtonAndAdd(window, yellowButtonPath, (int) (W * 0.75 - W / 10), (int) (H * 0.75), W / 5, H / 8); + JButton yellowButton = createImageButton(yellowButtonPath, (int) (W * 0.75 - W / 10), (int) (H * 0.75), W / 5, H / 8); + window.add(yellowButton); + yellowButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gameController.updateGameStateBasedOnPickedColor("yellow"); + window.dispose(); + window.setVisible(false); + gameController.setColorIsPicked(); // inform controller color picking is done + } + }); } + /** + * All magic numbers are manually adjustable positionable information for string prompts. + * They are targeted for visual-understanding. + * + * Add two lines of string prompts into the page. + * @param window JFrame containing the prompts + */ public void addPrompts(JFrame window) { // Prompt JLabel prompt1 = createLabel( "You played a wild card", (int) (W * 0.5), (int) (H * 0.25), 20); @@ -48,7 +116,11 @@ public class ChooseColorPopUp extends Frames { } - public static void main(String[] args) { - new ChooseColorPopUp(); + /** + * Set the ChooseColor window as visible or invisible. This is to prevent user click "X" by chance. + */ + public void setAsVisible(boolean bool) { + window.setVisible(bool); } + } diff --git a/src/GUI/Frames.java b/src/GUI/Frames.java index 8faac549490577e83ee2f6e88a7f3762efa564b2..d8caaa8ccb1173eff543d66b22efff926d5f50e8 100644 --- a/src/GUI/Frames.java +++ b/src/GUI/Frames.java @@ -51,18 +51,21 @@ public abstract class Frames { /** * Given some words to be prompted, pretty-print the words to a JLabel and return it. - * @param words words to be prompted + * Please notice, this function only handles one-line input (without "\n") + * @param words words to be prompted (no "\n" is allowed) * @param x center x-coordinate of the words * @param y center y-coordinate of the words - * @param fontSize fontsize to be used + * @param fontSize font size to be used * @return */ public JLabel createLabel(String words, int x, int y, float fontSize) { JLabel label = new JLabel(words); label.setFont(label.getFont().deriveFont(fontSize)); - int strLength = label.getFontMetrics(label.getFont()).stringWidth(label.getText()) + 10; + + // calculate the length & height of input words + int strLength = label.getFontMetrics(label.getFont()).stringWidth(label.getText()) + 10; // +10 (give more space) because sometimes the words are not properly demonstrated int strHeight = label.getFontMetrics(label.getFont()).getHeight(); - label.setBounds(x - (strLength-10) / 2, y - strHeight / 2, strLength, strHeight); + label.setBounds(x - (strLength-10) / 2, y - strHeight / 2, strLength, strHeight); // centering, and set size return label; } @@ -105,4 +108,23 @@ public abstract class Frames { window.add(promptGameStateBoarder); } + /** + * Build a dialogue with input information and add it to the given JFrame window. + * @param window the window where this dialogue will be inserted + * @param title the upper-left corner title for the dialogue + * @param prompt the prompt string on main body of the dialogue + * @param x x coordinate where this dialogue would occur + * @param y y coordinate where this dialogue would occur + * @param w the width of dialogue + * @param h the height of dialogue + */ + public void createDialogue(JFrame window, String title, String prompt, int x, int y, int w, int h) { + JDialog d = new JDialog(window, title); + JLabel p = new JLabel(prompt); + d.setBounds(x, y, w, h); + d.add(p); + d.setVisible(true); + p.setVisible(true); + } + } diff --git a/src/GUI/GameStagePage.java b/src/GUI/GameStagePage.java index 3f451f556510867f3c423ec7601770d2da6f4232..e0de091d7154e9154be2b11e8dba0d9e3d075c81 100644 --- a/src/GUI/GameStagePage.java +++ b/src/GUI/GameStagePage.java @@ -4,175 +4,389 @@ import javax.swing.*; import java.awt.*; import javax.swing.border.Border; import javax.swing.border.TitledBorder; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; import java.util.Random; import UNO.*; /** - * The Game UI. + * !!!!!!!!!!!!!!!!!!!!!! Viewer IN MVC !!!!!!!!!!!!!!!!!!!!!!! + * The Game UI. (Viewer) * Allows users to choose actions in their rounds, * show them their owned cards (can be hidden), * the current game state, and card pile status. + * + * MVC Design: This class won't employ any interface provided by Model (Player & ruleController). + * It will only interact with Game (Controller). */ public class GameStagePage extends Frames { - private final int W = 1080; // Width - private final int H = 720; // Height - private int winnerID; - private Game gameController; + private final int W = 1080; // Width of window + private final int H = 720; // Height of window + private final int currentPlayerID; // the id of current player + private final Player currentPlayer; // the cards info of this player will be on this window + private final Game gameController; //Game controller (Controller in MVC) + private final JFrame window; // the main frame of game stage page + private JButton hideCardButton; // GUI element - hide + private JButton showCardButton; // GUI element - show cards button + private ArrayList<JButton> cardFrontImgs; // GUI element - card images on the central panel + private ArrayList<JButton> cardBackImgs; // GUI element - card images on the central panel + private ArrayList<Boolean> cardClickStatus; // keep track of whether cards has been clicked + private ArrayList<Integer> playerCards; public GameStagePage(Game game) { - gameController = game; - winnerID = 1; // should be the correct winner passed by game controller! - JFrame window = new JFrame("UNO - Game Stage"); + gameController = game; //Game controller (Controller in MVC) + currentPlayerID = gameController.getCurrentPlayerID(); + playerCards = gameController.getPlayerCards(currentPlayerID); + currentPlayer = gameController.getPlayers().get(currentPlayerID); + + window = new JFrame("UNO - Game Stage"); addButtons(window); addPrompts(window); addBackground(window, W, H); window.setSize(W, H); window.setLayout(null); - window.setVisible(true); window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + window.setVisible(true); } + /** + * Add i. "Hide Cards", ii. "Show Cards", iii. "Play owned", iv. "Draw & Play", + * v. "Skip" to the page. + * @param window JFrame containing the buttons + */ public void addButtons(JFrame window) { - // hide card button - String hideCardButtonPath = "./src/GUI/assets/hideCardButton.png"; - createImageButtonAndAdd(window, hideCardButtonPath, (int) (0.85 * W), (int) (0.05 * H), W / 10, H / 12); - - - // show card button - String showCardButtonPath = "./src/GUI/assets/showCardButton.png"; - createImageButtonAndAdd(window, showCardButtonPath, (int) (0.85 * W), (int) (0.05 * H), W / 10, H / 12); - - - // Play Owned button - String playOwnedButtonPath = "./src/GUI/assets/playOwnedButton.png"; - createImageButtonAndAdd(window, playOwnedButtonPath, (int) (0.1 * W) - (int)(0.065 * W), (int) (0.8 * H), W / 8, H / 10); - - // Draw&Play button - String drawAndPlayPath = "./src/GUI/assets/drawAndPlayButton.png"; - createImageButtonAndAdd(window, drawAndPlayPath, (int) (0.3 * W) - (int)(0.065 * W), (int) (0.8 * H), W / 8, H / 10); - - // Play2Add button - String play2AddPath = "./src/GUI/assets/play2AddButton.png"; - createImageButtonAndAdd(window, play2AddPath, (int) (0.5 * W) - (int)(0.065 * W), (int) (0.8 * H), W / 8, H / 10); - - // Play2Subtract button - String play2SubPath = "./src/GUI/assets/play2SubButton.png"; - createImageButtonAndAdd(window, play2SubPath, (int) (0.7 * W) - (int)(0.065 * W), (int) (0.8 * H), W / 8, H / 10); - - // skip button - String skipPath = "./src/GUI/assets/skipButton.png"; - createImageButtonAndAdd(window, skipPath, (int) (0.9 * W) - (int)(0.065 * W), (int) (0.8 * H), W / 8, H / 10); + addShowCardButton(); // show card button + addHideCardButton(); // hide card button + addPlayOwnedButton(); // Play Owned button + addDrawAndPlayButton(); // Draw&Play button + addSkipButton(); // skip button } + /** + * All magic numbers are manually adjustable positionable information. + * They are targeted for visual-understanding. + * + * Prompt the round info, game state info, card info, card info onto the window. + * Refactored version from 1.1. + * @param window JFrame containing the prompts + */ public void addPrompts(JFrame window) { - - // Prompt on the Top - String promptTopStr = "It's Player " + gameController.getCurrentPlayerID() + " 's round!"; - JLabel promptTop = createLabel(promptTopStr, (int) (W * 0.5), (int) (H * 0.05), 30); + /* Prompt on the Top */ + String AImark = gameController.isHuman(currentPlayerID)? "":" (AI)"; + String promptTopStr = "It's Player " + gameController.getCurrentPlayerID() + AImark + "'s round!"; + JLabel promptTop = createLabel(promptTopStr, (int) (W * 0.5), (int) (H * 0.05), 30); window.add(promptTop); - // Prompt Game State - createTitledBorderBoxAdd(window, "Game State", (int)(0.05*W), (int) (0.15 * H), (int) (W * 0.25), (int) (H * 0.3)); + /* Prompt Game State (title) */ + createTitledBorderBoxAdd(window, "Game State", (int) (0.05 * W), (int) (0.15 * H), (int) (W * 0.25), (int) (H * 0.35)); - String prevCard = gameController.getRuler().getPreviousCard(); // add prev card - JLabel promptPrevCard1 = createLabel("Previous Round :", (int) (W * 0.175), (int) (H * 0.2), 15); - JLabel promptPrevCard2 = createLabel(prevCard, (int) (W * 0.175), (int) (H * 0.23), 15); - window.add(promptPrevCard1); - window.add(promptPrevCard2); + /* Prompt Game State - last played card. */ + String prevCard = gameController.getPreviousCard(); + createTwoLinePrompts("Last Card:", prevCard, "", (int) (H * 0.2)); - int penaltyDraw = gameController.getRuler().getPenaltyDraw(); // add penalty draw info - JLabel promptPenalty1 = createLabel("Stacked Draw : ", (int) (W * 0.175), (int) (H * 0.29), 15); - JLabel promptPenalty2 = createLabel(""+penaltyDraw, (int) (W * 0.175), (int) (H * 0.32), 15); - window.add(promptPenalty1); - window.add(promptPenalty2); + /* Prompt Game State - prev player action */ + String prevPlayerAct = gameController.getPreviousAction(); + createTwoLinePrompts("Prev Player Action:", prevPlayerAct, "", (int) (H * 0.275)); - int nextPlayerID = gameController.getNextPlayerID(); // prompt next player following current game state - JLabel promptNextPlayer1 = createLabel("Next Player : ", (int) (W * 0.175), (int) (H * 0.38), 15); - JLabel promptNextPlayer2 = createLabel(""+nextPlayerID, (int) (W * 0.175), (int) (H * 0.41), 15); - window.add(promptNextPlayer1); - window.add(promptNextPlayer2); + /* Prompt Game State - stacked penalty draw */ + int penaltyDraw = gameController.getPenaltyDraw(); + createTwoLinePrompts("Stacked Draw: ", ""+ penaltyDraw, "", (int) (H * 0.35)); - // Prompt card pile - createTitledBorderBoxAdd(window, "Card Piles", (int)(0.05*W), (int) (0.45 * H), (int) (W * 0.25), (int) (H * 0.3)); + /* prompt next player following current game state*/ + int nextPlayerID = gameController.getNextPlayerID(); + createTwoLinePrompts("Next Player: ", ""+ nextPlayerID, "", (int) (H * 0.425)); + /* Prompt card pile information */ + createTitledBorderBoxAdd(window, "Card Piles", (int) (0.05 * W), (int) (0.5 * H), (int) (W * 0.25), (int) (H * 0.25)); int numDrawPile = gameController.getCardManager().numCardLeft(); // add number of draw pile - JLabel promptDrawPile1 = createLabel("Draw Pile:", (int) (W * 0.175), (int) (H * 0.5), 18); - JLabel promptDrawPile2 = createLabel(numDrawPile + " Left", (int) (W * 0.175), (int) (H * 0.55), 18); - window.add(promptDrawPile1); - window.add(promptDrawPile2); - + createTwoLinePrompts("Draw Pile: ", ""+ numDrawPile, "", (int) (H * 0.575)); + /* prompt next player following current game state */ int numDiscardPile = gameController.getCardManager().numLeftDiscardPile(); // add number of discard pile - JLabel promptDiscardPile1 = createLabel("Discard Pile:", (int) (W * 0.175), (int) (H * 0.62), 18); - JLabel promptDiscardPile2 = createLabel(numDiscardPile + " Left", (int) (W * 0.175), (int) (H * 0.67), 18); - window.add(promptDiscardPile1); - window.add(promptDiscardPile2); - + createTwoLinePrompts("Discard Pile: ", ""+ numDiscardPile, "", (int) (H * 0.65)); - // Prompt player hand - createTitledBorderBoxAdd(window, "Player Cards", (int)(0.3*W), (int) (0.15 * H), (int) (W * 0.6), (int) (H * 0.6)); - promptHandHelper(window, (int)(0.3*W), (int) (0.15 * H), (int) (W * 0.6), (int) (H * 0.6)); + /* Prompt player hand cards */ + createTitledBorderBoxAdd(window, "Player Cards", (int) (0.3 * W), (int) (0.15 * H), (int) (W * 0.6), (int) (H * 0.6)); + cardFrontImgs = loadCardPrompts(window, true, (int) (0.3 * W), (int) (0.15 * H), (int) (W * 0.6), (int) (H * 0.6)); + cardBackImgs = loadCardPrompts(window, false, (int) (0.3 * W), (int) (0.15 * H), (int) (W * 0.6), (int) (H * 0.6)); } + /** + * Create two line prompts - helper for addPrompts. + * The line gap is set to 0.025H as default (manually adjusted). + * @param prefix First line prompt + * @param content the major prompting content + * @param suffix second lien suffix (e.g. units, or "Left") + * @param firstLineY the y coordinate of first line. + */ + private void createTwoLinePrompts(String prefix, String content, String suffix, int firstLineY) { + String prevCard = gameController.getPreviousCard(); + JLabel promptLine1 = createLabel(prefix, (int) (W * 0.175), firstLineY, 15); + JLabel promptLine2 = createLabel(content + suffix, (int) (W * 0.175), + (int) (firstLineY + 0.025 * H), 15); + window.add(promptLine1); + window.add(promptLine2); + } /** - * Add player's card to promptHand area - * @param window frame - * @param x,y,w,h the four attributes of promptHand + * All magic numbers are manually adjustable positionable information. + * They are targeted for visual-understanding. + * This function is not quite refactor-able as its inner variable has strong dependency. + * i.e. If extract some part of it, there might be 8 parameters, which lowers readability. + * + * Add player's hand cards to promptHand area. + * @param cardFront if true, load card front images, else back images + * @param window frame + * @param x,y,w,h the four attributes of promptHand */ - private void promptHandHelper(JFrame window, int x, int y, int w, int h) { - CardParser parser = new CardParser(); - int playerID = gameController.getCurrentPlayerID(); - Player player = gameController.getPlayers().get(playerID); - assert(player.getCards().size() <= 40); + private ArrayList<JButton> loadCardPrompts(JFrame window,boolean cardFront, int x, int y, int w, int h) { + if (cardFront) cardClickStatus = new ArrayList<>(); // we don't want to track click status when cards are hidden + ArrayList<JButton> cardImgs = new ArrayList<>(); + CardParser parser = new CardParser(); + assert (playerCards.size() <= 40); // It's hardly possible for any player to hold more than 40 cards. int cardBorderWidth = (int) (w / 12); int cardBorderHeight = (int) (h / 4.5); - y = y + (int) (0.02*h); // upper gap - - int xGap = (int) (0.01*w); // gap between cards - int yGap = (int) (0.01*h); - - for (int i = 0; i < player.getCards().size(); i++) { - int cardID = player.getCards().get(i); - Border line = BorderFactory.createLineBorder(Color.white); - TitledBorder cardBorder = BorderFactory.createTitledBorder(line, i + ""); - JLabel cardBorderLabel = new JLabel(); - cardBorderLabel.setBorder(cardBorder); - window.add(cardBorderLabel); + y = y + (int) (0.02 * h); // add some gap from top + int xGap = (int) (0.01 * w); // x gap between cards + int yGap = (int) (0.01 * h); // y gap between cards + for (int i = 0; i < playerCards.size(); i++) { + int cardID = playerCards.get(i); int xBorder = x + xGap + cardBorderWidth * (i % 10); int yBoarder = y + yGap + cardBorderHeight * ((int) (i / 10)); - cardBorderLabel.setBounds(xBorder, yBoarder, cardBorderWidth, cardBorderHeight); -// // User image-represented cards String cardDesc = parser.parseCardID(cardID); - String cardPath = "./src/GUI/assets/cards/"+cardDesc.replace(" ", "_")+".jpg"; - ImageIcon cardImgIcon = loadImageIcon(cardPath, (int)(cardBorderWidth*0.9), (int)(cardBorderHeight*0.9)); - JLabel cardImg = new JLabel(cardImgIcon); - cardImg.setBounds((int) (xBorder + xGap * 0.5), (int)(yBoarder + yGap * 1.5), (int)(cardBorderWidth*0.9), (int)(cardBorderHeight*0.9)); + String cardPath; + if (cardFront) cardPath = "./src/GUI/assets/cards/" + cardDesc.replace(" ", "_") + ".jpg"; + else cardPath = "./src/GUI/assets/cards/back.jpg"; + + ImageIcon cardImgIcon = loadImageIcon(cardPath, (int) (cardBorderWidth * 0.9), (int) (cardBorderHeight * 0.9)); + JButton cardImg = new JButton(cardImgIcon); + cardClickStatus.add(false); // initialize the button as not clicked + + if (cardFront) { // hidden cards (back) don't need event handler + cardImg.addActionListener(new cardClickedListener(cardImgs, cardImg)); + } + cardImg.setBounds((int) (xBorder + xGap * 0.5), (int) (yBoarder + yGap * 1.5), (int) (cardBorderWidth * 0.9), (int) (cardBorderHeight * 0.9)); + cardImgs.add(cardImg); + cardImg.setVisible(!cardFront); // card back images should be shown on the first hand to prevent others see the cards window.add(cardImg); + } + return cardImgs; + } + + /** + * All magic numbers are manually adjustable positionable information. + * They are targeted for visual-understanding. + * + * Add hide card button to the window. + */ + private void addHideCardButton() { + // hide card button + String hideCardButtonPath = "./src/GUI/assets/hideCardButton.png"; + hideCardButton = createImageButton(hideCardButtonPath, (int) (0.85 * W), (int) (0.05 * H), W / 10, H / 12); + hideCardButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + for (JButton cardImg : cardBackImgs) { + cardImg.setVisible(true); + } + for (JButton cardImg : cardFrontImgs) { + cardImg.setVisible(false); + } + showCardButton.setVisible(true); + hideCardButton.setVisible(false); + } + }); + window.add(hideCardButton); + } + + /** + * All magic numbers are manually adjustable positionable information. + * They are targeted for visual-understanding. + * + * Add show card button to the page. + * Notice the magic blanks are for centering. + */ + private void addShowCardButton() { + // hide card button + String hideCardButtonPath = "./src/GUI/assets/showCardButton.png"; + showCardButton = createImageButton(hideCardButtonPath, (int) (0.85 * W), (int) (0.05 * H), W / 10, H / 12); + showCardButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (!gameController.isHuman(currentPlayerID)) { // Player cannot view AI's cards + JOptionPane.showMessageDialog(window,"Sorry, you can't view AI's cards.","Illegal Action", JOptionPane.INFORMATION_MESSAGE); + return; + } + for (JButton cardImg : cardFrontImgs) cardImg.setVisible(true); + for (JButton cardImg : cardBackImgs) cardImg.setVisible(false); + hideCardButton.setVisible(true); + showCardButton.setVisible(false); + } + }); + window.add(showCardButton); + } + + /** + * All magic numbers are manually adjustable positionable information. + * They are targeted for visual-understanding. + * + * Add play owned card button to the page. + * Although quite complicated, none of the parts can be extracted. + * Notice the magic blanks are for centering. + */ + private void addPlayOwnedButton() { + String playOwnedButtonPath = "./src/GUI/assets/playOwnedButton.png"; + JButton playOwnedButton = createImageButton(playOwnedButtonPath, (int) (0.2 * W) - (int) (0.065 * W), (int) (0.8 * H), W / 8, H / 10); + playOwnedButton.addActionListener(new playOwnedButtonActionListener()); + playOwnedButton.setVisible(true); + window.add(playOwnedButton); + } + + /** + * Add draw&play button to the window. + * Notice the magic blanks are for centering. + */ + private void addDrawAndPlayButton() { + String drawAndPlayPath = "./src/GUI/assets/drawAndPlayButton.png"; + JButton drawAndPlayButton = createImageButton(drawAndPlayPath, (int) (0.5 * W) - (int) (0.065 * W), (int) (0.8 * H), W / 8, H / 10); + drawAndPlayButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (!gameController.isHuman(currentPlayerID)) return; // human cannot help AI to make decision + if (gameController.getNextPlayerSkiplevel() != 0) { // If there is pending stacked draw, player cannot draw&play + JOptionPane.showMessageDialog(window,"You can't Draw&Play because you are being skipped.","Illegal Action", JOptionPane.INFORMATION_MESSAGE); + return; + } + gameController.setUserAction(2); + } + }); + window.add(drawAndPlayButton); + } + + /** + * All magic numbers are manually adjustable positionable information. + * They are targeted for visual-understanding. + * + * Add skip button to the window. + */ + private void addSkipButton() { + String skipPath = "./src/GUI/assets/skipButton.png"; + JButton skipButton = createImageButton(skipPath, (int) (0.8 * W) - (int) (0.065 * W), (int) (0.8 * H), (int)(W / 8), (int)(H / 10)); + skipButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (!gameController.isHuman(currentPlayerID)) return; // human cannot help AI to make decision + if (gameController.getNextPlayerSkiplevel() == 0) { + JOptionPane.showMessageDialog(window,"You are not being skipped this round!","Illegal Action", JOptionPane.INFORMATION_MESSAGE); + return; + } + gameController.setUserAction(3); // will succeed in any conditions + } + }); + window.add(skipButton); + } + + /** + * Close the game stage page of one player + */ + public void dispose() { + window.dispose(); + } + + /** + * Get all selected cards in the current GUI. + * @return all cards (as ID) selected by the user as an ArrayList<Integer> + */ + public ArrayList<Integer> getSelectedCards() { + ArrayList<Integer> selectedCards = new ArrayList<>(); + ArrayList<Integer> cards = playerCards; + for (int i = 0; i < cardClickStatus.size(); i++) { + if (cardClickStatus.get(i)) { + selectedCards.add(cards.get(i)); + } + } + return selectedCards; + } -// // Use description-represented cards -// int xFont = xBorder + (int) (cardBorderWidth * 0.5); -// int yFont = yBoarder + (int) (cardBorderHeight * 0.5); -// JLabel cardLabel = createLabel(parser.parseCardID(cardID), xFont, yFont, 6); -// window.add(cardLabel); + /** + * When players played illegally, help them clear their previous choice. + */ + private void clearSelectedCards() { + for (int i = 0; i < cardFrontImgs.size(); i++) { + cardFrontImgs.get(i).setBorder(null); + cardClickStatus.set(i, false); } + } + /** + * All magic numbers are manually adjustable positionable information for centering string prompts. + * They are targeted for visual-understanding. + * + * The Event listener for "play owned" button. + * Although this is a long function, it's not separable because it needs to + * block many illegal inputs and return from the function. + */ + private class playOwnedButtonActionListener implements ActionListener { + @Override + public void actionPerformed(ActionEvent e) { + if (!gameController.isHuman(currentPlayerID)) return; // human cannot help AI to make decision + ArrayList<Integer> selectedCards = getSelectedCards(); + if (gameController.getNextPlayerSkiplevel() == 3) { // previous player played "skip" card + JOptionPane.showMessageDialog(window,"You can't Play any cards because you are being skipped.","Illegal Action", JOptionPane.INFORMATION_MESSAGE); + clearSelectedCards(); return; + } + if (selectedCards.size() == 0 || selectedCards.size() > 2) { // card chosen == 0 or > 2 + JOptionPane.showMessageDialog(window,"Sorry, your play is illegal","No/Too many Selected", JOptionPane.INFORMATION_MESSAGE); + clearSelectedCards(); return; + } + if (selectedCards.size() == 1) { // 1 card chosen + int cardID = selectedCards.get(0); + if (!gameController.isChosenCardLegalCaseOneCard(currentPlayer, cardID)) { + JOptionPane.showMessageDialog(window,"Sorry, your play is illegal","Illegal Play", JOptionPane.INFORMATION_MESSAGE); + clearSelectedCards(); return; + } + } else { // 2 cards chosen + int cardID1 = selectedCards.get(0); + int cardID2 = selectedCards.get(1); + if (!gameController.isChosenCardLegalCaseTwoCards(currentPlayer, cardID1, cardID2)) { + JOptionPane.showMessageDialog(window,"Sorry, your play is illegal","Illegal Play", JOptionPane.INFORMATION_MESSAGE); + clearSelectedCards(); return; + } + } + gameController.setUserAction(1); // the play was successful if the function arrives here + } } - public static void main(String[] args) { - Game game = new Game(2); - Random rand = new Random(); - int randomDraw = rand.nextInt(30) + 1; // randomly draw 1 ~ 30 cards - Player player0 = game.getPlayers().get(0); - Player player1 = game.getPlayers().get(1); - player0.drawCards(randomDraw); - player1.drawCards(31 - randomDraw); -// System.out.println(player0.getCards().size()); - new GameStagePage(game); + private class cardClickedListener implements ActionListener { + private ArrayList<JButton> cardImgs; // the list of card buttons + private JButton cardImg; // the button of a card + public cardClickedListener(ArrayList<JButton> cardButtons, JButton cardButton) { + cardImgs = cardButtons; + cardImg = cardButton; + } + + @Override + public void actionPerformed(ActionEvent e) { + int buttonIndex = cardImgs.indexOf(cardImg); + if (!cardClickStatus.get(buttonIndex)) { // currently the card is not clicked + cardClickStatus.set(buttonIndex, true); // set as clicked + cardImg.setBorder(BorderFactory.createLineBorder(Color.GREEN, 6)); // set the border as green + } else { + /* if the card has already been clicked + / un-click the card, and cancel the border*/ + cardClickStatus.set(buttonIndex, false); // set as clicked + cardImg.setBorder(null); // set the border as green + } + } } } + + + + diff --git a/src/GUI/PlayerNumPage.java b/src/GUI/PlayerNumPage.java index 29fac5f0973870f7fd76a320ba926bb8afccc187..5659dfdbfdc9e0461f6c7f1e1671686db1b2d2e6 100644 --- a/src/GUI/PlayerNumPage.java +++ b/src/GUI/PlayerNumPage.java @@ -1,18 +1,32 @@ package GUI; +import UNO.Game; import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.EventListener; import javax.swing.*; +import javax.swing.undo.AbstractUndoableEdit; /** + * !!!!!!!!!!!!!!!!!!!!!! Viewer IN MVC !!!!!!!!!!!!!!!!!!!!!!! * Page immediately follows welcome page. * Prompt users to declare number of players for a game. + * + * MVC Design: This class won't employ any interface provided by Model (Player & ruleController). + * It will only interact with Game (Controller). */ public class PlayerNumPage extends Frames { private final int W = 1080; // Width private final int H = 720; // Height - public PlayerNumPage() { + private JTextField inputBoxHuman; + private JTextField inputBoxAI; + private final Game gameController; + private final JFrame window; - JFrame window = new JFrame("UNO - Player Number"); + public PlayerNumPage(Game game) { + gameController = game; + window = new JFrame("UNO - Player Number"); addButtons(window); addPrompts(window); addBackground(window, W, H); @@ -22,28 +36,84 @@ public class PlayerNumPage extends Frames { window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } + /** + * All magic numbers are manually adjustable positionable information. + * They are targeted for visual-understanding. + * + * Add start & exit button into the page. + * @param window JFrame containing the buttons + */ public void addButtons(JFrame window) { // start button String startButtonPath = "./src/GUI/assets/startButton.png"; - createImageButtonAndAdd(window, startButtonPath, (int) (W / 2 - W * 0.1), (int) (H * 0.55), W / 5, H / 10); + JButton startButton = createImageButton(startButtonPath, (int) (W / 2 - W * 0.1), (int) (H * 0.55), W / 5, H / 10); + startButton.addActionListener(new startButtonEventListener(window)); + window.add(startButton); // exit button String exitButtonPath = "./src/GUI/assets/exitButton.png"; - createImageButtonAndAdd(window, exitButtonPath,(int) (W / 2 - W * 0.1), (int) (H * 0.55 + H * 0.15), W / 5, H / 10); + JButton exitButton = createImageButton(exitButtonPath, (int) (W / 2 - W * 0.1), (int) (H * 0.55 + H * 0.15), W / 5, H / 10); + exitButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + System.exit(0); + } + }); + window.add(exitButton); } + /** + * All magic numbers are manually adjustable positionable information. + * They are targeted for visual-understanding. + * + * Add two input boxes (for setting human & AI numbers), and corresponding prompts to the window + * @param window JFrame containing the prompts + */ public void addPrompts(JFrame window) { + //Prompt for human player: + JLabel promptHuman = createLabel("Number of human players:", (int) (W * 0.45), (int) (H * 0.15), 22); + window.add(promptHuman); // input box - Font font1 = new Font("SansSerif", Font.BOLD, 20); - JTextField inputBox = new JTextField("Please input number of players (supported for 2 ~ 9 players)..."); - inputBox.setFont(font1); - inputBox.setBounds((int) (W / 2 - W * 0.375), (int) (H * 0.25), (int) (W * 0.75), W / 10); - window.add(inputBox); + Font font = new Font("SansSerif", Font.BOLD, 22); + inputBoxHuman = new JTextField("0"); + inputBoxHuman.setFont(font); + inputBoxHuman.setBounds((int) (W * 0.615), (int) (H * 0.12), (int) (W / 20), (int) (W / 20)); + window.add(inputBoxHuman); + // Prompt for AI player: + JLabel promptAI = createLabel("Number of AI players:", (int) (W * 0.475), (int) (H * 0.3), 22); + window.add(promptAI); + inputBoxAI = new JTextField("0"); + inputBoxAI.setFont(font); + inputBoxAI.setBounds((int) (W * 0.615), (int) (H * 0.27), (int) (W / 20), (int) (W / 20)); + window.add(inputBoxAI); } + private class startButtonEventListener implements ActionListener { + private JFrame window; + public startButtonEventListener(JFrame frame) { + window = frame; + } - public static void main(String[] args) { - new PlayerNumPage(); + @Override + public void actionPerformed(ActionEvent e) { + int humanNum, AINum; + try { + humanNum = Integer.parseInt(inputBoxHuman.getText()); + AINum = Integer.parseInt(inputBoxAI.getText()); + if ( humanNum + AINum < 2 || humanNum + AINum >= 10) throw new ArithmeticException(); + window.dispose(); + gameController.setPlayerNumbers(humanNum, AINum); + gameController.setSetupDone(); + + } catch (ArithmeticException e1) { + JOptionPane.showMessageDialog(window,"Total player numbers should be more than 1 and less than 10.", + "Illegal Players Numbers", JOptionPane.INFORMATION_MESSAGE); + + } catch (Exception e2) { + JOptionPane.showMessageDialog(window, "Please input [0-9] in the input boxes.", + "Illegal Input", JOptionPane.INFORMATION_MESSAGE); + } + } } } diff --git a/src/GUI/PlayerWinPage.java b/src/GUI/PlayerWinPage.java index 0e3bab63788fcd42c179b1fbc970b249ab916fad..78e2c79dab908508fd85a1dd8be8902860fceea0 100644 --- a/src/GUI/PlayerWinPage.java +++ b/src/GUI/PlayerWinPage.java @@ -1,18 +1,30 @@ package GUI; +import UNO.Game; + import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; /** - * Frame for ending scene where a winner has been decided. + * !!!!!!!!!!!!!!!!!!!!!! Viewer IN MVC !!!!!!!!!!!!!!!!!!!!!!! + * Viewer for ending scene where a winner has been decided. + * + * MVC Design: This class won't employ any interface provided by Model (Player & ruleController). + * It will only interact with Game (Controller). */ public class PlayerWinPage extends Frames { - private final int W = 1080; // Width - private final int H = 720; // Height - private int winnerID; - - public PlayerWinPage() { - winnerID = 0; // should be the correct winner passed by game controller! + private final int W = 1080; // Width of window + private final int H = 720; // Height of window + private final int winnerID; + Game gameController; + /** + * @param game game Controller (Controller in MVC) + */ + public PlayerWinPage(Game game) { + gameController = game; + winnerID = game.getWinnerID(); // should be the correct winner passed by game controller! JFrame window = new JFrame("UNO - Player Won"); addButtons(window); addPrompts(window); @@ -24,25 +36,44 @@ public class PlayerWinPage extends Frames { } + /** + * All magic numbers are manually adjustable positionable information. + * They are targeted for visual-understanding. + * + * Add "new game" button to the page + * @param window JFrame containing the buttons + */ public void addButtons(JFrame window) { // new start button String startButtonPath = "./src/GUI/assets/newGameButton.png"; - createImageButtonAndAdd(window, startButtonPath, (int) (W / 2 - W * 0.1), (int) (H * 0.55), W / 5, H / 10); + JButton startButton = createImageButton(startButtonPath, (int) (W / 2 - W * 0.1), (int) (H * 0.55), (int) (W / 5), (int)(H / 10)); + startButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + gameController.setStartNewGame(); + window.dispose(); + } + }); + window.add(startButton); + + } + /** + * All magic numbers are manually adjustable positionable information. + * They are targeted for visual-understanding. + * + * Add two lines for winner-prompt into the page. + * @param window JFrame containing the prompts + */ public void addPrompts(JFrame window) { // Prompt - String promptWord = "Player " + winnerID + " wins!"; + String AIMark = gameController.getPlayers().get(winnerID).isHuman() ? "" : " (AI)"; + String promptWord = "Player " + winnerID + AIMark + " wins!"; JLabel prompt1 = createLabel(promptWord, (int) (W * 0.5), (int) (H * 0.25), 30); JLabel prompt2 = createLabel("Congratulations!!!", (int) (W * 0.5), (int) (H * 0.35), 30); window.add(prompt1); window.add(prompt2); - - - } - - public static void main(String[] args) { - new PlayerWinPage(); } } diff --git a/src/GUI/WelcomePage.java b/src/GUI/WelcomePage.java index 7e3014a24a2fd326f9c88e1baf5f5e01c16b3c22..13107248778eb80588eb2fb0f865eb0ccef733e3 100644 --- a/src/GUI/WelcomePage.java +++ b/src/GUI/WelcomePage.java @@ -1,14 +1,27 @@ package GUI; import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import UNO.*; /** - * Welcome page for UNO. + * !!!!!!!!!!!!!!!!!!!!!! Viewer IN MVC !!!!!!!!!!!!!!!!!!!!!!! + * Welcome page for UNO (Viewer). + * + * MVC Design: This class won't employ any interface provided by Model (Player & ruleController). + * It will only interact with Game (Controller). */ public class WelcomePage extends Frames { - private final int W = 1080; // Width - private final int H = 720; // Height - public WelcomePage() { + private final int W = 1080; // Width of window + private final int H = 720; // Height of window + private Game gameController; // Game Controller (Controller in MVC) + + /** + * @param game the game controller in the MVC + */ + public WelcomePage(Game game) { + gameController = game; JFrame window = new JFrame("UNO - Welcome"); addPrompts(window); addButtons(window); @@ -19,16 +32,45 @@ public class WelcomePage extends Frames { window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } + /** + * All magic numbers are manually adjustable positionable information. + * They are targeted for visual-understanding. + * + * Add welcome, exit buttons to the page. + * @param window JFrame containing the buttons + */ public void addButtons(JFrame window) { // start button String startButtonPath = "./src/GUI/assets/startButton.png"; - createImageButtonAndAdd(window, startButtonPath, (int) (W / 2 - W * 0.1), (int) (H * 0.55), W / 5, H / 10); + JButton startButton = createImageButton(startButtonPath, (int) (W / 2 - W * 0.1), (int) (H * 0.55), W / 5, H / 10); + startButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + new PlayerNumPage(gameController); + window.dispose(); + } + }); + window.add(startButton); // exit button String exitButtonPath = "./src/GUI/assets/exitButton.png"; - createImageButtonAndAdd(window, exitButtonPath, (int) (W / 2 - W * 0.1), (int) (H * 0.55 + H * 0.15), W / 5, H / 10); + JButton exitButton = createImageButton(exitButtonPath, (int) (W / 2 - W * 0.1), (int) (H * 0.55 + H * 0.15), W / 5, H / 10); + exitButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + System.exit(0); + } + }); + window.add(exitButton); } + /** + * All magic numbers are manually adjustable positionable information. + * They are targeted for visual-understanding. + * + * Add UNO logo to the page. + * @param window JFrame containing the prompts + */ public void addPrompts(JFrame window) { // logo String logoPath = "./src/GUI/assets/logo.png"; @@ -37,9 +79,6 @@ public class WelcomePage extends Frames { window.add(logo); } - public static void main(String[] args) { - new WelcomePage(); - } } diff --git a/src/Test/AIPlayerTest.java b/src/Test/AIPlayerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..444a63dd8d9d4db52815a1984d4e57c7baffd640 --- /dev/null +++ b/src/Test/AIPlayerTest.java @@ -0,0 +1,108 @@ +import UNO.*; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +public class AIPlayerTest { + + /** + * Test whether game could initialized properly with only AI + */ + @Test + public void testAIInitialization() { + Game game = new Game(0, 9); + game.initializeGame(); + for (Player player : game.getPlayers()) { + assert(player.getCards().size() == 7); + assert(!player.isHuman()); + } + } + + + /** + * Test some basic heuristics of strategic AI to guarantee normal case functionality. + */ + public void AIScenarioTest() { + Game game = new Game(0, 0); + game.setGapTime(0); + game.setUseGUI(false); + game.setManualSetup(true); + + ArrayList<Player> players = new ArrayList<>(); + AIPlayer player1 = new ArtificialIntelligence(0, game); + players.add(player1); + AIPlayer player2 = new ArtificialIntelligence(1, game); + players.add(player2); + + game.setPlayerNumbers(0,2); + game.initializeGame(); + + // suppose an AI got lots of red cards (red 1-7), and few blue number cards (blue 1-3). + player1.getCards().clear(); + ArrayList<Integer> newCards1 = new ArrayList<>(Arrays.asList(1,2,3,4,5,6,7,51,52,53)); + player1.getCards().addAll(newCards1); + assert(player1.pickColor().equals("red")); // when wild card is available AI should pick red + assert(player1.playCard() <= 7); // the player won't prioritize color he own less. + + // suppose an AI got both red number cards (red 1-7), and red symbol cards (21-24); + player1.getCards().clear(); + ArrayList<Integer> newCards2 = new ArrayList<>(Arrays.asList(1,2,3,4,5,6,7,21,22,23,24)); + player1.getCards().addAll(newCards2); + assert(player1.playCard() <= 7); // AI shouldn't prioritize symbol cards than number cards. + + // Suppose an AI got lots of symbol cards (<100 below), and some wild cards (>100), he should first play symbol cards. + player1.getCards().clear(); + ArrayList<Integer> newCards3 = new ArrayList<>(Arrays.asList(21, 24, 47, 49, 74, 99, 101, 104, 107)); + player1.getCards().addAll(newCards3); + assert(player1.playCard() <= 100); // AI shouldn't prioritize wild cards than symbol cards. + } + + /** + * Evaluate the quality of strategy through large-data test. + * This is what is known as the "downstream oriented evaluation" in machine learning. + * when totalRounds set to 100000, the strategic AI has winning rate of 58%, + * which is enough to reject the null hypothesis. + * Notice 100000 rounds will be enough to capture almost any corner cases so that + * test on component functions won't be necessary. + * @throws InterruptedException + */ + @Test + public void testStrategy() throws InterruptedException { + float totalRounds = 100000; + float intelligenceWin = 0; + float idiotWin = 0; + + for (int i = 0; i < totalRounds; i++) { + Game game = new Game(0, 0); + game.setGapTime(0); + game.setUseGUI(false); + game.setManualSetup(true); + + ArrayList<Player> players = new ArrayList<>(); + Player player1 = new ArtificialIntelligence(0, game); + players.add(player1); + Player player2 = new ArtificialIntelligence(1, game); + players.add(player2); + Player player3 = new ArtificialIdiot(2, game); + players.add(player3); + Player player4 = new ArtificialIdiot(3, game); + players.add(player4); + game.setPlayers(players); + + game.setPlayerNumbers(0,4); + game.initializeGame(); + game.gameStart(); + int winnerID = game.getWinnerID(); + if (winnerID == 0 || winnerID == 1) intelligenceWin += 1; + else idiotWin += 1; + } + + assert(intelligenceWin + idiotWin == totalRounds); // make sure not even a single round failed + System.out.println("Artificial intelligence winning rate: " + (intelligenceWin / totalRounds) * 100 + "%"); + System.out.println("Artificial idiot winning rate: " + (idiotWin / totalRounds) * 100 + "%"); + assert( (intelligenceWin / totalRounds) * 100 > 50); + } +} diff --git a/src/Test/PlayerTest.java b/src/Test/PlayerTest.java index 572327b32d76e32196c8e5684b9d466edcda812f..a5df98014e10c87e9e69ad009d9ef42a613dd214 100644 --- a/src/Test/PlayerTest.java +++ b/src/Test/PlayerTest.java @@ -1,9 +1,7 @@ import org.junit.jupiter.api.Test; import java.util.ArrayList; -import java.util.Random; -import static org.junit.jupiter.api.Assertions.*; import UNO.*; /** @@ -27,7 +25,8 @@ class PlayerTest { */ void TestUserPlayWillGoToDiscardPile() { for (int i = 0; i < 100; i++) { - Game game = new Game(1); + Game game = new Game(1,0); + game.initializeGame(); Player player = game.getPlayers().get(0); player.drawCards(50); @@ -35,7 +34,7 @@ class PlayerTest { int cardToPlay = legalCards.get(0); // make sure this play is legal - assert (player.optionPlayOwnedCard(cardToPlay)); + assert (player.optionPlayOwnedCard(cardToPlay, true)); // make sure the top of discard of pile is the card just played assert (game.getCardManager().getDiscardPile().get(0) == cardToPlay); @@ -54,7 +53,8 @@ class PlayerTest { @Test void testOptionDrawAndPlay() { for (int i = 0; i < 100; i++) { - Game game = new Game(1); + Game game = new Game(1,0); + game.initializeGame(); Player player = game.getPlayers().get(0); int cardPlayerWillGet = game.getCardManager().getCardPile().get(0); boolean newCardIsValid = game.getRuler().isValidPlay(player, cardPlayerWillGet, false); @@ -74,21 +74,9 @@ class PlayerTest { } - @Test - /** - * Test 3 - * Test game will end when a user wins - */ - void testGameCloseWhenPlayerWin() { - Game game = new Game(1); - game.gameStart(); - assert(game.getRounds() == 7); - } - - /** - * Test 4 + * Test 3 * Test when a player plays a wild card and declare the matchable color as "red", * the game state (controlled by ruleController) is properly updated. * Notice the declaration is hard-coded in stage assignment-1.0 @@ -100,7 +88,8 @@ class PlayerTest { int wildCardID = -1; boolean hasWild = false; while (!hasWild) { - game = new Game(1); + game = new Game(1,0); + game.initializeGame(); player = game.getPlayers().get(0); for (int cardID : player.getCards()) { if (cardID >= 101 && cardID <= 104) { @@ -110,71 +99,74 @@ class PlayerTest { } } } - player.optionPlayOwnedCard(wildCardID); + player.optionPlayOwnedCard(wildCardID, true); assert(game.getRuler().getMatchableColor().equals("red")); } /** - * Test 5 + * Test 4 * Test extra addition rule required in assignment-1.1 */ @Test void testAdditionRule() { - Game game = new Game(1); + Game game = new Game(1,0); + game.initializeGame(); Player player = game.getPlayers().get(0); player.drawCards(player.getGameController().getCardManager().numCardLeft()); // get all left cards setCurrentState(game.getRuler(), "red", "none", "8",0); - assert(player.optionPlayTwoOwnedCard_Add(52, 56)); // blue 2 + blue 6 should succeed + assert(player.optionPlayTwoOwnedCard_Add(52, 56, true)); // blue 2 + blue 6 should succeed // check state updated correctly assert(checkStateUpdatedCorrectly(game.getRuler(), "blue", "none", "8", 0)); setCurrentState(game.getRuler(), "red", "none", "8",0); - assert(!player.optionPlayTwoOwnedCard_Add(61, 54)); // blue 2 + blue 4 should fail + assert(!player.optionPlayTwoOwnedCard_Add(61, 54, true)); // blue 2 + blue 4 should fail setCurrentState(game.getRuler(), "red", "none", "8",0); - assert(!player.optionPlayTwoOwnedCard_Add(79, 84)); // yellow 4 + yellow 9 should fail + assert(!player.optionPlayTwoOwnedCard_Add(79, 84, true)); // yellow 4 + yellow 9 should fail setCurrentState(game.getRuler(), "red", "none", "8",0); - assert(!player.optionPlayTwoOwnedCard_Add(76, 57)); // yellow 1 + blue 7 should fail + assert(!player.optionPlayTwoOwnedCard_Add(76, 57, true)); // yellow 1 + blue 7 should fail setCurrentState(game.getRuler(), "red", "none", "8",0); - assert(!player.optionPlayTwoOwnedCard_Add(71, 74)); // two non-number card should fail + assert(!player.optionPlayTwoOwnedCard_Add(71, 74, true)); // two non-number card should fail setCurrentState(game.getRuler(), "red", "none", "8",0); - assert(!player.optionPlayTwoOwnedCard_Add(101, 106)); // pairs containing wild card should fail + assert(!player.optionPlayTwoOwnedCard_Add(101, 106, true)); // pairs containing wild card should fail } /** - * Test 6 + * Test 5 * Test extra subtraction rule required in assignment-1.1 * notice order of subtraction parameters does not matter */ @Test void testSubtractionRule() { - Game game = new Game(1); + Game game = new Game(1,0); + game.initializeGame(); Player player = game.getPlayers().get(0); player.drawCards(player.getGameController().getCardManager().numCardLeft()); // get all left cards setCurrentState(game.getRuler(), "red", "none", "3",0); - assert(player.optionPlayTwoOwnedCard_Sub(53, 56)); // blue 3 & blue 6 should succeed + assert(player.optionPlayTwoOwnedCard_Sub(53, 56, true)); // blue 3 & blue 6 should succeed // check state updated correctly assert(checkStateUpdatedCorrectly(game.getRuler(), "blue", "none", "3", 0)); setCurrentState(game.getRuler(), "red", "none", "3",0); - assert(!player.optionPlayTwoOwnedCard_Sub(61, 54)); // blue 2 & blue 4 should fail + assert(!player.optionPlayTwoOwnedCard_Sub(61, 54, true)); // blue 2 & blue 4 should fail setCurrentState(game.getRuler(), "red", "none", "3",0); - assert(!player.optionPlayTwoOwnedCard_Add(76, 54)); // yellow 1 & blue 4 should fail + assert(!player.optionPlayTwoOwnedCard_Add(76, 54, true)); // yellow 1 & blue 4 should fail setCurrentState(game.getRuler(), "red", "none", "3",0); - assert(!player.optionPlayTwoOwnedCard_Sub(71, 74)); // two non-number card should fail + assert(!player.optionPlayTwoOwnedCard_Sub(71, 74, true)); // two non-number card should fail setCurrentState(game.getRuler(), "red", "none", "3",0); - assert(!player.optionPlayTwoOwnedCard_Add(101, 106)); // pairs containing wild card should fail + assert(!player.optionPlayTwoOwnedCard_Add(101, 106, true)); // pairs containing wild card should fail + + setCurrentState(game.getRuler(), "blue", "none", "1",0); + assert(player.optionPlayTwoOwnedCard_Sub(59, 58, true)); // blue 9 & blue 8 should succeed - GUI gui = new GUI(player); - assert(gui.getPlayer() != null); } diff --git a/src/Test/RuleControllerTest.java b/src/Test/RuleControllerTest.java index eaa4fc21667e0817646ade205b13c4aa201082f0..7cbc9c2c9ec3e4db5908a390db8ab832cb682459 100644 --- a/src/Test/RuleControllerTest.java +++ b/src/Test/RuleControllerTest.java @@ -1,3 +1,4 @@ +import GUI.GameStagePage; import com.sun.source.tree.UsesTree; import org.junit.Rule; import org.junit.jupiter.api.Test; @@ -324,6 +325,14 @@ class RuleControllerTest { assert(checkStateUpdatedCorrectly(ruler, "NA", "none", "none", 2)); assert(ruler.getPenaltyDraw() == 8); + // previous play red draw 2 -- penalty draw should be 4 after blue skip 2 played + ruler.resetPenaltyDraw(); + setCurrentState(ruler, "red", "draw2", "none", 1); + ruler.increasePenaltyDraw(2); + ruler.isValidPlay(player, 48, true); // green draw 2 + assert(checkStateUpdatedCorrectly(ruler, "green", "draw2", "none", 1)); + assert(ruler.getPenaltyDraw() == 4); + } @@ -360,7 +369,8 @@ class RuleControllerTest { */ @Test void testCheckSkipAndDraw() { - Game game = new Game(1); + Game game = new Game(1,0); + game.initializeGame(); Player player = player_AllColor_Y_WildDraw4_Y(); // he cannot play wildDraw4 anyway player.setPlayerID(2); player.setGameController(game); @@ -371,7 +381,7 @@ class RuleControllerTest { int pileNumCheckPoint1 = game.getCardManager().numCardLeft(); setCurrentState(game.getRuler(), "red", "skip", "none", 3); game.getNextPlayerID(); - assert(game.getRuler().checkSkipAndDraw(player)); + assert(game.getRuler().checkSkipAndDraw(player, true)); assert(player.getCards().size() == playerNumCheckPoint1); assert(game.getCardManager().numCardLeft() == pileNumCheckPoint1); @@ -380,7 +390,7 @@ class RuleControllerTest { int pileNumCheckPoint2 = game.getCardManager().numCardLeft(); setCurrentState(game.getRuler(), "blue", "draw2", "none", 1); game.getRuler().increasePenaltyDraw(2); - assert(game.getRuler().checkSkipAndDraw(player)); + assert(game.getRuler().checkSkipAndDraw(player, true)); assert(player.getCards().size() == playerNumCheckPoint2 + 2); assert(game.getCardManager().numCardLeft() == pileNumCheckPoint2 - 2); game.getRuler().resetPenaltyDraw(); @@ -391,13 +401,60 @@ class RuleControllerTest { int pileNumCheckPoint3 = game.getCardManager().numCardLeft(); setCurrentState(game.getRuler(), "blue", "draw2", "none", 2); game.getRuler().increasePenaltyDraw(4); - assert(game.getRuler().checkSkipAndDraw(player)); + assert(game.getRuler().checkSkipAndDraw(player, true)); assert(player.getCards().size() == playerNumCheckPoint3 + 4); assert (game.getCardManager().numCardLeft() == pileNumCheckPoint3 - 4); game.getRuler().resetPenaltyDraw(); } + /** + * 14. Miscellaneous test cases (getters/setters) for Game Controller for coverage concerns... + * It also test the whether game could handle AI behaviors without any bugs. + */ + @Test + public void testGameControlGeneralCaseAI() throws InterruptedException { + Game game = new Game(0,0); + RuleController ruler = game.getRuler(); + game.setPlayerNumbers(0,2); + game.setStartNewGame(); + assert(game.toStartNewGame()); + + game.setGapTime(10); + game.setUseGUI(false); + game.initializeGame(); + game.setManualSetup(false); // we do not want to initialize players manually + game.setSetupDone(); + + // test whether the game has been set up as expected + assert(game.isSetupDone()); + assert(game.getPlayers().size() == 2); + assert(game.getRounds() == 1); + assert(ruler.getPenaltyDraw() == 0); + assert(game.getPlayerCardNumber(0) == 7); + assert(!ruler.getPreviousCard().equals("none")); + assert(ruler.getPenaltyDraw() == 0); + + ruler.updateGameStateBasedOnPickedColor("red"); + assert(ruler.getPreviousCard().startsWith("red")); + int currentPlayerID = game.getCurrentPlayerID(); + Player currentPlayer = game.getPlayers().get(currentPlayerID); + assert(game.getNextPlayerID() == (currentPlayerID + 1) % 2); + game.handleAIBehavior((AIPlayer) currentPlayer); + } + + /** + * 15. Test whether game controller could hanel human behavior without any bugs + */ + @Test + public void testGameControlCaseHuman() throws InterruptedException { + Game game = new Game(0,0); + game.setPlayerNumbers(2,0); + game.initializeGame(); + int currentPlayerID = game.getCurrentPlayerID(); + Player currentPlayer = game.getPlayers().get(currentPlayerID); + game.handleHumanBehavior(currentPlayer); + } /** @@ -471,10 +528,10 @@ class RuleControllerTest { /** * construct a player who has no red cards, but a wildDraw4 card * This player should be able to play wildDraw4 card if current allowed color is red - * @return + * @return A player with no red cards but a wildDraw4 card. */ private Player player_Red_N_WildDraw4_Y() { - Player player = new Player(0, null, true); + Player player = new Player(0, null); player.addOneCard(26); // green 1 player.addOneCard(51); // blue 1 player.addOneCard(76); // yellow 1 @@ -489,7 +546,7 @@ class RuleControllerTest { * @return */ private Player player_AllColor_Y_WildDraw4_Y() { - Player player = new Player(0, null, true); + Player player = new Player(0, null); player.addOneCard(1); // red 1 player.addOneCard(26); // green 1 player.addOneCard(51); // blue 1 @@ -504,7 +561,7 @@ class RuleControllerTest { * This player should not be able to play wildDraw4 card */ private Player playerAllCards() { - Player player = new Player(0, null, true); + Player player = new Player(0, null); for (int i = 1; i <= 108; i++) { player.addOneCard(i); } diff --git a/src/Test/cardDealTest.java b/src/Test/cardManagerTest.java similarity index 87% rename from src/Test/cardDealTest.java rename to src/Test/cardManagerTest.java index e740fb7b4c04e1bc95c73491c20d1cdcac518abf..92777406602de7596dd001eacceebabe0806e1f6 100644 --- a/src/Test/cardDealTest.java +++ b/src/Test/cardManagerTest.java @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; * 2. Interaction between Game and CardManager * 3. Player.{drawCards, playOneRound} */ -class cardDealTest { +class cardManagerTest { @Test /** * Test 0 @@ -50,19 +50,23 @@ class cardDealTest { */ void testInitialDeal() throws Exception { - Game game1 = new Game(1); + Game game1 = new Game(1,0); + game1.initializeGame(); for (Player p : game1.getPlayers()) { assert(p.getCards().size() == 7); } assert(game1.getCardManager().numCardLeft() == 101); // 108 - 7 - Game game2 = new Game(5); + Game game2 = new Game(5,0); + game2.initializeGame(); for (Player p : game2.getPlayers()) { assert(p.getCards().size() == 7); } + System.out.println(game2.getCardManager().numCardLeft()); assert(game2.getCardManager().numCardLeft() == 73); // 108 - 35 - Game game3 = new Game(9); + Game game3 = new Game(9, 0); + game3.initializeGame(); for (Player p : game3.getPlayers()) { assert(p.getCards().size() == 7); } @@ -78,7 +82,8 @@ class cardDealTest { * case 1: completely draw from discard pile */ void testDrawFromDiscardWhenDrawNotEnough_Case1() throws Exception { - Game game = new Game(1); + Game game = new Game(1,0); + game.initializeGame(); Player player = game.getPlayers().get(0); ArrayList<Integer> cards = player.getCards(); // direct reference to player's card player.drawCards(game.getCardManager().numCardLeft()); // get all cards in draw pile @@ -105,7 +110,8 @@ class cardDealTest { * case 2: partially from draw pile, partially from discard pile */ void testDrawFromDiscardWhenDrawNotEnough_Case2() throws Exception{ - Game game = new Game(1); + Game game = new Game(1,0); + game.initializeGame(); Player player = game.getPlayers().get(0); ArrayList<Integer> cards = player.getCards(); // direct reference to player's card player.drawCards(game.getCardManager().numCardLeft() - 2); // get 99 cards from draw pile diff --git a/src/UNO/AIPlayer.java b/src/UNO/AIPlayer.java new file mode 100644 index 0000000000000000000000000000000000000000..00c9f1d8786f40ac7e126ee634ddd413055c790b --- /dev/null +++ b/src/UNO/AIPlayer.java @@ -0,0 +1,70 @@ +package UNO; + +public abstract class AIPlayer extends Player { + /** + * Constructor for player objects + * + * @param ID An unique integer identifier for a player + * @param game An game controller object representing the game player being in + */ + public AIPlayer(int ID, Game game) { + super(ID, game); + + } + + /** + * AI's behavior of picking color. Any AI classes should implement this method. + * @return the color (string) he picks. + */ + public abstract String pickColor(); + + /** + * AI's behavior of deciding cards to play. Any AI classes should implement this method. + * @return the card (as ID) he picks. + */ + public abstract int playCard(); + + /** + * AI's behavior of picking actions. Any AI class should implement this method. + * @return the action (int) he picks. + */ + public abstract int makeActionDecision(); + + /** + * Convert color (ID) to Color (String)). + * @param colorID 0, 1, 2, 3, 4 + * @return color "NA", "red", "green", "blue", or "yellow" + */ + public String colorID2color(int colorID) { + return switch (colorID) { + case 0 -> "NA"; + case 1 -> "red"; + case 2 -> "green"; + case 3 -> "blue"; + case 4 -> "yellow"; + default -> null; // should not be reached + }; + } + + /** + * Convert color (String) to Color ID. + * @param color "NA", "red", "green", "blue", or "yellow" + * @return colorID 0, 1, 2, 3, 4 + */ + public int color2colorID(String color) { + return switch (color) { + case "NA" -> 0; + case "red" -> 1; + case "green" -> 2; + case "blue" -> 3; + case "yellow" -> 4; + default -> -1; // should not be reached + }; + } + + @Override + public boolean isHuman() { + return false; + } + +} diff --git a/src/UNO/ArtificialIdiot.java b/src/UNO/ArtificialIdiot.java new file mode 100644 index 0000000000000000000000000000000000000000..edb61687784e4b4e2142284029aac65286a1ab4e --- /dev/null +++ b/src/UNO/ArtificialIdiot.java @@ -0,0 +1,61 @@ +package UNO; + +import javax.print.attribute.standard.Finishings; +import java.util.*; + +public class ArtificialIdiot extends AIPlayer { + + private ArrayList<Integer> legalCards; + /** + * Constructor for player objects + * + * @param ID An unique integer identifier for a player + * @param game An game controller object representing the game player being in + */ + public ArtificialIdiot(int ID, Game game) { + super(ID, game); + } + + /** + * This is how AI makes its action decision. This decision should always be legal. + * @return 1 ("PlayOwned"), 2 ("Draw&Play"), 3 ("SKip") + */ + public int makeActionDecision() { + int currentSkipLevel = getGameController().getRuler().getNextPlayerSkiplevel(); // whether previous play is draw2, wild4, or skip + if (currentSkipLevel == 3) { // "skip card" + return 3; // skip his turn + } + legalCards = findLegalCard(); + if (currentSkipLevel != 0 && legalCards.size() == 0) { + // there's pending stacked draw, and AI don't have legal card to play + return 3; // skip his turn and takes stacked penalty + } + if (legalCards.size() == 0) { + // there's no pending stacked draw, and AI don't have legal card to play + return 2; // draw and play + } + return 1; // play owned cards + } + + + /** + * Randomly choose a card to play. Notice AI will only make this decision when it has legal cards. + * @return The card (as ID) that AI decided to play + */ + public int playCard() { + Random rand = new Random(); + int randIndex = rand.nextInt(legalCards.size()); + return legalCards.get(randIndex); + } + + /** + * AI will its preferred color when played a wild cards up calling this function. + * @return Color picked by AI + */ + public String pickColor() { + ArrayList<Integer> colors = new ArrayList<Integer>(Arrays.asList(1,2,3,4)); + Collections.shuffle(colors); + return colorID2color(colors.get(0)); + } + +} diff --git a/src/UNO/ArtificialIntelligence.java b/src/UNO/ArtificialIntelligence.java new file mode 100644 index 0000000000000000000000000000000000000000..5560dbdcd85acfec303092c4879bce5d5ea66138 --- /dev/null +++ b/src/UNO/ArtificialIntelligence.java @@ -0,0 +1,212 @@ +package UNO; + +import javax.print.attribute.standard.Finishings; +import java.util.*; + +public class ArtificialIntelligence extends AIPlayer { + + private ArrayList<Integer> legalCards; + + /** + * Constructor for player objects + * + * @param ID An unique integer identifier for a player + * @param game An game controller object representing the game player being in + */ + public ArtificialIntelligence(int ID, Game game) { + super(ID, game); + } + + /** + * This is how AI makes its action decision. This decision should always be legal. + * @return 1 ("PlayOwned"), 2 ("Draw&Play"), 3 ("SKip") + */ + public int makeActionDecision() { + int currentSkipLevel = getGameController().getRuler().getNextPlayerSkiplevel(); + if (currentSkipLevel == 3) { + return 3; // skip + } + legalCards = findLegalCard(); + if (currentSkipLevel != 0 && legalCards.size() == 0) { + // there's pending stacked draw, and AI don't have legal card to play + return 3; // skip + } + + if (legalCards.size() == 0) { + // there's no pending stacked draw, and AI don't have legal card to play + return 2; // draw and play + } + return 1; // play owned cards + } + + /** + * Strategically choose a card to play. Notice AI will only make this decision when it has legal cards. + * Also, AI will only play one card at a time. + * @return The card (as ID) that AI decided to play + */ + public int playCard() { + int nextPlayerID = getGameController().getNextPlayerID(); + if (getGameController().getPlayerCardNumber(nextPlayerID) <= 2) { + return findBestCard_CaseNextPlayerLessThan2(); + } else { + return findBestCard_CommonCase(); + } + } + + + /** + * AI will its preferred color when played a wild cards up calling this function. + * @return Color picked by AI + */ + public String pickColor() { + ArrayList<Integer> colorRanks = findBestColors(getCards()); + int bestColorID = colorRanks.get(0); + if (bestColorID == 0) { // AI owns A LOT OF wild cards. Extremely low probability. + return colorID2color(colorRanks.get(1)); + } + return colorID2color(bestColorID); + } + + private int findBestCard_CommonCase() { + ArrayList<Integer> priority = calcPriority_CommonCase(legalCards); + int maxScore = Collections.max(priority); + int maxIndex = priority.indexOf(maxScore); + return legalCards.get(maxIndex); + } + + /** + * Priority Heuristics in common case: + * (1) Worst color number > best color skip / draw2 / reserve. + * (2) Play 0 first if possible. + * (3) Always reserve wild/wildDraw4. + * (4) reserve > skip > draw2 + * @param cards candidate cards. + * @return the priority of each card. + */ + private ArrayList<Integer> calcPriority_CommonCase(ArrayList<Integer> cards) { + ArrayList<Integer> priority = new ArrayList<>(); + ArrayList<Integer> colorRanks = findBestColors(getCards()); // best color in all cards + for (int i = 0; i < cards.size(); i++) { + int cardID = cards.get(i); + String cardDesc = parser.parseCardID(cardID); + String color = parser.parseCardDescription(cardDesc)[0]; + int colorID = color2colorID(color); + int rank = colorRanks.indexOf(colorID); // the ranking of color on the card + switch (rank) { // weighing based on color of card + case 0 -> priority.add(8); // best color + case 1 -> priority.add(6); + case 2 -> priority.add(4); + case 3 -> priority.add(2); + case 4 -> priority.add(1); // worst color + } + String content = parser.parseCardDescription(cardDesc)[2]; + switch (content) { // weighing based on card content + case "skip" -> priority.set(i, priority.get(i) + 3); + case "draw2" -> priority.set(i, priority.get(i) + 2); + case "reverse" -> priority.set(i, priority.get(i) + 4); + case "0" -> priority.set(i, priority.get(i) + 15); + case "wild", "wildDraw4" -> priority.set(i, priority.get(i) + 1); + default -> priority.set(i, priority.get(i) + 12); // 1-9 + } + } + return priority; + } + + + private int findBestCard_CaseNextPlayerLessThan2() { + ArrayList<Integer> priority = calcPriority_CaseNextPlayerLessThan2(legalCards); + int maxScore = Collections.max(priority); + int maxIndex = priority.indexOf(maxScore); + return legalCards.get(maxIndex); + } + + /** + * Priority Heuristics when next player has less than 2 cards left,: + * (1) draw2 > reverse > skip > wildDraw4 > wild > 0 > 1-9 + * (2) worst color draw2/reverse/skip > best number + * + * @param cards candidate cards. + * @return the priority of each card. + */ + + private ArrayList<Integer> calcPriority_CaseNextPlayerLessThan2(ArrayList<Integer> cards) { + ArrayList<Integer> priority = new ArrayList<>(); + ArrayList<Integer> colorRanks = findBestColors(getCards()); // best color in all cards + for (int i = 0; i < cards.size(); i++) { + int cardID = cards.get(i); + String cardDesc = parser.parseCardID(cardID); + String color = parser.parseCardDescription(cardDesc)[0]; + int colorID = color2colorID(color); + int rank = colorRanks.indexOf(colorID); // the ranking of color on the card + switch (rank) { // weighing based on color of card + case 0 -> priority.add(8); // best color + case 1 -> priority.add(6); + case 2 -> priority.add(4); + case 3 -> priority.add(2); + case 4 -> priority.add(1); // worst color + } + String content = parser.parseCardDescription(cardDesc)[2]; + switch (content) { // weighing based on card content + case "skip" -> priority.set(i, priority.get(i) + 3); + case "draw2" -> priority.set(i, priority.get(i) + 15); + case "reverse" -> priority.set(i, priority.get(i) + 12); + case "0" -> priority.set(i, priority.get(i) + 2); + case "wild", "wildDraw4" -> priority.set(i, priority.get(i) + 8); + default -> priority.set(i, priority.get(i) + 1); // 1-9 + } + } + return priority; + } + + + /** + * Find the best color that AI currently own most. + * + * @param cards candidate cards to be considered. + * 0 NA, 1 red , 2 green, 3 blue, 4 yellow + * @return ranking of owned color as ArrayList + */ + + private ArrayList<Integer> findBestColors(ArrayList<Integer> cards) { + ArrayList<Integer> weights = new ArrayList<>(); + for (int i = 0; i < 5; i++) weights.add(0); // 0 0 0 0 0 + + // adding weight by iterating through hand cards + for (Integer card : cards) { + String cardDesc = parser.parseCardID(card); + String color = parser.parseCardDescription(cardDesc)[0]; + String content = parser.parseCardDescription(cardDesc)[2]; + int colorID = color2colorID(color); + switch (content) { // different cards should have different weight contribution + case "skip" -> weights.set(colorID, weights.get(colorID) + 5); + case "draw2" -> weights.set(colorID, weights.get(colorID) + 6); + case "reverse" -> weights.set(colorID, weights.get(colorID) + 4); + case "0" -> weights.set(colorID, weights.get(colorID) + 2); + case "wild", "wildDraw4" -> weights.set(colorID, weights.get(colorID) + 1);//reserve wild cards + default -> weights.set(colorID, weights.get(colorID) + 4); + } + } + return argsort(weights.toArray(), false); + } + + + /** + * Return sorted indices of given arraylist. (descending order) + * e.g. given array [4,1,2,3] -> return [0,3,2,1] + * Notice this version only support Integer! + * Equivalent to argsort in python. + */ + private ArrayList<Integer> argsort(final Object[] arrayToSort, boolean ascending) { + Integer[] indices = new Integer[arrayToSort.length]; + for (int i = 0; i < indices.length; i++) { + indices[i] = i; + } + Arrays.sort(indices, new Comparator<Integer>() { + @Override + public int compare(final Integer i1, final Integer i2) { + return (ascending ? 1 : -1) * Integer.compare((Integer) arrayToSort[i1], (Integer) arrayToSort[i2]); + } + }); + return new ArrayList<>(Arrays.asList(indices)); + } +} \ No newline at end of file diff --git a/src/UNO/CardManager.java b/src/UNO/CardManager.java index d308e133a9bb13b46b9546b4cc26649a18f8d8bd..27136a88cfc8ba1613ecbb9671c533457351718a 100644 --- a/src/UNO/CardManager.java +++ b/src/UNO/CardManager.java @@ -1,4 +1,5 @@ package UNO; + import java.lang.reflect.Array; import java.util.*; diff --git a/src/UNO/CardParser.java b/src/UNO/CardParser.java index 34b126b0644c91aef7ee37bedf2faec9d93e181b..c140e10a9fda2a0e2a3fa137c80c2d04cf1c2e5a 100644 --- a/src/UNO/CardParser.java +++ b/src/UNO/CardParser.java @@ -1,4 +1,5 @@ package UNO; + import java.util.*; /** diff --git a/src/UNO/CmdUI.java b/src/UNO/CmdUI.java deleted file mode 100644 index a209c29084342198c770fc55db9f22fdb801c422..0000000000000000000000000000000000000000 --- a/src/UNO/CmdUI.java +++ /dev/null @@ -1,192 +0,0 @@ -package UNO; -import java.util.Scanner; - - -/** - * This class implements the behavior of terminal UI. - * For debug only. For formal usage, please check package GUI - */ -public class CmdUI { - Player player; - CardParser parser; - public static Game gameController; - public static RuleController ruler; - - public CmdUI(Player p) { - player = p; - gameController = player.getGameController(); - parser = (gameController != null) ? player.parser : null; // the game might be null for testing purpose - ruler = (gameController != null) ? gameController.getRuler() : null; - - } - - /** - * FOR CMD LINE DEBUGGING - * When it's this player's turn, the player must pick one card to play - * @return the ** cardID ** of the chosen card - */ -// public int promptTakeAction() { -// // instruct player to take a move -// Scanner inputScanner = new Scanner(System.in); -// boolean receivedValidInput = false; -// int action = -1; -// while (!receivedValidInput) { -// promptChooseAction(); -// String input = inputScanner.nextLine(); -// try { -// action = Integer.parseInt(input); -// if (action == 1 || action == 2) { -// receivedValidInput = true; -// } -// -// } catch (Exception e) { -// System.out.println("Please choose action from 1 or 2"); -// } -// } -// -// if (action == 1) { -// return actionOne(); -// } else if (action == 2){ -// return actionTwo(); -// } -// -// return -1; // user did not play valid card -// } - -// private int actionOne() { -// -// int cardID = -1; -// while (true) { -// player.printCardsInHand(); -// player.printLegalCards(); -// System.out.println("Which card would you like to play? Please input the corresponding card number..."); -// -// int chosenCardIndex = -1; -// -// while (chosenCardIndex < 0) { -// chosenCardIndex = getInputCardIndex(); -// } -// cardID = player.getCards().get(chosenCardIndex); -// if(gameController.getRuler().isValidPlay(player, cardID, true)) { -// // maintain list-cards and discardPile -// player.getCards().remove(chosenCardIndex); -// gameController.getCardManager().insertOneCardToDiscardPile(cardID); -// break; -// } -// } - -// if (parser.isNonColorCard(cardID)) { -// int colorChosen = -1; -// while (colorChosen < 0) { -// promptChooseColor(); -// colorChosen = getInputColor(); -// } -// ruler.setMatchableColor(parser.colorDict.get(colorChosen)); -// } -// return cardID; -// } - -// private int actionTwo () { -// player.drawCards( 1); // draw new card and add it to cards in hand -// int chosenCardIndex = player.getCards().size() - 1; -// int cardID = player.getCards().get(chosenCardIndex); -// if(ruler.isValidPlay(player, cardID, true)) { -// player.getCards().remove(chosenCardIndex); -// gameController.getCardManager().insertOneCardToDiscardPile(cardID); -// -// if (parser.isNonColorCard(cardID)) { -// // to implement -// int colorChosen = -1; -// while (colorChosen < 0) { -// promptChooseColor(); -// colorChosen = getInputColor(); -// } -// gameController.getRuler().setMatchableColor(parser.colorDict.get(colorChosen)); -// } -// -// return cardID; -// } else { -// System.out.println("Sorry, the newly drawn card is not playable :("); -// return -1; -// } -// } - - - /** - * helper function for playCardWithCommandLine - * parse the integer inputted by the player - * @return the ** INDEX ** of card in list cards that user chooses - */ -// private int getInputCardIndex() { -// Scanner inputScanner = new Scanner(System.in); -// String input = inputScanner.nextLine(); -// int decision; -// try { -// decision = Integer.parseInt(input) - 1; // -1 because we want the index in cards list -// } catch (Exception e) { -// System.out.println("Please input the number corresponding the card you choose!"); -// return -1; -// } -// -// if (decision < 0 || decision >= player.getCards().size()) { -// System.out.println("The card you chose does not exist!"); -// return -1; -// } -// return decision; -// } - - /** - * helper function for playCardWithCommandLine - * parse the integer inputted by the player - * @return the key of the color (in cardManager.colorDict) chosen by the user - */ -// private int getInputColor() { -// Scanner inputScanner = new Scanner(System.in); -// String input = inputScanner.nextLine(); -// int decision; -// try { -// decision = Integer.parseInt(input); -// } catch (Exception e) { -// System.out.println("Please input the number corresponding the card you choose!"); -// return -1; -// } -// -// if (decision != 1 && decision != 2 && decision != 3 && decision != 4) { -// System.out.println("Please choose a valid color (represented by number)!"); -// return -1; -// } -// return decision; -// } -// -// /** -// * prompt player to take action current round -// */ -// private void promptChooseAction() { -// player.printCardsInHand(); -// System.out.println("The following are playable:"); -// player.printLegalCards(); -// System.out.println("Please choose you action this round:"); -// System.out.println("[1] Play a owned card"); -// System.out.println("[2] Draw a card and play it if possible"); -// System.out.println("Please input 1 or 2 : "); -// } -// -// private void promptChooseColor() { -// player.printCardsInHand(); -// System.out.println("Please pick a color for the next rounds:"); -// System.out.println("[1] Red"); -// System.out.println("[2] Green"); -// System.out.println("[3] Blue"); -// System.out.println("[4] Yellow"); -// System.out.println("Please choose one from above: "); -// } -// -// public void printForcedSkip() { -// System.out.println("Sorry, you lost this turn and was forcefully skipped."); -// } - - - -} - - diff --git a/src/UNO/GUI.java b/src/UNO/GUI.java deleted file mode 100644 index 2de961d04433a46028fafe85d4dab2ec39c98bd2..0000000000000000000000000000000000000000 --- a/src/UNO/GUI.java +++ /dev/null @@ -1,32 +0,0 @@ -package UNO; - - -/** - * Prepared for Assignment-1.2. - * Serves as the interaction between the GUI package and player class. - */ - -public class GUI { - private Player player; - public GUI(Player p) { - player = p; - } - - - /** - * FOR real playing scenarios with GUI - * When it's this player's turn, the player must pick one card to play - */ -// public int playCardWithGUI() { -// return 0; -// } - - /** - * get player using this gui. - */ - public Player getPlayer() { - return player; - } -} - - diff --git a/src/UNO/Game.java b/src/UNO/Game.java index a5f4bda7eb6e4546e4e51475ec8d5b21057c1e45..1dbe41723069677c53f107624712b9565cc4b3de 100644 --- a/src/UNO/Game.java +++ b/src/UNO/Game.java @@ -1,44 +1,90 @@ package UNO; -import java.util.*; +import java.util.*; +import java.lang.Thread; +import GUI.*; +import java.io.*; /** + * !!!!!!!!!!!!!!!!!!!!!! Controller IN MVC !!!!!!!!!!!!!!!!!!!!!!! * The game controller object representing a UNO game. * Organize the interactions of all other classes in the UNO package. */ public class Game { - private static CardParser parser = new CardParser(); + // private static CardParser parser = new CardParser(); private static final int INIT_DRAW = 7; // every player get 7 cards at beginning private int rounds = 1; private RuleController ruler; private CardManager gameCardManager; private ArrayList<Player> players; + private boolean setupDone = false; + private int winnerID = -1; private int currentPlayerID; // playerID starts from 0 !!!!! + private int humanNum; + private int AINum; + private boolean startNewGame = false; + private int gapTime = 5000; // as micro-seconds + private boolean useGUI = true; + private boolean manualSetup = false; + + /** + * The action current player choose. Will only be modified by GameStagePage when + * current player make a legal move. (i.e. play is legal) + */ + private int currentPlayerAction; + + /** + * Whether a color has been declared in ChooseColorPage. Will only be modified by ChooseColorPage when + * current player plays a wild card and click one of the four color buttons. + */ + private boolean colorIsPicked; /** * Default constructor for Game object - * @param playerNum a number between 2 and 9 - number of players for the game (1 for debugging only) + * @param humanPlayerNum a number between 2 and 9 - number of human players for the game (1 for debugging only) + * @param AIPlayerNum a number between 2 and 9 - number of AI players for the game (1 for debugging only) + * Notice that humanPlayerNum + AIPlayerNum should be in [2,10) in formal games. */ - public Game(int playerNum) { - assert(playerNum >= 1 && playerNum < 10); // UNO is intended for 2-9 players, 1 for debug only! + public Game(int humanPlayerNum, int AIPlayerNum) { + humanNum = humanPlayerNum; + AINum = AIPlayerNum; gameCardManager = new CardManager(); ruler = new RuleController(); + } + + + /** + * Initialize the game. This function should only be called when number of players have been decided. + */ + public void initializeGame() { + int playerNum = humanNum + AINum; decideFirstPlayerID(playerNum); + if (!manualSetup) initializePlayers(); + for (Player player : players) { + player.drawCards(INIT_DRAW); + } + } + + private void initializePlayers() { + int playerNum = humanNum + AINum; players = new ArrayList<>(); + ArrayList<Boolean> isHuman = new ArrayList<>(); + for (int i = 0; i < humanNum; i++) isHuman.add(true); + for (int i = 0; i < AINum; i++) isHuman.add(false); + Collections.shuffle(isHuman); - // playerID starts from 0 !!!!! + /* playerID starts from 0 !!!!! */ for (int i = 0; i < playerNum; i++) { - Player player = new Player(i, this, true); - player.drawCards(INIT_DRAW); + Player player = isHuman.get(i) ? new Player(i, this) : new ArtificialIntelligence(i, this); players.add(player); } } - /** * Randomly decide the start position of this game + * * @param playerNum total number of players */ private void decideFirstPlayerID(int playerNum) { @@ -58,37 +104,177 @@ public class Game { } } - /** - * Left for assignment1.2 - * Run one round of UNO - * very import function ! - */ -// public void runOneRound() { -// ruler.reportCurrentState(this); -// System.out.println("It's now Player " + (currentPlayerID + 1) + "'s turn..."); -// Player currentPlayer = players.get(currentPlayerID); -// currentPlayer.playOneRound(); -// -// } - - /** * This is where the game loop starts. */ - public void gameStart() { + public void gameStart() throws InterruptedException { while (true) { + colorIsPicked = false; // be ready for each human player request of picking colors. Player currentPlayer = players.get(currentPlayerID); - currentPlayer.runOneRoundForTesting(); - if (currentPlayer.playerWin()) { - break; + if (currentPlayer.isHuman()) { + handleHumanBehavior(currentPlayer); + } else { + handleAIBehavior((AIPlayer) currentPlayer); } + + if (winnerID != -1) break; updateNextPlayerID(); rounds++; } } + /** + * This function creates the GUI for user to choose action, view hand cards, etc. + * Servers as the "controller" for human in MVC. + * It updates the game states stored in the RuleController class (which serves as the "Model"), + * when players make a legal action. (shares part of the functionality of main loop) + * + * TEST MANUALLY + */ + public void handleHumanBehavior(Player currentPlayer) { + GameStagePage gamePage = new GameStagePage(this); + currentPlayerAction = -1; + + PrintStream original = System.out; + System.setOut(new PrintStream(new OutputStream() {public void write(int b) {}})); // we don't want garbage print-out information + while (currentPlayerAction == -1) { // see the comment for this variable on top for details + System.out.println("looping..."); // do not delete this line or code won't run! + } + System.setOut(original); + + /* Actions arriving here is guaranteed to be legal. */ + handleCurrentPlayerAction(gamePage, currentPlayer); + + if (currentPlayer.playerWin()) { // a player played out all hand cards + winnerID = currentPlayerID; + new PlayerWinPage(this); + } + gamePage.dispose(); + } + + /** + * handle the decision made by human player. (share part of the functionality of handleHumanBehavior) + * TESTED MANUALLY + */ + private void handleCurrentPlayerAction(GameStagePage gamePage, Player currentPlayer) { + switch (currentPlayerAction) { + case 1 -> { // play owned cards - case where user plays one owned card + ArrayList<Integer> selectedCards = gamePage.getSelectedCards(); + humanPlayOwned(currentPlayer, selectedCards); + } + case 2 -> { // draw & play + /* draw pile and discard pile are both empty, we simply skip */ + if (gameCardManager.numCardLeft() + gameCardManager.numLeftDiscardPile() == 0) { + currentPlayer.optionSkip(); + return; + } + currentPlayer.drawCards(1); + int newCardID = currentPlayer.getCards().get(getPlayerCardNumber(currentPlayerID) - 1); + boolean legal = currentPlayer.optionPlayOwnedCard(newCardID, true); + String prevCardColor = ruler.getPreviousCard().split(" ")[0]; + String action = legal ? "Draw & Play (OK)" : "Draw & Play (FAIL)"; + ruler.setPreviousAction(action); + if (prevCardColor.equals("NA")) promptPlayerChooseColor(); // wild card, choose color + } + case 3 -> { + ruler.setPreviousAction("Skip"); + currentPlayer.optionSkip(); // skip + } + } + } + + /** + * Helper function for handling a human player's valid play. + * @param currentPlayer the player being in the round + * @param selectedCards arraylist of cards selected by the player (obtained from Viewer) + */ + private void humanPlayOwned(Player currentPlayer, ArrayList<Integer> selectedCards) { + if (selectedCards.size() == 1) { // player selected one card to play + int cardID = selectedCards.get(0); + currentPlayer.optionPlayOwnedCard(cardID, true); + String prevCardColor = ruler.getPreviousCard().split(" ")[0]; + ruler.setPreviousAction("Play Owned (1)"); + if (prevCardColor.equals("NA")) promptPlayerChooseColor(); + + } else { // play owned cards - case where player selected two cards to play + int cardID1 = selectedCards.get(0); + int cardID2 = selectedCards.get(1); + ruler.setPreviousAction("Play Owned (2)"); + /* only one of following two will succeed */ + if (!currentPlayer.optionPlayTwoOwnedCard_Add(cardID1, cardID2, true)) + currentPlayer.optionPlayTwoOwnedCard_Sub(cardID1, cardID2, true); + } + } + + /** + * This function provides visualization (GUI) for rounds of AI in the game loop. + * Servers as the "controller" for AI in MVC. + * It updates the game states stored in the RuleController class (which serves as the "Model"), + * when AIs make a legal action. (shares part of the functionality of main loop) + * This function could not be combined with handleBehavior. + */ + public void handleAIBehavior(AIPlayer currentPlayer) { + GameStagePage gamePage = useGUI ? new GameStagePage(this) : null; + int decision = currentPlayer.makeActionDecision(); + switch (decision) { + case 1 -> { // play owned cards. Notice AI won't play 2 cards at one time + AIPlayOwned(currentPlayer); + } + case 2 -> { // draw & play + AIDrawAndPlay(currentPlayer); + } + case 3 -> { // skip this round + ruler.setPreviousAction("Skip"); + currentPlayer.optionSkip(); // skip + } + } + + try { Thread.sleep(gapTime);} + catch (Exception e) { System.out.println("Interrupted"); } // should not be reached + if (currentPlayer.playerWin()) { // a player played out all hand cards + winnerID = currentPlayerID; + if (useGUI) new PlayerWinPage(this); + } + if (gamePage != null) gamePage.dispose(); + } + + + /** + * Helper function for AI drawing a card and play. + * @param currentPlayer the current AI player in the round + */ + private void AIDrawAndPlay(AIPlayer currentPlayer) { + currentPlayer.drawCards(1); + int newCardID = currentPlayer.getCards().get(getPlayerCardNumber(currentPlayerID) - 1); + boolean legal = currentPlayer.optionPlayOwnedCard(newCardID, true); + String prevCardColor = ruler.getPreviousCard().split(" ")[0]; + if (prevCardColor.equals("NA")) { // a wild card is played by AI + String colorDecision = currentPlayer.pickColor(); + ruler.updateGameStateBasedOnPickedColor(colorDecision); + } + String action = legal ? "Draw & Play (OK)" : "Draw & Play (FAIL)"; + ruler.setPreviousAction(action); + } + + /** + * Helper function for AI playing his owned cards. + * @param currentPlayer the current AI player in the round + */ + private void AIPlayOwned(AIPlayer currentPlayer) { + int cardID = currentPlayer.playCard(); + currentPlayer.optionPlayOwnedCard(cardID, true); + String prevCardColor = ruler.getPreviousCard().split(" ")[0]; + if (prevCardColor.equals("NA")) { // a wild card is played by AI + String colorDecision = currentPlayer.pickColor(); + ruler.updateGameStateBasedOnPickedColor(colorDecision); + } + ruler.setPreviousAction("Play Owned (1)"); // (1) because one card is played + } + + /** * Getter for game rounds. + * * @return rounds of game already proceeded as integer */ public int getRounds() { @@ -98,6 +284,7 @@ public class Game { /** * Get game state (rule controller) info. + * * @return game state information wrapper (ruleController) */ public RuleController getRuler() { @@ -114,6 +301,7 @@ public class Game { /** * Getter for all players in a game. + * * @return arraylist<Player> representing all players */ public ArrayList<Player> getPlayers() { @@ -121,7 +309,7 @@ public class Game { } /** - * Get the current player ID. Built for GUI. + * Get the current player ID. Built for GUI (Viewer in MVC). * @return the ID of currentPlayer */ public int getCurrentPlayerID() { @@ -136,6 +324,202 @@ public class Game { if (ruler.getIsClockwise()) return (currentPlayerID + 1) % players.size(); else return (currentPlayerID - 1 + players.size()) % players.size(); } + + /** + * Get the number of cards of one player + * @return the number of cards left in the player's hand + */ + public int getPlayerCardNumber(int playerID) { + return players.get(playerID).getCards().size(); + } + + /** + * Get the winner ID of the game. + * + * @return winner ID. + */ + public int getWinnerID() { + return winnerID; + } + + /** + * actionID - { 1 : play, 2 : draw & play, 3 : skip} + */ + public void setUserAction(int actionID) { + currentPlayerAction = actionID; + } + + /** + * Set color is picked as TRUE. + */ + public void setColorIsPicked() { + colorIsPicked = true; + } + + /** + * Interface for manually add players into the game. + * @param playerList ArrayList<Player> of players to add + */ + public void setPlayers(ArrayList<Player> playerList) { + players = playerList; + } + + /** + * Create a pop-up window for users who played wild cards to pick color. + * TESTED MANUALLY! + */ + private void promptPlayerChooseColor() { + ChooseColorPopUp colorPage = new ChooseColorPopUp(this); + while (!colorIsPicked) { + colorPage.setAsVisible(true); // prevent clicking on "X" by chance + if (colorIsPicked) colorPage.setAsVisible(false); // prevent race condition + } + } + + /** + * Set the number of player numbers. + * @param humanPlayerNum the number of human players + * @param AIPlayerNum the number of AI players + */ + public void setPlayerNumbers(int humanPlayerNum, int AIPlayerNum) { + humanNum = humanPlayerNum; + AINum = AIPlayerNum; + } + + /** + * Prepared for GUI - player number page (view) to inform the game controller that set up is ready. + */ + public void setSetupDone() { + setupDone = true; + } + + /** + * Whether the game initialization set-up is ready. + */ + public boolean isSetupDone() { + return setupDone; + } + + /** + * Prepared for GUI - winner page (view) to inform the game controller to start a new game. + */ + public void setStartNewGame() { + startNewGame = true; + } + + /** + * Whether to start a new game after this game is finished + * @return Whether to start a new game after this game is finished + */ + public boolean toStartNewGame() { + return startNewGame; + } + + /** + * To set a time gap after each AI player made a decision. + * @param t time for sleeping (as 1/1000 seconds) + */ + public void setGapTime(int t) { + gapTime = t; + } + + /** + * Whether or not to start a game with GUI. + * @param b a boolean value + */ + public void setUseGUI(boolean b) { + useGUI = b; + } + + /** + * Whether to set up a game manually for debugging + * @param b a boolean value + */ + public void setManualSetup(boolean b) { + manualSetup = b; + } + + /** + * Get the previous played cards from RuleController(Model in MVC) for GUI (viewer). + * @return previous card played + */ + public String getPreviousCard() { + return ruler.getPreviousCard(); + } + + /** + * Get action performed by previous player from RuleController(Model in MVC) for GUI(viewer). + * @return action of previous player + */ + public String getPreviousAction() { + return ruler.getPreviousAction(); + } + + /** + * Get penalty draw information from RuleController (Model in MVC) for GUI (Viewer). + * @return stacked penalty draw + */ + public int getPenaltyDraw() { + return ruler.getPenaltyDraw(); + } + + /** + * Get current skip level from RuleController (Model in MVC) for GUI (Viewer). + * @return the skip level for next player. + */ + public int getNextPlayerSkiplevel() { + return ruler.getNextPlayerSkiplevel(); + } + + /** + * Handle request by viewer (GUI) to update game state stored in Model (RuleController), + * when player plays a wild/wildDraw4 card. + * @param color color picked by the player + */ + public void updateGameStateBasedOnPickedColor(String color) { + ruler.updateGameStateBasedOnPickedColor(color); + } + + /** + * Get the hand cards of a given player (Model in MVC) for the Viewer. + * @param playerID the target player + * @return hand cards of a player + */ + public ArrayList<Integer> getPlayerCards(int playerID) { + return players.get(playerID).getCards(); + } + + /** + * Helps Viewer (GUI) know whether a player is human. + * @param playerID the target player + * @return whether the player is human + */ + public boolean isHuman(int playerID) { + return players.get(playerID).isHuman(); + } + + /** + * Helps Viewer (GUI) judge whether a chosen card is legal play. + * @param currentPlayer the player playing the card + * @param cardID the card being played + * @return whether the card is legal or not + */ + public boolean isChosenCardLegalCaseOneCard(Player currentPlayer, int cardID) { + return currentPlayer.optionPlayOwnedCard(cardID, false); + } + + /** + * Helps Viewer (GUI) judge whether two chosen cards are legal play using addition /subtraction rules. + * @param currentPlayer the player playing the card + * @param cardID1,cardID2 two cards chosen by the player + * @return whether the composed play is legal or not + */ + public boolean isChosenCardLegalCaseTwoCards(Player currentPlayer, int cardID1, int cardID2) { + boolean addLegal = currentPlayer.optionPlayTwoOwnedCard_Add(cardID1, cardID2, false); + boolean subLegal = currentPlayer.optionPlayTwoOwnedCard_Sub(cardID1, cardID2, false); + return addLegal || subLegal; + } + } diff --git a/src/UNO/Main.java b/src/UNO/Main.java new file mode 100644 index 0000000000000000000000000000000000000000..5edd625824f0e88a6d33e66cffc44dc2d6eebb13 --- /dev/null +++ b/src/UNO/Main.java @@ -0,0 +1,43 @@ +package UNO; + +import GUI.*; + +import javax.swing.*; +import java.io.OutputStream; +import java.io.PrintStream; + +/** + * Enter point for the whole game. + */ +public class Main { + /** + * The enter for running the whole game. It eases the interaction between Game (Controller in MVC), + * and the GUI (Viewer in MVC). Note that update of game state stored in RuleController class (Model in MVC), + * should not be updated here. + */ + public static void main(String[] args) throws InterruptedException { + while(true) { + Game game = new Game(0, 0); // declare an uninitialized game + new WelcomePage(game); + PrintStream original = System.out; + System.setOut(new PrintStream(new OutputStream() { + public void write(int b) { + } + })); // we don't want garbage print-out information + while (!game.isSetupDone()) { // see the comment for this variable on top for details + System.out.println("looping..."); // do not delete this line or code won't run! + } + System.setOut(original); + game.initializeGame(); + game.gameStart(); + System.setOut(new PrintStream(new OutputStream() { + public void write(int b) { + } + })); // we don't want garbage print-out information + while (!game.toStartNewGame()) { // see the comment for this variable on top for details + System.out.println("looping..."); // do not delete this line or code won't run! + } + System.setOut(original); + } + } +} diff --git a/src/UNO/Player.java b/src/UNO/Player.java index 4c57ca7d3c1484855a9c149c6e8301c61c94c351..19f4bc6b7ff41ab9678b72ef9393c90b94dd0d4e 100644 --- a/src/UNO/Player.java +++ b/src/UNO/Player.java @@ -1,36 +1,31 @@ package UNO; -import javax.xml.stream.FactoryConfigurationError; -import java.lang.reflect.Array; + import java.util.*; /** + * !!!!!!!!!!!!!!!!!!!!!!!!! Part of Model in MVC !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! * Player object representing each player in the game. + * There will be no interaction with the Viewer (GUI) in this class. */ public class Player { public static CardParser parser; private final ArrayList<Integer> cards; - private int playerID; private Game gameController; private RuleController ruler; - private CmdUI prompterCmd; - private GUI prompterGUI; - /** * Constructor for player objects * - * @param ID An unique integer identifier for a player - * @param game An game controller object representing the game player being in - * @param useCmdAsUI Whether or not to use terminal as default UI + * @param ID An unique integer identifier for a player + * @param game An game controller object representing the game player being in */ - public Player(int ID, Game game, Boolean useCmdAsUI) { - playerID = ID; + public Player(int ID, Game game) { cards = new ArrayList<>(); gameController = game; ruler = (game != null) ? game.getRuler() : null; // null only for testing purpose parser = (game != null) ? RuleController.parser : null; - prompterCmd = useCmdAsUI ? new CmdUI(this) : null; - prompterGUI = useCmdAsUI ? null : new GUI(this); +// prompterCmd = useCmdAsUI ? new CmdUI(this) : null; +// prompterGUI = useCmdAsUI ? null : new GUI(this); } /** @@ -43,23 +38,6 @@ public class Player { cards.addAll(newCards); } - /** - * Print all cards that this user has to terminal - */ -// public void printCardsInHand() { -// if (!cards.isEmpty()) { -// System.out.println("\n\n==================================================================="); -// System.out.println("Currently have " + cards.size() + " cards:"); -// for (int i = 0; i < cards.size(); i++) { -// System.out.println("[" + (i + 1) + "] " + parser.parseCardID(cards.get(i))); -// } -// System.out.println("===================================================================\n\n"); -// -// } else { -// System.out.println("============================================================"); -// System.out.println("You got not cards in hands! Congrats, you are the winner!"); -// } -// } /** * Iterate through cards to find all playable cards under the game state controlled by player.ruler @@ -73,44 +51,8 @@ public class Player { return legalCards; } - /** - * Iterate through cards and see if any of the player's cards are playable - * - * @return an array list of the indices of legal cards in cards owned by player - */ -// public ArrayList<Integer> findLegalCardIndex() { -// ArrayList<Integer> legalCardIndices = new ArrayList<>(); -// for (int i = 0; i < cards.size(); i++) { -// if (ruler.isValidPlay(this, cards.get(i), false)) legalCardIndices.add(i); -// } -// return legalCardIndices; -// } - - - /** - * Print all cards the player own - */ -// public void printLegalCards() { -// ArrayList<Integer> legals = findLegalCard(); -// if (!legals.isEmpty()) { -// System.out.println("\n\n==================================================================="); -// System.out.println("The following cards are playable in current turn:"); -// for (int i = 0; i < legals.size(); i++) { -// System.out.println("[" + (i + 1) + "] " + parser.parseCardID(legals.get(i))); -// } -// System.out.println("===================================================================\n\n"); -// -// } else { -// System.out.println("\n\n============================================================"); -// System.out.println("You don't have any playable cards now."); -// System.out.println("============================================================\n\n"); -// } -// } - - /** * Getter for cards (private attribute for tracking all cards owned by a player) - * * @return the reference of cards (ArrayList<Integer>) of the player */ public ArrayList<Integer> getCards() { @@ -118,43 +60,6 @@ public class Player { } - /** - * Prepared for assignment1.3 - * Caller for player to play one round - * skip, draw, play ... all behavior will be handled by this function - */ -// public void playOneRound() { -// if (prompterGUI != null) { -// playOneRoundGUI(); -// } else if (prompterCmd != null) { -// playOneRoundCmd(); -// } -// } - - /** - * Prepared for assignment1.2 - * Playing with cmd as UI - */ -// public void playOneRoundCmd() { -// if (ruler.checkSkipAndDraw(this)) { -// prompterCmd.printForcedSkip(); -// } else { -// prompterCmd.promptTakeAction(); -// } -// -// } - - /** - * Prepared for assignment1.2 - * TO BE IMPLEMENTED... - */ -// public void playOneRoundGUI() { -// if (ruler.shouldPlayerBeSkipped(this)) { -// prompterGUI -// } else {} - -// } - /** * For Test only. Add one card with Given card ID to the player's cards. * @param cardID cardID to be inserted @@ -164,24 +69,6 @@ public class Player { } -// public boolean playOneRoundNonInteract(Integer option) { -// if (ruler.checkSkipAndDraw(this)) { -// return false; -// } -// -// if (option == 1) { -// optionPlayOwnedCard(); -// } else if (option == 2) { -// optionDrawCardAndPlay(); -// } else if (option == 3) { -// optionPlayTwoOwnedCard_Add(); -// } else if (option == 4) { -// optionPlayTwoOwnedCard_Sub(); -// } -// return false; -// } - - /** * For Assignment1.0 only * Implement the logic of player playing a card. @@ -195,23 +82,22 @@ public class Player { * * @Return whether this player successfully played the indicated card */ - public boolean optionPlayOwnedCard(int cardID) { - if (ruler.checkSkipAndDraw(this)) return false; + public boolean optionPlayOwnedCard(int cardID, boolean updateIfPossible) { + if (ruler.checkSkipAndDraw(this, false)) return false; assert(cards.contains(cardID)); - if (ruler.isValidPlay(this, cardID, true)) { - // maintain list-cards and discardPile + boolean isLegal = ruler.isValidPlay(this, cardID, updateIfPossible); + if (isLegal && updateIfPossible) { + /* maintain list-cards and discardPile */ cards.remove(cards.indexOf(cardID)); gameController.getCardManager().insertOneCardToDiscardPile(cardID); - if (parser.isNonColorCard(cardID)) { - // instead of choose colors - // we will forcefully set it to red for assignment 0 to make testing easier + /* instead of choose colors, + we will forcefully set it to red for assignment 0 to make testing easier */ ruler.setMatchableColor(parser.colorDict.get(1)); } - return true; } - return false; + return isLegal; } @@ -229,28 +115,41 @@ public class Player { * @Return whether this player successfully played the newly drawn card */ public boolean optionDrawCardAndPlay() { - if (ruler.checkSkipAndDraw(this)) return false; + if (ruler.checkSkipAndDraw(this, false)) return false; drawCards(1); int chosenCardIndex = getCards().size() - 1; int newCard = cards.get(chosenCardIndex); - if (ruler.isValidPlay(this, newCard, true)) { cards.remove(chosenCardIndex); gameController.getCardManager().insertOneCardToDiscardPile(newCard); - if (parser.isNonColorCard(newCard)) { - // instead of choose colors - // we will forcefully set it to red for assignment 0 to make testing easier + /* instead of choose colors, + we will forcefully set it to red for assignment 0 to make testing easier */ ruler.setMatchableColor(parser.colorDict.get(1)); } - return true; } return false; // played will be skipped } + /** + * Implement the option where player choose to skip his round. + */ + public void optionSkip() { + CardManager cardManager= gameController.getCardManager(); + int totalCardsLeft = cardManager.numCardLeft() + cardManager.numLeftDiscardPile(); + /* If the card left on the draw pile is not enough for stacked draw, draw as many as possible. */ + if (totalCardsLeft < ruler.getPenaltyDraw()) { + ruler.setNextPlayerSkiplevel(0); + drawCards(totalCardsLeft); + ruler.resetPenaltyDraw(); + } else { + ruler.drawStackedPenalty(this); // else just take all stacked penalty draw. + } + } + /** * Implement extra rule - addition by assignment-1.1. * @@ -262,12 +161,13 @@ public class Player { * the two cards will be removed from hand & inserted to discard pile. * @Return whether this player successfully played the two indicated card */ - public boolean optionPlayTwoOwnedCard_Add(int cardID1, int cardID2) { - if (ruler.checkSkipAndDraw(this)) return false; + public boolean optionPlayTwoOwnedCard_Add(int cardID1, int cardID2, boolean updateIfPossible) { + if (ruler.checkSkipAndDraw(this, false)) return false; assert(cards.contains(cardID1) && cards.contains(cardID2)); // checks player owns the card int equiCardID = addTwoCard(cardID1, cardID2); // the equivalent card ID after edition if (equiCardID == -1) return false; - if (ruler.isValidPlay(this, equiCardID, true)) { + boolean isLegal = ruler.isValidPlay(this, equiCardID, updateIfPossible); + if (isLegal && updateIfPossible) { // maintain list-cards and discardPile cards.remove(cards.indexOf(cardID1)); cards.remove(cards.indexOf(cardID2)); @@ -275,7 +175,8 @@ public class Player { gameController.getCardManager().insertOneCardToDiscardPile(cardID2); return true; } - return false; + + return isLegal; } /** @@ -289,12 +190,12 @@ public class Player { * the two cards will be removed from hand & inserted to discard pile. * @Return whether this player successfully played the two indicated card */ - public boolean optionPlayTwoOwnedCard_Sub(int cardID1, int cardID2) { - if (ruler.checkSkipAndDraw(this)) return false; - + public boolean optionPlayTwoOwnedCard_Sub(int cardID1, int cardID2, boolean updateIfPossible) { + if (ruler.checkSkipAndDraw(this, false)) return false; int equiCardID = subTwoCard(cardID1, cardID2); // the equivalent card ID after subtraction if (equiCardID == -1) return false; - if (ruler.isValidPlay(this, equiCardID, true)) { + boolean isLegal = ruler.isValidPlay(this, equiCardID, updateIfPossible); + if (isLegal && updateIfPossible) { // maintain list-cards and discardPile cards.remove(cards.indexOf(cardID1)); cards.remove(cards.indexOf(cardID2)); @@ -302,32 +203,22 @@ public class Player { gameController.getCardManager().insertOneCardToDiscardPile(cardID2); return true; } - return false; + return isLegal; } /** * A player wins when he does not own any card * - * @return wheh\]ter the player wins + * @return whether the player wins */ public boolean playerWin() { return cards.isEmpty(); } - /** - * A player unconditionally plays a card, ignoring the rule - * Built for testing whether a player wins - */ - public void runOneRoundForTesting() { - cards.remove(0); - } - - /** * Helper function for adding two cards * Cards must be number cards and with same color to be able to added together * Return a equivalent cardID for the convenience of judging legality - * * @param cardID1 first card ID to be added * @param cardID2 second card ID to be added * @return (int) The cardID with equivalent effect of the addition, -1 if addition not legal or not found @@ -339,7 +230,6 @@ public class Player { int num2 = parseResult[1]; int numResult = num1 + num2; String equiDesc = equiDescConstructor(cardID1, numResult); - for (int i = 1; i <= 108; i++) { String iterDesc = parser.parseCardID(i); if (iterDesc.equals(equiDesc)) return i; @@ -364,7 +254,6 @@ public class Player { int num2 = parseResult[1]; int numResult = num1 > num2 ? (num1 - num2) : (num2 - num1); // make sure result is always positive String equiDesc = equiDescConstructor(cardID1, numResult); - for (int i = 1; i <= 108; i++) { String iterDesc = parser.parseCardID(i); if (iterDesc.equals(equiDesc)) return i; @@ -415,16 +304,8 @@ public class Player { * @param id id to be set */ public void setPlayerID(int id) { - playerID = id; } - /** - * Getter for player ID - * @return ID of a player - */ -// public int getPlayerID() { -// return playerID; -// } /** * Get information of what game is the player currently in @@ -442,14 +323,6 @@ public class Player { } -// /** -// * Get game state (rule controller) info -// * @return game state information wrapper (ruleController) -// */ -// public RuleController getRuler() { -// return ruler; -// } - /** * @param r the ruler controller for the player * Set information of what game is the player currently in @@ -458,6 +331,13 @@ public class Player { ruler = r; } + /** + * @Return true (Player class objects are human) + */ + public boolean isHuman() { + return true; + } + } \ No newline at end of file diff --git a/src/UNO/RuleController.java b/src/UNO/RuleController.java index 904b42da16fb27320deb92406e86b7b935078e63..96b9db8346d3c96be119c348fcfefb15c60a8f9d 100644 --- a/src/UNO/RuleController.java +++ b/src/UNO/RuleController.java @@ -1,13 +1,17 @@ package UNO; + import java.util.ArrayList; import java.util.Random; /** - * A class controlling the game rule and tracking the game state. + * !!!!!!!!!!!!!!!!!!!!!! Part of Model IN MVC!!!!!!!!!!!!!!!!!!!!!!! + * A class handling the game rule and storing the game state (backend data). * It judges whether each play of card is valid, and can update game state correspondingly. * Game state is represented by currentMatchableColor, currentMatchableNumber, * currentMatchableSymbol, cumulativePenaltyDraw(stacked draw), and nextPlayerSkipLevel. * See the getter & setter for details for these variables. + * + * There will be no interaction with the Viewer (GUI) in this class! */ public class RuleController { public static CardParser parser = new CardParser(); @@ -19,10 +23,12 @@ public class RuleController { private String currentMatchableNumber; private String currentMatchableSymbol = "none"; // Card played in the first round can only match by either color or number private String previousCard; + private String previousAction = "None"; private int nextPlayerSkipLevel = 0; private int cumulativePenaltyDraw = 0; private boolean isClockWise = true; // if clockwise, next player will be the element in player list + public RuleController() { // initialize first matched color and number boolean chosen = false; @@ -50,28 +56,36 @@ public class RuleController { * @param player the pending player * @return whether the player was skipped and forced to draw */ - public boolean checkSkipAndDraw(Player player) { + public boolean checkSkipAndDraw(Player player, boolean drawIfSkipped) { if (nextPlayerSkipLevel == 3) { assert cumulativePenaltyDraw == 0; nextPlayerSkipLevel = 0; // the next player should no longer be skipped return true; } else if (nextPlayerSkipLevel != 0) { - // player will be skipped and forced to draw cards + // player will be skipped and forced to draw cards (if drawIfSkipped as true) // unless they have draw2/wildDraw4 cards assert cumulativePenaltyDraw != 0; boolean toSkip = player.findLegalCard().isEmpty(); - if (toSkip) { - nextPlayerSkipLevel = 0; - player.drawCards(cumulativePenaltyDraw); - resetPenaltyDraw(); + if (toSkip && drawIfSkipped) { + drawStackedPenalty(player); } return toSkip; + } else { return false; } } + /** + * For a player to clear all stacked penalty draw. + * @param player The victim + */ + public void drawStackedPenalty(Player player) { + nextPlayerSkipLevel = 0; + player.drawCards(cumulativePenaltyDraw); + resetPenaltyDraw(); + } /** @@ -83,14 +97,14 @@ public class RuleController { * @return whether the play is valid */ public boolean isValidPlay(Player player, int cardID, boolean updateIfValid) { - assert(player != null); - String cardDescription = parser.parseCardID(cardID); - String[] result = parser.parseCardDescription(cardDescription); + assert(player != null); + String cardDescription = parser.parseCardID(cardID); + String[] result = parser.parseCardDescription(cardDescription); String color = result[0]; String type = result[1]; String content = result[2]; - boolean valid = false; + boolean valid; if (nextPlayerSkipLevel == 3) { valid = false; @@ -101,7 +115,7 @@ public class RuleController { } else if (nextPlayerSkipLevel == 1) { if (content.equals("wildDraw4")) valid = checkDraw4IsLegal(player); - else return content.equals("draw2"); // draw 2 is always allowed in this case + else valid = content.equals("draw2"); // draw 2 is always allowed in this case } else { valid = isValidSkip0(player, color, content); @@ -277,18 +291,6 @@ public class RuleController { */ public void setNextPlayerSkiplevel(int level) { nextPlayerSkipLevel = level; } -// private String descSkipLevel() { -// if (nextPlayerSkipLevel == 0) { -// return("level 0: player won't be skipped"); -// } else if (nextPlayerSkipLevel == 1) { -// return("level 1: player can play a either a draw2 card or a wildDraw4 card to avoid being skipped"); -// } else if (nextPlayerSkipLevel == 2) { -// return("level 2: next player can only play a wildDraw4 card of any color to avoid being skipped"); -// } else { -// return("level 3: next player will be skipped anyway."); -// } -// } - /** * Get the total number of stacked card to be drawn (cumulativePenaltyDraw). * @return cumulativePenaltyDraw @@ -338,6 +340,28 @@ public class RuleController { } + public void setPreviousAction(String action) { + previousAction = action; + } + public String getPreviousAction() { + return previousAction; + } + + + /** + * Update game state information (previous card & matchable color ) when wild cards are played. + * @param color A color string of declared color. + */ + public void updateGameStateBasedOnPickedColor(String color) { + CardParser parser = new CardParser(); + setMatchableColor(color); + String prevCard = getPreviousCard(); + String type = parser.parseCardDescription(prevCard)[1]; + String content = parser.parseCardDescription(prevCard)[2]; + setPreviousCard(color + " "+ type + " " + content); // e.g. "newColor sym wild" + } + + // /** // * Print current game state to terminal // * @param gameController the game object representing current game. diff --git a/src/sp21-cs242-assignment11.iml b/src/sp21-cs242-assignment11.iml new file mode 100644 index 0000000000000000000000000000000000000000..059b6dacfea03c3b05eb9ce83d13b78bd826d261 --- /dev/null +++ b/src/sp21-cs242-assignment11.iml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/Test" isTestSource="true" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="module" module-name="sp21-cs242-assignment1" /> + </component> +</module> \ No newline at end of file