Compare commits

...

10 Commits

Author SHA1 Message Date
9ffc92f1a6 added crits
~30 min of work
2024-01-22 01:39:39 +01:00
bfde39a595 added gh link 2024-01-21 15:16:41 +01:00
11c13364b4 Documentation
~1h work
2024-01-21 15:16:03 +01:00
7f0800cf52 cleanup db access 2024-01-21 15:02:19 +01:00
69cb4417fd fight unit tests
~1.25h work
2024-01-21 14:03:25 +01:00
37d9f84787 finished fight logic
~2h work
2024-01-21 12:17:09 +01:00
4874f110c1 improvements to router tests
~15 min work
2024-01-21 00:07:36 +01:00
6da608f90b reworked session handler to non-singleton
~10 min work
2024-01-21 00:06:44 +01:00
9b1448edd8 router tests
~1.25h work
2024-01-20 21:45:56 +01:00
52f7866707 restructure for tests
~ 10 min work
2024-01-20 20:29:50 +01:00
67 changed files with 870 additions and 136 deletions

17
.idea/libraries/junit_jupiter.xml generated Normal file
View File

@ -0,0 +1,17 @@
<component name="libraryTable">
<library name="junit.jupiter" type="repository">
<properties maven-id="org.junit.jupiter:junit-jupiter:5.9.0" />
<CLASSES>
<root url="jar://$PROJECT_DIR$/lib/junit-jupiter-5.9.0.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/junit-jupiter-api-5.9.0.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/opentest4j-1.2.0.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/junit-platform-commons-1.9.0.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/apiguardian-api-1.1.2.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/junit-jupiter-params-5.9.0.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/junit-jupiter-engine-5.9.0.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/junit-platform-engine-1.9.0.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

13
.idea/libraries/mockito_core.xml generated Normal file
View File

@ -0,0 +1,13 @@
<component name="libraryTable">
<library name="mockito.core" type="repository">
<properties maven-id="org.mockito:mockito-core:5.8.0" />
<CLASSES>
<root url="jar://$PROJECT_DIR$/lib/mockito-core-5.8.0.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/byte-buddy-1.14.10.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/byte-buddy-agent-1.14.10.jar!/" />
<root url="jar://$PROJECT_DIR$/lib/objenesis-3.3.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

1
.idea/sqldialects.xml generated
View File

@ -2,5 +2,6 @@
<project version="4"> <project version="4">
<component name="SqlDialectMappings"> <component name="SqlDialectMappings">
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/720ef4bc-36c4-4861-8776-49b639600223/console.sql" dialect="PostgreSQL" /> <file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/720ef4bc-36c4-4861-8776-49b639600223/console.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/setup.sql" dialect="PostgreSQL" />
</component> </component>
</project> </project>

View File

@ -47,17 +47,17 @@ read -p "Press any key to resume ..." null
# -------------------------------------------------- # --------------------------------------------------
echo "3) create packages (done by admin)" echo "3) create packages (done by admin)"
curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"845f0dc7-37d0-426e-994e-43fc3ac83c08\", \"Name\":\"WaterGoblin\", \"Damage\": 10.0}, {\"Id\":\"99f8f8dc-e25e-4a95-aa2c-782823f36e2a\", \"Name\":\"Dragon\", \"Damage\": 50.0}, {\"Id\":\"e85e3976-7c86-4d06-9a80-641c2019a79f\", \"Name\":\"WaterSpell\", \"Damage\": 20.0}, {\"Id\":\"1cb6ab86-bdb2-47e5-b6e4-68c5ab389334\", \"Name\":\"Ork\", \"Damage\": 45.0}, {\"Id\":\"dfdd758f-649c-40f9-ba3a-8657f4b3439f\", \"Name\":\"FireSpell\", \"Damage\": 25.0}]" curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"845f0dc7-37d0-426e-994e-43fc3ac83c08\", \"Name\":\"WaterGoblin\", \"Damage\": 10.0, \"Crit\":30}, {\"Id\":\"99f8f8dc-e25e-4a95-aa2c-782823f36e2a\", \"Name\":\"Dragon\", \"Damage\": 50.0, \"Crit\":10}, {\"Id\":\"e85e3976-7c86-4d06-9a80-641c2019a79f\", \"Name\":\"WaterSpell\", \"Damage\": 20.0, \"Crit\":null}, {\"Id\":\"1cb6ab86-bdb2-47e5-b6e4-68c5ab389334\", \"Name\":\"Ork\", \"Damage\": 45.0, \"Crit\":0}, {\"Id\":\"dfdd758f-649c-40f9-ba3a-8657f4b3439f\", \"Name\":\"FireSpell\", \"Damage\": 25.0, \"Crit\":null}]"
echo . echo .
curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"644808c2-f87a-4600-b313-122b02322fd5\", \"Name\":\"WaterGoblin\", \"Damage\": 9.0}, {\"Id\":\"4a2757d6-b1c3-47ac-b9a3-91deab093531\", \"Name\":\"Dragon\", \"Damage\": 55.0}, {\"Id\":\"91a6471b-1426-43f6-ad65-6fc473e16f9f\", \"Name\":\"WaterSpell\", \"Damage\": 21.0}, {\"Id\":\"4ec8b269-0dfa-4f97-809a-2c63fe2a0025\", \"Name\":\"Ork\", \"Damage\": 55.0}, {\"Id\":\"f8043c23-1534-4487-b66b-238e0c3c39b5\", \"Name\":\"WaterSpell\", \"Damage\": 23.0}]" curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"644808c2-f87a-4600-b313-122b02322fd5\", \"Name\":\"WaterGoblin\", \"Damage\": 9.0, \"Crit\":15}, {\"Id\":\"4a2757d6-b1c3-47ac-b9a3-91deab093531\", \"Name\":\"Dragon\", \"Damage\": 55.0, \"Crit\":15}, {\"Id\":\"91a6471b-1426-43f6-ad65-6fc473e16f9f\", \"Name\":\"WaterSpell\", \"Damage\": 21.0, \"Crit\":null}, {\"Id\":\"4ec8b269-0dfa-4f97-809a-2c63fe2a0025\", \"Name\":\"Ork\", \"Damage\": 55.0, \"Crit\":5}, {\"Id\":\"f8043c23-1534-4487-b66b-238e0c3c39b5\", \"Name\":\"WaterSpell\", \"Damage\": 23.0, \"Crit\":null}]"
echo . echo .
curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"b017ee50-1c14-44e2-bfd6-2c0c5653a37c\", \"Name\":\"WaterGoblin\", \"Damage\": 11.0}, {\"Id\":\"d04b736a-e874-4137-b191-638e0ff3b4e7\", \"Name\":\"Dragon\", \"Damage\": 70.0}, {\"Id\":\"88221cfe-1f84-41b9-8152-8e36c6a354de\", \"Name\":\"WaterSpell\", \"Damage\": 22.0}, {\"Id\":\"1d3f175b-c067-4359-989d-96562bfa382c\", \"Name\":\"Ork\", \"Damage\": 40.0}, {\"Id\":\"171f6076-4eb5-4a7d-b3f2-2d650cc3d237\", \"Name\":\"RegularSpell\", \"Damage\": 28.0}]" curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"b017ee50-1c14-44e2-bfd6-2c0c5653a37c\", \"Name\":\"WaterGoblin\", \"Damage\": 11.0, \"Crit\":45}, {\"Id\":\"d04b736a-e874-4137-b191-638e0ff3b4e7\", \"Name\":\"Dragon\", \"Damage\": 70.0, \"Crit\":20}, {\"Id\":\"88221cfe-1f84-41b9-8152-8e36c6a354de\", \"Name\":\"WaterSpell\", \"Damage\": 22.0, \"Crit\":null}, {\"Id\":\"1d3f175b-c067-4359-989d-96562bfa382c\", \"Name\":\"Ork\", \"Damage\": 40.0, \"Crit\":0}, {\"Id\":\"171f6076-4eb5-4a7d-b3f2-2d650cc3d237\", \"Name\":\"RegularSpell\", \"Damage\": 28.0, \"Crit\":null}]"
echo . echo .
curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"ed1dc1bc-f0aa-4a0c-8d43-1402189b33c8\", \"Name\":\"WaterGoblin\", \"Damage\": 10.0}, {\"Id\":\"65ff5f23-1e70-4b79-b3bd-f6eb679dd3b5\", \"Name\":\"Dragon\", \"Damage\": 50.0}, {\"Id\":\"55ef46c4-016c-4168-bc43-6b9b1e86414f\", \"Name\":\"WaterSpell\", \"Damage\": 20.0}, {\"Id\":\"f3fad0f2-a1af-45df-b80d-2e48825773d9\", \"Name\":\"Ork\", \"Damage\": 45.0}, {\"Id\":\"8c20639d-6400-4534-bd0f-ae563f11f57a\", \"Name\":\"WaterSpell\", \"Damage\": 25.0}]" curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"ed1dc1bc-f0aa-4a0c-8d43-1402189b33c8\", \"Name\":\"WaterGoblin\", \"Damage\": 10.0, \"Crit\":30}, {\"Id\":\"65ff5f23-1e70-4b79-b3bd-f6eb679dd3b5\", \"Name\":\"Dragon\", \"Damage\": 50.0, \"Crit\":20}, {\"Id\":\"55ef46c4-016c-4168-bc43-6b9b1e86414f\", \"Name\":\"WaterSpell\", \"Damage\": 20.0, \"Crit\":null}, {\"Id\":\"f3fad0f2-a1af-45df-b80d-2e48825773d9\", \"Name\":\"Ork\", \"Damage\": 45.0, \"Crit\":10}, {\"Id\":\"8c20639d-6400-4534-bd0f-ae563f11f57a\", \"Name\":\"WaterSpell\", \"Damage\": 25.0, \"Crit\":null}]"
echo . echo .
curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"d7d0cb94-2cbf-4f97-8ccf-9933dc5354b8\", \"Name\":\"WaterGoblin\", \"Damage\": 9.0}, {\"Id\":\"44c82fbc-ef6d-44ab-8c7a-9fb19a0e7c6e\", \"Name\":\"Dragon\", \"Damage\": 55.0}, {\"Id\":\"2c98cd06-518b-464c-b911-8d787216cddd\", \"Name\":\"WaterSpell\", \"Damage\": 21.0}, {\"Id\":\"951e886a-0fbf-425d-8df5-af2ee4830d85\", \"Name\":\"Ork\", \"Damage\": 55.0}, {\"Id\":\"dcd93250-25a7-4dca-85da-cad2789f7198\", \"Name\":\"FireSpell\", \"Damage\": 23.0}]" curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"d7d0cb94-2cbf-4f97-8ccf-9933dc5354b8\", \"Name\":\"WaterGoblin\", \"Damage\": 9.0, \"Crit\":30}, {\"Id\":\"44c82fbc-ef6d-44ab-8c7a-9fb19a0e7c6e\", \"Name\":\"Dragon\", \"Damage\": 55.0, \"Crit\":10}, {\"Id\":\"2c98cd06-518b-464c-b911-8d787216cddd\", \"Name\":\"WaterSpell\", \"Damage\": 21.0, \"Crit\":null}, {\"Id\":\"951e886a-0fbf-425d-8df5-af2ee4830d85\", \"Name\":\"Ork\", \"Damage\": 55.0, \"Crit\":15}, {\"Id\":\"dcd93250-25a7-4dca-85da-cad2789f7198\", \"Name\":\"FireSpell\", \"Damage\": 23.0, \"Crit\":null}]"
echo . echo .
curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"b2237eca-0271-43bd-87f6-b22f70d42ca4\", \"Name\":\"WaterGoblin\", \"Damage\": 11.0}, {\"Id\":\"9e8238a4-8a7a-487f-9f7d-a8c97899eb48\", \"Name\":\"Dragon\", \"Damage\": 70.0}, {\"Id\":\"d60e23cf-2238-4d49-844f-c7589ee5342e\", \"Name\":\"WaterSpell\", \"Damage\": 22.0}, {\"Id\":\"fc305a7a-36f7-4d30-ad27-462ca0445649\", \"Name\":\"Ork\", \"Damage\": 40.0}, {\"Id\":\"84d276ee-21ec-4171-a509-c1b88162831c\", \"Name\":\"RegularSpell\", \"Damage\": 28.0}]" curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"b2237eca-0271-43bd-87f6-b22f70d42ca4\", \"Name\":\"WaterGoblin\", \"Damage\": 11.0, \"Crit\":20}, {\"Id\":\"9e8238a4-8a7a-487f-9f7d-a8c97899eb48\", \"Name\":\"Dragon\", \"Damage\": 70.0, \"Crit\":5}, {\"Id\":\"d60e23cf-2238-4d49-844f-c7589ee5342e\", \"Name\":\"WaterSpell\", \"Damage\": 22.0, \"Crit\":null}, {\"Id\":\"fc305a7a-36f7-4d30-ad27-462ca0445649\", \"Name\":\"Ork\", \"Damage\": 40.0, \"Crit\":10}, {\"Id\":\"84d276ee-21ec-4171-a509-c1b88162831c\", \"Name\":\"RegularSpell\", \"Damage\": 28.0, \"Crit\":null}]"
echo . echo .
echo . echo .
@ -95,11 +95,11 @@ read -p "Press any key to resume ..." null
# -------------------------------------------------- # --------------------------------------------------
echo "6) add new packages" echo "6) add new packages"
curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"67f9048f-99b8-4ae4-b866-d8008d00c53d\", \"Name\":\"WaterGoblin\", \"Damage\": 10.0}, {\"Id\":\"aa9999a0-734c-49c6-8f4a-651864b14e62\", \"Name\":\"RegularSpell\", \"Damage\": 50.0}, {\"Id\":\"d6e9c720-9b5a-40c7-a6b2-bc34752e3463\", \"Name\":\"Knight\", \"Damage\": 20.0}, {\"Id\":\"02a9c76e-b17d-427f-9240-2dd49b0d3bfd\", \"Name\":\"RegularSpell\", \"Damage\": 45.0}, {\"Id\":\"2508bf5c-20d7-43b4-8c77-bc677decadef\", \"Name\":\"FireElf\", \"Damage\": 25.0}]" curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"67f9048f-99b8-4ae4-b866-d8008d00c53d\", \"Name\":\"WaterGoblin\", \"Damage\": 10.0, \"Crit\":50}, {\"Id\":\"aa9999a0-734c-49c6-8f4a-651864b14e62\", \"Name\":\"RegularSpell\", \"Damage\": 50.0, \"Crit\":null}, {\"Id\":\"d6e9c720-9b5a-40c7-a6b2-bc34752e3463\", \"Name\":\"Knight\", \"Damage\": 20.0}, {\"Id\":\"02a9c76e-b17d-427f-9240-2dd49b0d3bfd\", \"Name\":\"RegularSpell\", \"Damage\": 45.0, \"Crit\":null}, {\"Id\":\"2508bf5c-20d7-43b4-8c77-bc677decadef\", \"Name\":\"FireElf\", \"Damage\": 25.0, \"Crit\":70}]"
echo . echo .
curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"70962948-2bf7-44a9-9ded-8c68eeac7793\", \"Name\":\"WaterGoblin\", \"Damage\": 9.0}, {\"Id\":\"74635fae-8ad3-4295-9139-320ab89c2844\", \"Name\":\"FireSpell\", \"Damage\": 55.0}, {\"Id\":\"ce6bcaee-47e1-4011-a49e-5a4d7d4245f3\", \"Name\":\"Knight\", \"Damage\": 21.0}, {\"Id\":\"a6fde738-c65a-4b10-b400-6fef0fdb28ba\", \"Name\":\"FireSpell\", \"Damage\": 55.0}, {\"Id\":\"a1618f1e-4f4c-4e09-9647-87e16f1edd2d\", \"Name\":\"FireElf\", \"Damage\": 23.0}]" curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"70962948-2bf7-44a9-9ded-8c68eeac7793\", \"Name\":\"WaterGoblin\", \"Damage\": 9.0, \"Crit\":30}, {\"Id\":\"74635fae-8ad3-4295-9139-320ab89c2844\", \"Name\":\"FireSpell\", \"Damage\": 55.0, \"Crit\":null}, {\"Id\":\"ce6bcaee-47e1-4011-a49e-5a4d7d4245f3\", \"Name\":\"Knight\", \"Damage\": 21.0}, {\"Id\":\"a6fde738-c65a-4b10-b400-6fef0fdb28ba\", \"Name\":\"FireSpell\", \"Damage\": 55.0, \"Crit\":null}, {\"Id\":\"a1618f1e-4f4c-4e09-9647-87e16f1edd2d\", \"Name\":\"FireElf\", \"Damage\": 23.0, \"Crit\":60}]"
echo . echo .
curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"2272ba48-6662-404d-a9a1-41a9bed316d9\", \"Name\":\"WaterGoblin\", \"Damage\": 11.0}, {\"Id\":\"3871d45b-b630-4a0d-8bc6-a5fc56b6a043\", \"Name\":\"Dragon\", \"Damage\": 70.0}, {\"Id\":\"166c1fd5-4dcb-41a8-91cb-f45dcd57cef3\", \"Name\":\"Knight\", \"Damage\": 22.0}, {\"Id\":\"237dbaef-49e3-4c23-b64b-abf5c087b276\", \"Name\":\"WaterSpell\", \"Damage\": 40.0}, {\"Id\":\"27051a20-8580-43ff-a473-e986b52f297a\", \"Name\":\"FireElf\", \"Damage\": 28.0}]" curl -i -X POST http://localhost:10001/packages --header "Content-Type: application/json" --header "Authorization: Bearer $token3" -d "[{\"Id\":\"2272ba48-6662-404d-a9a1-41a9bed316d9\", \"Name\":\"WaterGoblin\", \"Damage\": 11.0, \"Crit\":30}, {\"Id\":\"3871d45b-b630-4a0d-8bc6-a5fc56b6a043\", \"Name\":\"Dragon\", \"Damage\": 70.0, \"Crit\":20}, {\"Id\":\"166c1fd5-4dcb-41a8-91cb-f45dcd57cef3\", \"Name\":\"Knight\", \"Damage\": 22.0}, {\"Id\":\"237dbaef-49e3-4c23-b64b-abf5c087b276\", \"Name\":\"WaterSpell\", \"Damage\": 40.0, \"Crit\":null}, {\"Id\":\"27051a20-8580-43ff-a473-e986b52f297a\", \"Name\":\"FireElf\", \"Damage\": 28.0, \"Crit\":55}]"
echo . echo .
echo . echo .

View File

@ -1 +1,54 @@
# mtcg # MTCG
This repository is for the 3rd semester project "MonsterTradingCardGame" for software-engineering 1.
## Setup
To run the project, modify the connection string in "connection_string.txt" to allow access to the database. The database structure can be recreated with the commands found in setup.sql
## Structure
The project consists of two main parts, the HTTP-server, and the actual application. Additionally, there are the database helper functions
### HTTP Server
The Server manages routing and resolving of requests, and sends them to the specified services.
### DB
The db part is an attempt to simplify database access, but due to some more complex calls this hasn't worked out completely. It still makes the code more readable, and abstracts SQL statements to enums.
### MTCG
The last part, the application itself, mostly consists of the Service classes, implementing the Service interface, and the corresponding main classes, which for the most part just interface with the database functions. There are two relevant exceptions: The battle class (and helpers), and the session handler:
Sessions are one of the few things, that isn't stored in the database, but in memory. Everytime a user makes a request that has to be authenticated, the session handler looks it up in its map, and compares the requirements (admin, username).
The battle handler is responsible for matching players, and creating a new battle. As multiple users might try to start a battle simultaneously, it needs to be thread-safe, and run the actual battles asynchronously. It implements a queue system, and creates a separate battle instance per two players. Since every new request also creates a new thread, the battle handler only needs to be thread-safe, and only one player runs the actual battle for it to be multithreading. Both players then read from their shared battle class once the battle is complete to retrieve the log.
## Potential improvements
- Currently, the exact REST path is discarded after the router resolves it, as multiple functions share the same service class. This could be alleviated by either creating a separate service per route, which however would result in about 20 services, or otherwise parsing the route, which may defeat the point of the service interface, depending on implementation. This inefficiency also results in another issue, the service implementations have quite cluttered switches.
- As previously mentioned, the db functions are not very complete, and might not improve db access, as some hacks and manually adapting the query is required in quite a few instances. For statements which are fully supported though, it simplifies db calls quite a bit. Depending on IDE the whole system might be obsolete though.
## Unit tests
There are two parts of the project which are tested through unit tests: the router and the fight logic. The router is relevant because it allows most other code to rely on different forms of testing, as the router is guaranteed to be working, the fight logic is relevant to test as other testing methods are not precise enough to map an error to a specific part of the code. For other methods integration tests are easier to integrate, and server the same purpose, assuming the database and router are working correctly (hence the unit tests for the router).
## Lessons learned
- Starting with the HTTP server made it quite easy testing the application with integration tests (manual using postman or automated using the provided curl script).
- A separate db helper class would probably make sense for larger, more complex projects, but was overkill here.
## Unique feature
My unique feature is the ability to crit: Whenever two monsters fight (i.e. no spells involved), they can critically strike for an increase in damage of 50%. Each card has a crit modifier, default to 0.
## Further notes
The curl script was modified to automatically store the authentication tokens in variables.
Time tracking was added directly to the commits for a total of about 60 hours of work; commits without any time are less than 5 minutes.
https://github.com/Nanogamer7/swen-mtcg/

1
connection_string.txt Normal file
View File

@ -0,0 +1 @@
jdbc:postgresql://localhost:5432/mydb?user=postgres&password=postgres

Binary file not shown.

BIN
lib/byte-buddy-1.14.10.jar Normal file

Binary file not shown.

Binary file not shown.

BIN
lib/hamcrest-core-1.1.jar Normal file

Binary file not shown.

Binary file not shown.

BIN
lib/junit-4.10.jar Normal file

Binary file not shown.

BIN
lib/junit-jupiter-5.9.0.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
lib/mockito-core-5.8.0.jar Normal file

Binary file not shown.

BIN
lib/objenesis-3.3.jar Normal file

Binary file not shown.

BIN
lib/opentest4j-1.2.0.jar Normal file

Binary file not shown.

View File

@ -1,23 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4"> <module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true"> <component name="AdditionalModuleElements">
<exclude-output /> <content url="file://$MODULE_DIR$" dumb="true">
<content url="file://$MODULE_DIR$"> <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
</content> </content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module-library" scope="PROVIDED">
<library>
<CLASSES>
<root url="jar://$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.30/lombok-1.18.30.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="library" name="fasterxml.jackson.core" level="project" />
<orderEntry type="library" name="fasterxml.jackson.core.annotations" level="project" />
<orderEntry type="library" name="fasterxml.jackson.core.databind" level="project" />
</component> </component>
</module> </module>

225
setup.sql Normal file
View File

@ -0,0 +1,225 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 16.0 (Debian 16.0-1.pgdg120+1)
-- Dumped by pg_dump version 16.1
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Name: cardtype; Type: TYPE; Schema: public; Owner: postgres
--
CREATE TYPE public.cardtype AS ENUM (
'monster',
'spell'
);
ALTER TYPE public.cardtype OWNER TO postgres;
--
-- Name: delete_package(); Type: FUNCTION; Schema: public; Owner: postgres
--
CREATE FUNCTION public.delete_package() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
DELETE FROM packages
WHERE uuid = OLD.package;
RETURN OLD;
END;
$$;
ALTER FUNCTION public.delete_package() OWNER TO postgres;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- Name: cards; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.cards (
uuid uuid DEFAULT gen_random_uuid() NOT NULL,
damage double precision DEFAULT 0 NOT NULL,
crit integer DEFAULT 0,
owner uuid,
deck boolean DEFAULT false NOT NULL,
trade boolean DEFAULT false NOT NULL,
package uuid,
name character varying,
CONSTRAINT "trade or deck" CHECK (((deck IS FALSE) OR (trade IS FALSE)))
);
ALTER TABLE public.cards OWNER TO postgres;
--
-- Name: packages; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.packages (
uuid uuid DEFAULT gen_random_uuid() NOT NULL,
created_at timestamp without time zone DEFAULT now()
);
ALTER TABLE public.packages OWNER TO postgres;
--
-- Name: trades; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.trades (
uuid uuid DEFAULT gen_random_uuid() NOT NULL,
card uuid NOT NULL,
card_type public.cardtype NOT NULL,
min_dmg double precision NOT NULL,
user_uuid uuid
);
ALTER TABLE public.trades OWNER TO postgres;
--
-- Name: users; Type: TABLE; Schema: public; Owner: postgres
--
CREATE TABLE public.users (
uuid uuid DEFAULT gen_random_uuid() NOT NULL,
username character varying NOT NULL,
password character varying NOT NULL,
name character varying,
bio character varying,
image character varying,
elo integer DEFAULT 100 NOT NULL,
wins integer DEFAULT 0 NOT NULL,
losses integer DEFAULT 0 NOT NULL,
coins integer DEFAULT 20 NOT NULL,
admin boolean DEFAULT false
);
ALTER TABLE public.users OWNER TO postgres;
--
-- Data for Name: cards; Type: TABLE DATA; Schema: public; Owner: postgres
--
--
-- Data for Name: packages; Type: TABLE DATA; Schema: public; Owner: postgres
--
--
-- Data for Name: trades; Type: TABLE DATA; Schema: public; Owner: postgres
--
--
-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: postgres
--
--
-- Name: cards cards_pk; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.cards
ADD CONSTRAINT cards_pk PRIMARY KEY (uuid);
--
-- Name: packages packages_pk; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.packages
ADD CONSTRAINT packages_pk PRIMARY KEY (uuid);
--
-- Name: trades trades_pk; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.trades
ADD CONSTRAINT trades_pk PRIMARY KEY (uuid);
--
-- Name: users user_pk; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT user_pk PRIMARY KEY (uuid);
--
-- Name: users username; Type: CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.users
ADD CONSTRAINT username UNIQUE (username);
--
-- Name: cards delete_package_trigger; Type: TRIGGER; Schema: public; Owner: postgres
--
CREATE TRIGGER delete_package_trigger AFTER UPDATE ON public.cards FOR EACH ROW WHEN (((new.package IS NULL) AND (old.package IS NOT NULL))) EXECUTE FUNCTION public.delete_package();
--
-- Name: cards cards_packages_uuid_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.cards
ADD CONSTRAINT cards_packages_uuid_fk FOREIGN KEY (package) REFERENCES public.packages(uuid) ON DELETE SET NULL;
--
-- Name: cards cards_users_uuid_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.cards
ADD CONSTRAINT cards_users_uuid_fk FOREIGN KEY (owner) REFERENCES public.users(uuid) ON DELETE SET NULL;
--
-- Name: trades trades_cards_uuid_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.trades
ADD CONSTRAINT trades_cards_uuid_fk FOREIGN KEY (card) REFERENCES public.cards(uuid);
--
-- Name: trades trades_users_uuid_fk; Type: FK CONSTRAINT; Schema: public; Owner: postgres
--
ALTER TABLE ONLY public.trades
ADD CONSTRAINT trades_users_uuid_fk FOREIGN KEY (user_uuid) REFERENCES public.users(uuid);
--
-- PostgreSQL database dump complete
--

View File

@ -1,54 +0,0 @@
package at.nanopenguin.mtcg.application;
import at.nanopenguin.mtcg.Pair;
import at.nanopenguin.mtcg.application.service.schemas.Card;
import lombok.Getter;
import lombok.Setter;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Battle {
private record RoundResult(String winnerName, Card winnerCard, String loserName, Card loserCard, boolean draw);
private Pair<Combatant, Combatant> combatants;
@Getter
private volatile List<String> log = new ArrayList<>();
@Getter @Setter
private boolean firstPlayer = true;
public Battle(Combatant combatant) {
this.combatants = new Pair<>(combatant, null);
}
public void addCombatant(Combatant combatant) {
this.combatants.setRight(combatant);
}
public void start() throws SQLException {
do {
this.playRound();
} while (combatants.left().deckSize() > 0 && combatants.right().deckSize() > 0);
// placeholder
boolean leftWins = new Random().nextBoolean();
this.combatants.left().updateStats(leftWins);
this.combatants.right().updateStats(!leftWins);
}
private void playRound() {
Pair<Card, Card> cards = new Pair<>(this.combatants.left().getCard(), this.combatants.right().getCard());
RoundResult result = this.fight(cards.left(), cards.right());
this.log.add()
}
private boolean fight(Card left, Card right) {
}
private String createCombatString(RoundResult result) {
}
}

View File

@ -1,5 +1,6 @@
package at.nanopenguin.mtcg; package at.nanopenguin.mtcg;
import at.nanopenguin.mtcg.application.SessionHandler;
import at.nanopenguin.mtcg.application.service.*; import at.nanopenguin.mtcg.application.service.*;
import at.nanopenguin.mtcg.http.HttpMethod; import at.nanopenguin.mtcg.http.HttpMethod;
import at.nanopenguin.mtcg.http.Router; import at.nanopenguin.mtcg.http.Router;
@ -10,33 +11,34 @@ import java.io.IOException;
public class Main { public class Main {
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
Router router = new Router(); Router router = new Router();
SessionHandler sessionHandler = new SessionHandler();
router.addRoute(HttpMethod.GET, "/test/{var}/service", new TestService(), new int[]{2}); router.addRoute(HttpMethod.GET, "/test/{var}/service", new TestService(), new int[]{2});
/* users */ /* users */
router.addRoute(HttpMethod.POST, "/users", new UserService(), new int[]{}); router.addRoute(HttpMethod.POST, "/users", new UserService(sessionHandler), new int[]{});
router.addRoute(HttpMethod.GET, "/users/{username}", new UserService(), new int[]{2}); router.addRoute(HttpMethod.GET, "/users/{username}", new UserService(sessionHandler), new int[]{2});
router.addRoute(HttpMethod.PUT, "/users/{username}", new UserService(), new int[]{2}); router.addRoute(HttpMethod.PUT, "/users/{username}", new UserService(sessionHandler), new int[]{2});
router.addRoute(HttpMethod.POST, "/sessions", new UserService(), new int[]{}); router.addRoute(HttpMethod.POST, "/sessions", new UserService(sessionHandler), new int[]{});
/* packages */ /* packages */
router.addRoute(HttpMethod.POST, "/packages", new PackagesService(), new int[]{}); router.addRoute(HttpMethod.POST, "/packages", new PackagesService(sessionHandler), new int[]{});
router.addRoute(HttpMethod.POST, "/transactions/packages", new PackagesService(), new int[]{}); router.addRoute(HttpMethod.POST, "/transactions/packages", new PackagesService(sessionHandler), new int[]{});
/* cards */ /* cards */
router.addRoute(HttpMethod.GET, "/cards", new CardsService(), new int[]{}); router.addRoute(HttpMethod.GET, "/cards", new CardsService(sessionHandler), new int[]{});
router.addRoute(HttpMethod.GET, "/deck", new CardsService(), new int[]{}); router.addRoute(HttpMethod.GET, "/deck", new CardsService(sessionHandler), new int[]{});
router.addRoute(HttpMethod.PUT, "/deck", new CardsService(), new int[]{}); router.addRoute(HttpMethod.PUT, "/deck", new CardsService(sessionHandler), new int[]{});
/* game */ /* game */
router.addRoute(HttpMethod.GET, "/stats", new GameService(), new int[]{}); router.addRoute(HttpMethod.GET, "/stats", new GameService(sessionHandler), new int[]{});
router.addRoute(HttpMethod.GET, "/scoreboard", new GameService(), new int[]{}); router.addRoute(HttpMethod.GET, "/scoreboard", new GameService(sessionHandler), new int[]{});
router.addRoute(HttpMethod.POST, "/battles", new GameService(), new int[]{}); router.addRoute(HttpMethod.POST, "/battles", new GameService(sessionHandler), new int[]{});
/* trading */ /* trading */
router.addRoute(HttpMethod.GET, "/tradings", new TradingService(), new int[]{}); router.addRoute(HttpMethod.GET, "/tradings", new TradingService(sessionHandler), new int[]{});
router.addRoute(HttpMethod.POST, "/tradings", new TradingService(), new int[]{}); router.addRoute(HttpMethod.POST, "/tradings", new TradingService(sessionHandler), new int[]{});
router.addRoute(HttpMethod.DELETE, "/tradings/{tradingDealId}", new TradingService(), new int[]{2}); router.addRoute(HttpMethod.DELETE, "/tradings/{tradingDealId}", new TradingService(sessionHandler), new int[]{2});
router.addRoute(HttpMethod.POST, "/tradings/{tradingDealId}", new TradingService(), new int[]{2}); router.addRoute(HttpMethod.POST, "/tradings/{tradingDealId}", new TradingService(sessionHandler), new int[]{2});
Server server = new Server(10001, 10, router); Server server = new Server(10001, 10, router);
server.start(); server.start();

View File

@ -15,4 +15,13 @@ public class Pair<T, U> {
public T left() { return this.left; }; public T left() { return this.left; };
public U right() { return this.right; }; public U right() { return this.right; };
@Override
public boolean equals(Object o) {
if (o instanceof Pair<?,?>) {
return left.equals(((Pair<?, ?>) o).left) && right.equals(((Pair<?, ?>) o).right);
}
return super.equals(o);
}
} }

View File

@ -0,0 +1,193 @@
package at.nanopenguin.mtcg.application;
import at.nanopenguin.mtcg.Pair;
import at.nanopenguin.mtcg.application.service.schemas.Card;
import lombok.*;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
public class Battle {
@RequiredArgsConstructor
enum ElementMod {
HALF(0.5),
NONE(1),
DOUBLE(2);
public final double percentMod;
}
record FightDTO(Combatant player, Card card) {};
record RoundResult(FightDTO winner, FightDTO loser, boolean draw, ElementMod winnerMod, ElementMod loserMod) {};
private Pair<Combatant, Combatant> combatants;
@Getter
private volatile List<String> log = new ArrayList<>();
@Getter @Setter
private boolean firstPlayer = true;
public Battle(Combatant combatant) {
this.combatants = new Pair<>(combatant, null);
}
public void addCombatant(Combatant combatant) {
this.combatants.setRight(combatant);
}
public void start() throws SQLException {
boolean leftWins;
int round = 0;
do {
this.playRound(++round);
if (round == 100) return; // don't update stats on draw
System.out.println(combatants.left().deckSize() + " - " + combatants.right().deckSize() );
} while ((leftWins = combatants.left().deckSize() > 0) && combatants.right().deckSize() > 0);
this.combatants.left().updateStats(leftWins);
this.combatants.right().updateStats(!leftWins);
}
void playRound(int round) {
RoundResult result = this.fight(
new FightDTO(
this.combatants.left(),
this.combatants.left().getAndRemoveCard()),
new FightDTO(
this.combatants.right(),
this.combatants.right().getAndRemoveCard()),
true);
this.log.add(this.createCombatString(round, result));
result.winner().player().addCard( // return card to winner deck
result.winner().card());
if (result.draw()) {
result.loser().player().addCard( // on draw both players get their card
result.loser().card());
return;
}
result.winner.player().addCard( // on lose winner gets losers card to deck
result.loser().card());
}
static boolean isImmune(Card defend, Card attack) {
if (defend.name().equals("Dragon") && attack.name().endsWith("Goblin")) return true;
if (defend.name().equals("Wizzard") && attack.name().equals("Ork")) return true;
if (defend.name().equals("WaterSpell") && attack.name().equals("Knight")) return true;
if (defend.name().equals("Kraken") && attack.name().endsWith("Spell")) return true;
if (defend.name().equals("FireElf") && attack.name().equals("Dragon")) return true;
return false;
}
enum Element {
NORMAL,
FIRE,
WATER;
private Element[] strong;
private Element[] weak;
// improvement: map to get mods straight from this enum
static {
NORMAL.strong = new Element[]{WATER};
NORMAL.weak = new Element[]{FIRE};
FIRE.strong = new Element[]{NORMAL};
FIRE.weak = new Element[]{WATER};
WATER.strong = new Element[]{FIRE};
WATER.weak = new Element[]{NORMAL};
}
}
static Pair<ElementMod, ElementMod> getElementMod(Card left, Card right) {
Pair<ElementMod, ElementMod> returnMods = new Pair<>(ElementMod.NONE, ElementMod.NONE);
if (!left.name().endsWith("Spell") && !right.name().endsWith("Spell")) return returnMods;
Element leftElement = Element.NORMAL;
Element rightElement = Element.NORMAL;
for (val value : Element.values()) {
if (left.name().toLowerCase().startsWith(value.name().toLowerCase())) leftElement = value;
if (right.name().toLowerCase().startsWith(value.name().toLowerCase())) rightElement = value;
}
final Element finalLeftElement = leftElement; // lambdas
final Element finalRightElement = rightElement;
if (Arrays.stream(leftElement.strong).anyMatch(value -> value == finalRightElement)) returnMods.setLeft(ElementMod.DOUBLE);
else if (Arrays.stream(leftElement.weak).anyMatch(value -> value == finalRightElement)) returnMods.setLeft(ElementMod.HALF);
if (Arrays.stream(rightElement.strong).anyMatch(value -> value == finalLeftElement)) returnMods.setRight(ElementMod.DOUBLE);
else if (Arrays.stream(rightElement.weak).anyMatch(value -> value == finalLeftElement)) returnMods.setRight(ElementMod.HALF);
return returnMods;
}
static RoundResult fight(FightDTO left, FightDTO right, boolean allowCrit) {
if (isImmune(left.card(), right.card())) return new RoundResult(left, right, false, ElementMod.NONE, ElementMod.NONE);
if (isImmune(right.card(), left.card())) return new RoundResult(right, left, false, ElementMod.NONE, ElementMod.NONE);
Pair<ElementMod, ElementMod> dmgMods = getElementMod(left.card(), right.card());
allowCrit = allowCrit && left.card().crit() != null && right.card().crit() != null;
double leftCrit = allowCrit && new Random().nextInt() % 100 < left.card().crit() ? 1.5 : 1;
double rightCrit = allowCrit && new Random().nextInt() % 100 < right.card().crit() ? 1.5 : 1;
boolean leftWins = left.card().damage() * dmgMods.left().percentMod * leftCrit >= right.card().damage() * dmgMods.right().percentMod * rightCrit;
return new RoundResult(
leftWins ? left : right,
leftWins ? right : left,
left.card().damage() * dmgMods.left().percentMod * leftCrit == right.card().damage() * dmgMods.right().percentMod * rightCrit,
leftWins ? dmgMods.left() : dmgMods.right(),
leftWins ? dmgMods.right() : dmgMods.left());
}
static String createCombatString(int round, RoundResult result) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder
.append(round)
.append(": ")
.append(String.format("%s: %s (%s damage)",
result.winner().player().name,
result.winner().card().name(),
result.winner().card().damage()))
.append(" vs. ")
.append(String.format("%s: %s (%s damage)",
result.loser().player().name,
result.loser().card().name(),
result.loser().card().damage()))
.append(" => ");
if (result.winnerMod() != ElementMod.NONE || result.loserMod() != ElementMod.NONE) {
stringBuilder
.append(String.format("%s vs. %s",
result.winner().card().damage(),
result.loser().card().damage()))
.append(" -> ")
.append(String.format("%s vs. %s",
result.winner().card().damage() * result.winnerMod().percentMod,
result.loser().card().damage() * result.loserMod().percentMod))
.append(" => ");
}
if (result.draw()) {
return stringBuilder.append("Draw").toString();
}
stringBuilder
.append(result.winner().card().name())
.append(" wins");
return stringBuilder.toString();
}
}

View File

@ -47,7 +47,7 @@ public class Combatant {
.executeUpdate(); .executeUpdate();
}; };
public Card getCard() { public Card getAndRemoveCard() {
return this.deck.popRandom(); return this.deck.popRandom();
} }

View File

@ -34,6 +34,7 @@ public class Package {
.table(Table.CARDS); .table(Table.CARDS);
if (card.id() != null) query.parameter("uuid", card.id()); if (card.id() != null) query.parameter("uuid", card.id());
if (query.parameter("damage", card.damage()) if (query.parameter("damage", card.damage())
.parameter("crit", card.crit())
.parameter("name", card.name()) .parameter("name", card.name())
.parameter("package", packageUuid) .parameter("package", packageUuid)
.executeUpdate() != 1){ .executeUpdate() != 1){

View File

@ -12,21 +12,8 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
public final class SessionHandler { public final class SessionHandler {
private static SessionHandler INSTANCE;
private final Map<UUID, UserInfo> sessions = new HashMap<>(); private final Map<UUID, UserInfo> sessions = new HashMap<>();
private SessionHandler() {
}
public static SessionHandler getInstance() {
if (INSTANCE == null) {
INSTANCE = new SessionHandler();
}
return INSTANCE;
}
public synchronized UUID login(UserCredentials userCredentials) throws SQLException { // avoid multiple logins of same user public synchronized UUID login(UserCredentials userCredentials) throws SQLException { // avoid multiple logins of same user
val result = DbQuery.builder() val result = DbQuery.builder()

View File

@ -20,6 +20,7 @@ public class UserCards extends User {
.column("uuid AS id") .column("uuid AS id")
.column("name") .column("name")
.column("damage") .column("damage")
.column("crit")
.condition("owner", userUuid); .condition("owner", userUuid);
if (deckOnly) dbQueryBuilder.condition("deck", true); if (deckOnly) dbQueryBuilder.condition("deck", true);
ArrayList<Card> cards = new ArrayList<>(); ArrayList<Card> cards = new ArrayList<>();

View File

@ -19,14 +19,19 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
public class CardsService implements Service { public class CardsService implements Service {
private final SessionHandler sessionHandler;
public CardsService(SessionHandler sessionHandler) {
this.sessionHandler = sessionHandler;
}
@Override @Override
public Response handleRequest(HttpRequest request) throws JsonProcessingException, SQLException, ArrayIndexOutOfBoundsException { public Response handleRequest(HttpRequest request) throws JsonProcessingException, SQLException, ArrayIndexOutOfBoundsException {
UUID authToken = SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization")); UUID authToken = SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization"));
if (SessionHandler.getInstance().verifyUUID(authToken) != TokenValidity.VALID) if (this.sessionHandler.verifyUUID(authToken) != TokenValidity.VALID)
return new Response(HttpStatus.UNAUTHORIZED); return new Response(HttpStatus.UNAUTHORIZED);
UUID userUuid = SessionHandler.getInstance().userUuidFromToken(authToken); UUID userUuid = this.sessionHandler.userUuidFromToken(authToken);
if (request.getPath().split("/")[1].equals("cards") && request.getMethod() == HttpMethod.GET) { if (request.getPath().split("/")[1].equals("cards") && request.getMethod() == HttpMethod.GET) {
val result = UserCards.get(userUuid, false); val result = UserCards.get(userUuid, false);

View File

@ -12,14 +12,19 @@ import java.sql.SQLException;
import java.util.UUID; import java.util.UUID;
public class GameService implements Service { public class GameService implements Service {
private final SessionHandler sessionHandler;
public GameService(SessionHandler sessionHandler) {
this.sessionHandler = sessionHandler;
}
@Override @Override
public Response handleRequest(HttpRequest request) throws JsonProcessingException, SQLException { public Response handleRequest(HttpRequest request) throws JsonProcessingException, SQLException {
UUID authToken = SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization")); UUID authToken = SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization"));
if (SessionHandler.getInstance().verifyUUID(authToken) != TokenValidity.VALID) if (this.sessionHandler.verifyUUID(authToken) != TokenValidity.VALID)
return new Response(HttpStatus.UNAUTHORIZED); return new Response(HttpStatus.UNAUTHORIZED);
UUID userUuid = SessionHandler.getInstance().userUuidFromToken(authToken); UUID userUuid = this.sessionHandler.userUuidFromToken(authToken);
if (request.getPath().split("/")[1].equals("stats") && request.getMethod() == HttpMethod.GET) { if (request.getPath().split("/")[1].equals("stats") && request.getMethod() == HttpMethod.GET) {
return new Response(HttpStatus.OK, new ObjectMapper().writeValueAsString(User.getStats(userUuid))); return new Response(HttpStatus.OK, new ObjectMapper().writeValueAsString(User.getStats(userUuid)));

View File

@ -20,13 +20,18 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
public class PackagesService implements Service { public class PackagesService implements Service {
private final SessionHandler sessionHandler;
public PackagesService(SessionHandler sessionHandler) {
this.sessionHandler = sessionHandler;
}
@Override @Override
public Response handleRequest(HttpRequest request) throws JsonProcessingException, SQLException, ArrayIndexOutOfBoundsException { public Response handleRequest(HttpRequest request) throws JsonProcessingException, SQLException, ArrayIndexOutOfBoundsException {
UUID token = SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization")); UUID token = SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization"));
if (request.getPath().split("/")[1].equals("packages") && request.getMethod() == HttpMethod.POST) { if (request.getPath().split("/")[1].equals("packages") && request.getMethod() == HttpMethod.POST) {
return switch (SessionHandler.getInstance().verifyUUID(token, true)) { return switch (this.sessionHandler.verifyUUID(token, true)) {
case MISSING, INVALID -> new Response(HttpStatus.UNAUTHORIZED); case MISSING, INVALID -> new Response(HttpStatus.UNAUTHORIZED);
case FORBIDDEN -> new Response(HttpStatus.FORBIDDEN); case FORBIDDEN -> new Response(HttpStatus.FORBIDDEN);
case VALID -> new Response( case VALID -> new Response(
@ -37,12 +42,12 @@ public class PackagesService implements Service {
} }
if (String.join("/", Arrays.copyOfRange(request.getPath().split("/"), 1, 3)).equals("transactions/packages") && request.getMethod() == HttpMethod.POST) { if (String.join("/", Arrays.copyOfRange(request.getPath().split("/"), 1, 3)).equals("transactions/packages") && request.getMethod() == HttpMethod.POST) {
if (SessionHandler.getInstance().verifyUUID(token) != TokenValidity.VALID) return new Response(HttpStatus.UNAUTHORIZED); if (this.sessionHandler.verifyUUID(token) != TokenValidity.VALID) return new Response(HttpStatus.UNAUTHORIZED);
val result = Package.addToUser(SessionHandler.getInstance().userUuidFromToken(token)); val result = Package.addToUser(this.sessionHandler.userUuidFromToken(token));
if (result.left() == PurchaseStatus.SUCCESS) { if (result.left() == PurchaseStatus.SUCCESS) {
return new Response(HttpStatus.OK, "application/json", new ObjectMapper().writeValueAsString(result.right())); return new Response(HttpStatus.OK, "application/json", new ObjectMapper().writeValueAsString(result.right()));
} }
return new Response(switch (Package.addToUser(SessionHandler.getInstance().userUuidFromToken(token)).left()) { return new Response(switch (Package.addToUser(this.sessionHandler.userUuidFromToken(token)).left()) {
case NO_PACKAGE_AVAILABLE -> HttpStatus.NOT_FOUND; case NO_PACKAGE_AVAILABLE -> HttpStatus.NOT_FOUND;
case NOT_ENOUGH_MONEY -> HttpStatus.FORBIDDEN; case NOT_ENOUGH_MONEY -> HttpStatus.FORBIDDEN;
default -> HttpStatus.INTERNAL; default -> HttpStatus.INTERNAL;

View File

@ -15,13 +15,19 @@ import java.sql.SQLException;
import java.util.UUID; import java.util.UUID;
public class TradingService implements Service { public class TradingService implements Service {
private final SessionHandler sessionHandler;
public TradingService(SessionHandler sessionHandler) {
this.sessionHandler = sessionHandler;
}
@Override @Override
public Response handleRequest(HttpRequest request) throws JsonProcessingException, ArrayIndexOutOfBoundsException, SQLException { public Response handleRequest(HttpRequest request) throws JsonProcessingException, ArrayIndexOutOfBoundsException, SQLException {
UUID authToken = SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization")); UUID authToken = SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization"));
if (SessionHandler.getInstance().verifyUUID(authToken) != TokenValidity.VALID) if (this.sessionHandler.verifyUUID(authToken) != TokenValidity.VALID)
return new Response(HttpStatus.UNAUTHORIZED); return new Response(HttpStatus.UNAUTHORIZED);
UUID userUuid = SessionHandler.getInstance().userUuidFromToken(authToken); UUID userUuid = this.sessionHandler.userUuidFromToken(authToken);
if (request.getPath().split("/")[1].equals("tradings")) { if (request.getPath().split("/")[1].equals("tradings")) {
return switch (request.getMethod()) { return switch (request.getMethod()) {

View File

@ -18,12 +18,17 @@ import java.sql.SQLException;
import java.util.UUID; import java.util.UUID;
public class UserService implements Service { public class UserService implements Service {
private final SessionHandler sessionHandler;
public UserService(SessionHandler sessionHandler) {
this.sessionHandler = sessionHandler;
}
@Override @Override
public Response handleRequest(HttpRequest request) throws JsonProcessingException, SQLException, ArrayIndexOutOfBoundsException { public Response handleRequest(HttpRequest request) throws JsonProcessingException, SQLException, ArrayIndexOutOfBoundsException {
if (request.getPath().split("/")[1].equals("sessions") && request.getMethod() == HttpMethod.POST) { if (request.getPath().split("/")[1].equals("sessions") && request.getMethod() == HttpMethod.POST) {
// login // login
UUID uuid = SessionHandler.getInstance().login(new ObjectMapper().readValue(request.getBody(), UserCredentials.class)); UUID uuid = this.sessionHandler.login(new ObjectMapper().readValue(request.getBody(), UserCredentials.class));
return uuid != null ? return uuid != null ?
new Response(HttpStatus.OK, "application/json", uuid.toString()) : new Response(HttpStatus.OK, "application/json", uuid.toString()) :
new Response(HttpStatus.UNAUTHORIZED); new Response(HttpStatus.UNAUTHORIZED);
@ -33,7 +38,7 @@ public class UserService implements Service {
return switch (request.getMethod()) { return switch (request.getMethod()) {
case GET -> { case GET -> {
String username = request.getPath().split("/")[2]; String username = request.getPath().split("/")[2];
if (SessionHandler.getInstance().verifyUUID(SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization")), username, true) != TokenValidity.VALID) if (this.sessionHandler.verifyUUID(SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization")), username, true) != TokenValidity.VALID)
yield new Response(HttpStatus.UNAUTHORIZED); yield new Response(HttpStatus.UNAUTHORIZED);
val userData = User.retrieve(username); val userData = User.retrieve(username);
yield userData != null ? yield userData != null ?
@ -47,7 +52,7 @@ public class UserService implements Service {
case PUT -> { case PUT -> {
String username = request.getPath().split("/")[2]; String username = request.getPath().split("/")[2];
UserData userData = new ObjectMapper().readValue(request.getBody(), UserData.class); UserData userData = new ObjectMapper().readValue(request.getBody(), UserData.class);
if (SessionHandler.getInstance().verifyUUID(SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization")), username, true) != TokenValidity.VALID) if (this.sessionHandler.verifyUUID(SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization")), username, true) != TokenValidity.VALID)
yield new Response(HttpStatus.UNAUTHORIZED); yield new Response(HttpStatus.UNAUTHORIZED);
yield User.update(username, userData) ? new Response(HttpStatus.OK) : new Response(HttpStatus.NOT_FOUND); yield User.update(username, userData) ? new Response(HttpStatus.OK) : new Response(HttpStatus.NOT_FOUND);
} }

View File

@ -10,5 +10,6 @@ import java.util.UUID;
public record Card( public record Card(
UUID id, UUID id,
String name, String name,
Float damage) { Float damage,
Integer crit) {
} }

View File

@ -6,6 +6,9 @@ import lombok.NonNull;
import lombok.Singular; import lombok.Singular;
import lombok.val; import lombok.val;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.*; import java.sql.*;
import java.util.*; import java.util.*;
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Condition;
@ -14,7 +17,15 @@ import java.util.stream.Stream;
@Builder @Builder
public final class DbQuery { public final class DbQuery {
private static final String connectionString = "jdbc:postgresql://localhost:5432/mydb?user=postgres&password=postgres"; private static final String connectionString;
static {
try {
connectionString = Files.readString(Paths.get("connection_string.txt"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@NonNull @NonNull
private final SqlCommand command; private final SqlCommand command;
@ -143,8 +154,6 @@ public final class DbQuery {
} }
statementExecutor.setObject(i++, value); statementExecutor.setObject(i++, value);
} }
System.out.println(sql);
return statementExecutor.executeUpdate(); return statementExecutor.executeUpdate();
} }
} }
@ -171,8 +180,6 @@ public final class DbQuery {
} }
statementExecutor.setObject(i++, value); statementExecutor.setObject(i++, value);
} }
System.out.println(sql);
return statementExecutor.execute(); return statementExecutor.execute();
} }
} }
@ -200,7 +207,6 @@ public final class DbQuery {
statementExecutor.setObject(i++, value); statementExecutor.setObject(i++, value);
} }
System.out.println(sql);
try (ResultSet resultSet = statementExecutor.executeQuery()) { try (ResultSet resultSet = statementExecutor.executeQuery()) {
List<Map<String, Object>> result = new ArrayList<>(); List<Map<String, Object>> result = new ArrayList<>();

View File

@ -1,6 +1,7 @@
package at.nanopenguin.mtcg.http; package at.nanopenguin.mtcg.http;
import at.nanopenguin.mtcg.application.service.Service; import at.nanopenguin.mtcg.application.service.Service;
import lombok.NonNull;
import java.util.*; import java.util.*;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@ -8,11 +9,14 @@ import java.util.stream.IntStream;
public class Router { public class Router {
private final Map<HttpMethod, Map<String, Route>> routeMap = new HashMap<>(); private final Map<HttpMethod, Map<String, Route>> routeMap = new HashMap<>();
public void addRoute(final HttpMethod method, final String route, final Service service, final int[] pathVars) { public Router() {
Map<String, Route> map = this.routeMap.get(method); for (HttpMethod httpMethod : HttpMethod.values()) {
if (method != null && map == null) { this.routeMap.put(httpMethod, new HashMap<>());
this.routeMap.put(method, (map = new HashMap<>()));
} }
}
public void addRoute(@NonNull final HttpMethod method, final String route, final Service service, final int[] pathVars) {
Map<String, Route> map = this.routeMap.get(method);
List<String> routeComponents = new ArrayList<>(Arrays.asList(route.split("/"))); List<String> routeComponents = new ArrayList<>(Arrays.asList(route.split("/")));
for ( Integer pathVarPos : pathVars) { for ( Integer pathVarPos : pathVars) {

View File

@ -0,0 +1,117 @@
package at.nanopenguin.mtcg.application;
import at.nanopenguin.mtcg.Pair;
import at.nanopenguin.mtcg.application.service.schemas.Card;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.Mockito;
public class BattleTest {
private static Card cardWithName(String name) {
Card cardMock = Mockito.mock(Card.class);
Mockito.when(cardMock.name()).thenReturn(name);
return cardMock;
}
private static Card cardWithNameAndDmg(String name, float dmg) {
Card cardMock = Mockito.mock(Card.class);
Mockito.when(cardMock.name()).thenReturn(name);
Mockito.when(cardMock.damage()).thenReturn(dmg);
return cardMock;
}
@ParameterizedTest
@CsvSource({
"WaterGoblin, FireSpell, DOUBLE, HALF", // generic
"FireGoblin, NormalSpell, DOUBLE, HALF",
"NormalGoblin, WaterSpell, DOUBLE, HALF",
"FireGoblin, WaterSpell, HALF, DOUBLE", // generic reverse
"NormalGoblin, FireSpell, HALF, DOUBLE",
"WaterGoblin, NormalSpell, HALF, DOUBLE",
"WaterGoblin, FireTroll, NONE, NONE", // no spell
"WaterSpell, FireSpell, DOUBLE, HALF", // two spells
"FireGoblin, FireSpell, NONE, NONE", // same element
"FireSpell, Dragon, DOUBLE, HALF" // non elemental
})
void elementMods(String name1, String name2, Battle.ElementMod expectedMod1, Battle.ElementMod expectedMod2){
Card card1 = cardWithName(name1);
Card card2 = cardWithName(name2);
Assertions.assertEquals(new Pair<>(expectedMod1, expectedMod2), Battle.getElementMod(card1, card2));
}
@ParameterizedTest
@CsvSource({
"Dragon, FireGoblin, true",
"Dragon, WaterGoblin, true",
"Dragon, NormalGoblin, true",
"WaterGoblin, Dragon, false",
"Wizzard, Ork, true",
"Ork, Wizzard, false",
"WaterSpell, Knight, true",
"FireSpell, Knight, false",
"Knight, WaterSpell, false",
"Kraken, WaterSpell, true",
"Kraken, FireSpell, true",
"FireElf, Dragon, true",
"NormalElf, Dragon, false"
})
void immunityTest(String name1, String name2, boolean expected) {
Card card1 = cardWithName(name1);
Card card2 = cardWithName(name2);
Assertions.assertEquals(expected, Battle.isImmune(card1, card2));
}
private static Battle.FightDTO getFightDTO(String cardName, float dmg) {
Battle.FightDTO fightDTOmock = Mockito.mock(Battle.FightDTO.class);
Card cardMock = cardWithNameAndDmg(cardName, dmg);
Mockito.when(fightDTOmock.card()).thenReturn(cardMock);
return fightDTOmock;
}
enum fightOutcome {
LEFT_WINS,
DRAW,
RIGHT_WINS
}
@ParameterizedTest
@CsvSource({
"WaterGoblin, 10, FireTroll, 15, RIGHT_WINS, NONE, NONE",
"FireTroll, 15, WaterGoblin, 10, LEFT_WINS, NONE, NONE",
"FireSpell, 10, WaterSpell, 20, RIGHT_WINS, DOUBLE, HALF",
"FireSpell, 20, WaterSpell, 5, DRAW, HALF, DOUBLE",
"FireSpell, 90, WaterSpell, 5, LEFT_WINS, HALF, DOUBLE",
"FireSpell, 10, WaterGoblin, 10, RIGHT_WINS, DOUBLE, HALF",
"WaterSpell, 10, WaterGoblin, 10, DRAW, NONE, NONE",
"RegularSpell, 10, WaterGoblin, 10, LEFT_WINS, DOUBLE, HALF",
"RegularSpell, 10, Knight, 15, RIGHT_WINS, NONE, NONE",
})
void fightTest(String card1, float dmg1, String card2, float dmg2, fightOutcome outcome, Battle.ElementMod expectedMod1, Battle.ElementMod expectedMod2) {
Battle.FightDTO fightDTO1 = getFightDTO(card1, dmg1);
Battle.FightDTO fightDTO2 = getFightDTO(card2, dmg2);
Battle.RoundResult result = Battle.fight(fightDTO1, fightDTO2, false);
switch (outcome) {
case LEFT_WINS -> {
Assertions.assertEquals(fightDTO1, result.winner());
Assertions.assertFalse(result.draw());
}
case DRAW -> {
Assertions.assertTrue(result.draw());
}
case RIGHT_WINS -> {
Assertions.assertEquals(fightDTO2, result.winner());
Assertions.assertFalse(result.draw());
}
}
Assertions.assertEquals(expectedMod1, result.winnerMod());
Assertions.assertEquals(expectedMod2, result.loserMod());
}
}

View File

@ -0,0 +1,145 @@
package at.nanopenguin.mtcg.http;
import at.nanopenguin.mtcg.application.service.Service;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
public class RouterTest {
private static Router router;
private static Service mockedService;
private static Service mockedServiceUnwanted;
@BeforeEach
void setup() {
router = new Router();
mockedService = Mockito.mock(Service.class);
mockedServiceUnwanted = Mockito.mock(Service.class);
}
@Test
void simpleRoute() throws SQLException, JsonProcessingException {
router.addRoute(HttpMethod.GET, "/get", mockedService, new int[]{});
Service result = router.resolveRoute(HttpMethod.GET, "/get");
Assertions.assertNotNull(result);
result.handleRequest(null);
Mockito.verify(mockedService, Mockito.times(1)).handleRequest(null);
}
@Test
void wrongPath() throws SQLException, JsonProcessingException {
router.addRoute(HttpMethod.GET, "/get", mockedService, new int[]{});
Assertions.assertNull(router.resolveRoute(HttpMethod.GET, "/different"));
}
@Test
void wrongMethod() throws SQLException, JsonProcessingException {
router.addRoute(HttpMethod.GET, "/get", mockedService, new int[]{});
Service result = router.resolveRoute(HttpMethod.GET, "/get");
Assertions.assertNotNull(result);
result.handleRequest(null);
Assertions.assertNull(router.resolveRoute(HttpMethod.POST, "/get"));
Assertions.assertNull(router.resolveRoute(HttpMethod.PUT, "/get"));
Assertions.assertNull(router.resolveRoute(HttpMethod.DELETE, "/get"));
}
@Test
void differentHttpExists() throws SQLException, JsonProcessingException {
router.addRoute(HttpMethod.GET, "/get", mockedService, new int[]{});
router.addRoute(HttpMethod.POST, "/get", mockedServiceUnwanted, new int[]{});
Service result = router.resolveRoute(HttpMethod.GET, "/get");
Assertions.assertNotNull(result);
result.handleRequest(null);
Mockito.verify(mockedService, Mockito.times(1)).handleRequest(null);
Mockito.verify(mockedServiceUnwanted, Mockito.times(0)).handleRequest(null);
}
@Test
void longerPathExists() throws SQLException, JsonProcessingException {
router.addRoute(HttpMethod.GET, "/get", mockedService, new int[]{});
router.addRoute(HttpMethod.GET, "/get/long", mockedServiceUnwanted, new int[]{});
Service result = router.resolveRoute(HttpMethod.GET, "/get");
Assertions.assertNotNull(result);
result.handleRequest(null);
Mockito.verify(mockedService, Mockito.times(1)).handleRequest(null);
Mockito.verify(mockedServiceUnwanted, Mockito.times(0)).handleRequest(null);
}
@Test
void shorterPathExists() throws SQLException, JsonProcessingException {
router.addRoute(HttpMethod.GET, "/get/path", mockedService, new int[]{});
router.addRoute(HttpMethod.GET, "/get", mockedServiceUnwanted, new int[]{});
Service result = router.resolveRoute(HttpMethod.GET, "/get/path");
Assertions.assertNotNull(result);
result.handleRequest(null);
Mockito.verify(mockedService, Mockito.times(1)).handleRequest(null);
Mockito.verify(mockedServiceUnwanted, Mockito.times(0)).handleRequest(null);
}
@Test
void pathWithVar() throws SQLException, JsonProcessingException {
router.addRoute(HttpMethod.GET, "/get/{var}", mockedService, new int[]{2});
Service result = router.resolveRoute(HttpMethod.GET, "/get/value");
Assertions.assertNotNull(result);
result.handleRequest(null);
Mockito.verify(mockedService, Mockito.times(1)).handleRequest(null);
}
@Test
void longerPathWithVarExists() throws SQLException, JsonProcessingException {
router.addRoute(HttpMethod.GET, "/get", mockedService, new int[]{});
router.addRoute(HttpMethod.GET, "/get/{var}", mockedServiceUnwanted, new int[]{2});
Service result = router.resolveRoute(HttpMethod.GET, "/get");
Assertions.assertNotNull(result);
result.handleRequest(null);
Mockito.verify(mockedService, Mockito.times(1)).handleRequest(null);
Mockito.verify(mockedServiceUnwanted, Mockito.times(0)).handleRequest(null);
}
@Test
void pathBeforeVarIsValid() throws SQLException, JsonProcessingException {
router.addRoute(HttpMethod.GET, "/get/{var}", mockedService, new int[]{2});
router.addRoute(HttpMethod.GET, "/get", mockedServiceUnwanted, new int[]{});
Service result = router.resolveRoute(HttpMethod.GET, "/get/value");
Assertions.assertNotNull(result);
result.handleRequest(null);
Mockito.verify(mockedService, Mockito.times(1)).handleRequest(null);
Mockito.verify(mockedServiceUnwanted, Mockito.times(0)).handleRequest(null);
}
@Test
void pathContinuesAfterVar() throws SQLException, JsonProcessingException {
router.addRoute(HttpMethod.GET, "/get/{var}/path", mockedService, new int[]{2});
Service result = router.resolveRoute(HttpMethod.GET, "/get/value/path");
Assertions.assertNotNull(result);
result.handleRequest(null);
Mockito.verify(mockedService, Mockito.times(1)).handleRequest(null);
}
@Test
void pathWithTwoVars() throws SQLException, JsonProcessingException {
router.addRoute(HttpMethod.GET, "/get/{var}/path/{var}", mockedService, new int[]{2, 4});
Service result = router.resolveRoute(HttpMethod.GET, "/get/value/path/other_value");
Assertions.assertNotNull(result);
result.handleRequest(null);
Mockito.verify(mockedService, Mockito.times(1)).handleRequest(null);
}
}