summaryrefslogtreecommitdiff
path: root/backend
diff options
context:
space:
mode:
authorjbion <joffrey.bion@amadeus.com>2017-01-20 16:11:24 +0100
committerjbion <joffrey.bion@amadeus.com>2017-01-20 16:11:35 +0100
commitb32cdf5c4f5d0b4f31b3bcfa64fe9328ed78d818 (patch)
treebdda009d448cb6b56415d90eae90eceb138318e4 /backend
parentMerge remote-tracking branch 'origin/feature/front' (diff)
downloadseven-wonders-b32cdf5c4f5d0b4f31b3bcfa64fe9328ed78d818.tar.gz
seven-wonders-b32cdf5c4f5d0b4f31b3bcfa64fe9328ed78d818.tar.bz2
seven-wonders-b32cdf5c4f5d0b4f31b3bcfa64fe9328ed78d818.zip
Move frontend and backend into 2 separate subprojects
Diffstat (limited to 'backend')
-rw-r--r--backend/build.gradle44
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/SevenWonders.java12
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/actions/ChooseNameAction.java19
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/actions/CreateGameAction.java19
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/actions/JoinGameAction.java17
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/actions/PrepareCardAction.java16
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/actions/ReorderPlayersAction.java18
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/actions/UpdateSettingsAction.java19
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.java24
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.java38
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/config/WebSocketConfig.java51
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/controllers/GameController.java63
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/controllers/LobbyController.java170
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/errors/ApiMisuseException.java8
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/errors/ErrorType.java5
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/errors/ExceptionHandler.java81
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/errors/UIError.java54
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/errors/UIErrorDetail.java37
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/errors/UserInputException.java21
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/Game.java235
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/Lobby.java138
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/Player.java59
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/Settings.java84
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/State.java6
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/api/Action.java20
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/api/CustomizableSettings.java95
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/api/HandCard.java44
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/api/PlayerMove.java45
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/api/PlayerTurnInfo.java76
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/api/PreparedCard.java24
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/api/Table.java84
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/boards/Board.java173
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/boards/BoardElementType.java28
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/boards/Military.java54
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/boards/RelativeBoardPosition.java28
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/boards/Science.java65
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/boards/ScienceType.java5
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/cards/Card.java116
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/cards/CardBack.java14
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/cards/Color.java5
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/cards/Decks.java65
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/cards/HandRotationDirection.java20
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/cards/Hands.java64
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/cards/Requirements.java88
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/GameDefinition.java68
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/GameDefinitionLoader.java84
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/CardDefinition.java38
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/DecksDefinition.java76
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/Definition.java24
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/EffectsDefinition.java66
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderDefinition.java27
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSide.java5
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSideDefinition.java30
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethod.java36
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderStageDefinition.java21
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializer.java48
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializer.java84
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializer.java30
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializer.java38
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.java41
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializer.java64
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/effects/BonusPerBoardElement.java86
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/effects/Discount.java44
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/effects/Effect.java15
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/effects/EndGameEffect.java11
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/effects/GoldIncrease.java40
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/effects/InstantOwnBoardEffect.java20
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/effects/MilitaryReinforcements.java40
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/effects/ProductionIncrease.java41
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/effects/RawPointsIncrease.java40
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/effects/ScienceProgress.java22
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbility.java46
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbilityActivation.java26
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/moves/BuildWonderMove.java38
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/moves/CardFromHandMove.java20
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/moves/CopyGuildMove.java49
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/moves/DiscardMove.java27
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/moves/Move.java50
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/moves/MoveType.java39
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/moves/PlayCardMove.java35
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/moves/PlayFreeCardMove.java39
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/resources/BoughtResources.java24
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/resources/Production.java103
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/resources/Provider.java18
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/resources/ResourceType.java39
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/resources/Resources.java65
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/resources/TradingRules.java40
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/scoring/PlayerScore.java33
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreBoard.java24
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreCategory.java5
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/wonders/Wonder.java102
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/game/wonders/WonderStage.java50
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/repositories/GameRepository.java49
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/repositories/LobbyRepository.java59
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/repositories/PlayerRepository.java60
-rw-r--r--backend/src/main/java/org/luxons/sevenwonders/validation/DestinationAccessValidator.java76
-rw-r--r--backend/src/main/resources/org/luxons/sevenwonders/game/data/cards.json1719
-rw-r--r--backend/src/main/resources/org/luxons/sevenwonders/game/data/wonders.json515
-rw-r--r--backend/src/main/resources/static/app.js90
-rw-r--r--backend/src/main/resources/static/images/background.jpgbin0 -> 100272 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/academy.pngbin0 -> 87620 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/age1.pngbin0 -> 67850 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/age2.pngbin0 -> 68501 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/age3.pngbin0 -> 63391 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/altar.pngbin0 -> 80843 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/apothecary.pngbin0 -> 88905 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/aqueduct.pngbin0 -> 90765 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/archeryrange.pngbin0 -> 86327 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/arena.pngbin0 -> 84837 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/arsenal.pngbin0 -> 88257 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/barracks.pngbin0 -> 83840 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/baths.pngbin0 -> 84236 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/bazar.pngbin0 -> 80862 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/brickyard.pngbin0 -> 79194 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/buildersguild.pngbin0 -> 86054 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/caravansery.pngbin0 -> 85841 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/chamberofcommerce.pngbin0 -> 89136 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/circus.pngbin0 -> 95879 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/claypit.pngbin0 -> 78992 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/claypool.pngbin0 -> 76294 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/courthouse.pngbin0 -> 82399 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/craftsmensguild.pngbin0 -> 90528 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/dispensary.pngbin0 -> 86175 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/easttradingpost.pngbin0 -> 88611 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/excavation.pngbin0 -> 82667 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/forestcave.pngbin0 -> 75845 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/fortifications.pngbin0 -> 85633 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/forum.pngbin0 -> 85713 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/foundry.pngbin0 -> 78894 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/gardens.pngbin0 -> 85889 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/glassworks.pngbin0 -> 81916 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/guardtower.pngbin0 -> 77432 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/haven.pngbin0 -> 93143 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/laboratory.pngbin0 -> 87869 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/library.pngbin0 -> 80338 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/lighthouse.pngbin0 -> 79746 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/lodge.pngbin0 -> 76021 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/loom.pngbin0 -> 85480 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/lumberyard.pngbin0 -> 83067 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/magistratesguild.pngbin0 -> 88073 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/marketplace.pngbin0 -> 89816 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/mine.pngbin0 -> 83500 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/observatory.pngbin0 -> 81745 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/orevein.pngbin0 -> 82176 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/palace.pngbin0 -> 85097 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/pantheon.pngbin0 -> 83290 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/pawnshop.pngbin0 -> 83440 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/philosophersguild.pngbin0 -> 89645 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/press.pngbin0 -> 88277 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/quarry.pngbin0 -> 77177 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/sawmill.pngbin0 -> 80987 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/school.pngbin0 -> 80260 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/scientistsguild.pngbin0 -> 86768 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/scriptorium.pngbin0 -> 84987 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/senate.pngbin0 -> 91055 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/shipownersguild.pngbin0 -> 86836 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/siegeworkshop.pngbin0 -> 89072 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/spiesguild.pngbin0 -> 83823 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/stables.pngbin0 -> 85649 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/statue.pngbin0 -> 83639 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/stockade.pngbin0 -> 70706 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/stonepit.pngbin0 -> 84418 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/strategistsguild.pngbin0 -> 86575 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/study.pngbin0 -> 84016 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/tavern.pngbin0 -> 81229 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/temple.pngbin0 -> 78057 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/theater.pngbin0 -> 89703 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/timberyard.pngbin0 -> 82874 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/townhall.pngbin0 -> 84439 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/tradersguild.pngbin0 -> 88057 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/trainingground.pngbin0 -> 84102 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/treefarm.pngbin0 -> 88252 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/university.pngbin0 -> 74203 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/vineyard.pngbin0 -> 81329 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/walls.pngbin0 -> 83027 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/westtradingpost.pngbin0 -> 90680 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/workersguild.pngbin0 -> 84595 bytes
-rw-r--r--backend/src/main/resources/static/images/cards/workshop.pngbin0 -> 82116 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/buy.pngbin0 -> 5534 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/card.pngbin0 -> 4367 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/check.pngbin0 -> 3655 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/clay.pngbin0 -> 19566 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/coin.pngbin0 -> 4515 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/coin1.pngbin0 -> 6284 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/coin3.pngbin0 -> 8770 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/free.pngbin0 -> 5062 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/glass.pngbin0 -> 20961 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/linen.pngbin0 -> 21053 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/no.pngbin0 -> 5883 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/ore.pngbin0 -> 21524 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/paper.pngbin0 -> 22695 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/pyramid-stage0.pngbin0 -> 3286 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/pyramid-stage1.pngbin0 -> 4114 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/pyramid-stage2.pngbin0 -> 4285 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/pyramid-stage3.pngbin0 -> 20663 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/pyramid.pngbin0 -> 3886 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/stone.pngbin0 -> 21516 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/trash.pngbin0 -> 5146 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/victory1.pngbin0 -> 3676 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/victory3.pngbin0 -> 4786 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/victory5.pngbin0 -> 7657 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/victoryminus1.pngbin0 -> 5925 bytes
-rw-r--r--backend/src/main/resources/static/images/tokens/wood.pngbin0 -> 21642 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/alexandriaA.pngbin0 -> 334731 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/alexandriaB.pngbin0 -> 336572 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/babylonA.pngbin0 -> 372621 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/babylonB.pngbin0 -> 372561 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/ephesosA.pngbin0 -> 352173 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/ephesosB.pngbin0 -> 352272 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/extra/agrigentoA.jpgbin0 -> 705403 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/extra/angkorwatA.jpgbin0 -> 930685 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/extra/angkorwatB.jpgbin0 -> 987688 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/extra/avalonA.jpgbin0 -> 658280 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/extra/ctesiphonB.jpgbin0 -> 692738 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/extra/iramA.jpgbin0 -> 734054 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/extra/persepolisA.jpgbin0 -> 1057711 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/extra/romaA.jpgbin0 -> 200076 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/extra/sangri-laA.jpgbin0 -> 682795 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/extra/spahanA.jpgbin0 -> 774749 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/extra/the-great-wallA.jpgbin0 -> 72721 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/extra/veniseA.jpgbin0 -> 945317 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/extra/veniseB.jpgbin0 -> 941879 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/gizahA.pngbin0 -> 316028 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/gizahB.pngbin0 -> 322626 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/halikarnassusA.pngbin0 -> 311219 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/halikarnassusB.pngbin0 -> 319909 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/olympiaA.pngbin0 -> 338194 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/olympiaB.pngbin0 -> 338448 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/rhodosA.pngbin0 -> 450747 bytes
-rw-r--r--backend/src/main/resources/static/images/wonders/rhodosB.pngbin0 -> 398666 bytes
-rw-r--r--backend/src/main/resources/static/index.html64
-rw-r--r--backend/src/main/resources/static/test-ws.js40
-rw-r--r--backend/src/main/resources/static/test.html55
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/LobbyTest.java170
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/api/TableTest.java49
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/boards/BoardTest.java107
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/boards/MilitaryTest.java72
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/boards/RelativeBoardPositionTest.java44
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/boards/ScienceTest.java113
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/cards/CardBackTest.java15
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/cards/CardTest.java106
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/cards/DecksTest.java110
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/cards/HandRotationDirectionTest.java15
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/cards/HandsTest.java141
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/data/GameDefinitionLoaderTest.java16
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/data/GameDefinitionTest.java25
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethodTest.java96
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializerTest.java128
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializerTest.java203
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializerTest.java50
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializerTest.java100
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializerTest.java107
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializerTest.java145
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/effects/BonusPerBoardElementTest.java139
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/effects/DiscountTest.java72
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/effects/GoldIncreaseTest.java78
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/effects/MilitaryReinforcementsTest.java79
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/effects/ProductionIncreaseTest.java85
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/effects/RawPointsIncreaseTest.java61
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/effects/ScienceProgressTest.java38
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/effects/SpecialAbilityActivationTest.java94
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/resources/ProductionTest.java271
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/resources/ResourcesTest.java431
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/resources/TradingRulesTest.java96
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/game/test/TestUtils.java191
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/repositories/GameRepositoryTest.java61
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.java77
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.java73
-rw-r--r--backend/src/test/java/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.java147
269 files changed, 10894 insertions, 0 deletions
diff --git a/backend/build.gradle b/backend/build.gradle
new file mode 100644
index 00000000..61e8960b
--- /dev/null
+++ b/backend/build.gradle
@@ -0,0 +1,44 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.4.2.RELEASE'
+ }
+}
+
+apply plugin: 'java'
+apply plugin: 'idea'
+apply plugin: 'org.springframework.boot'
+
+group 'org.luxons'
+version '1.0-SNAPSHOT'
+
+sourceCompatibility = 1.8
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ compile 'com.google.code.gson:gson:2.8.0'
+ compile 'ch.qos.logback:logback-classic:1.1.8'
+
+ compile 'org.springframework.boot:spring-boot-starter-websocket'
+ compile 'org.springframework.security:spring-security-core:4.2.0.RELEASE'
+ compile 'org.webjars:webjars-locator'
+ compile 'org.webjars:sockjs-client:1.0.2'
+ compile 'org.webjars:stomp-websocket:2.3.3'
+ compile 'org.webjars:bootstrap:3.3.7'
+ compile 'org.webjars:jquery:3.1.0'
+
+ testCompile 'org.springframework.boot:spring-boot-starter-test'
+}
+
+jar {
+ from('../frontend/dist') {
+ into 'static'
+ }
+}
+
+jar.dependsOn(':frontend:assemble') \ No newline at end of file
diff --git a/backend/src/main/java/org/luxons/sevenwonders/SevenWonders.java b/backend/src/main/java/org/luxons/sevenwonders/SevenWonders.java
new file mode 100644
index 00000000..2c20c5d3
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/SevenWonders.java
@@ -0,0 +1,12 @@
+package org.luxons.sevenwonders;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class SevenWonders {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SevenWonders.class, args);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/actions/ChooseNameAction.java b/backend/src/main/java/org/luxons/sevenwonders/actions/ChooseNameAction.java
new file mode 100644
index 00000000..42a26f37
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/actions/ChooseNameAction.java
@@ -0,0 +1,19 @@
+package org.luxons.sevenwonders.actions;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+public class ChooseNameAction {
+
+ @NotNull
+ @Size(min=2, max=20)
+ private String playerName;
+
+ public String getPlayerName() {
+ return playerName;
+ }
+
+ public void setPlayerName(String playerName) {
+ this.playerName = playerName;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/actions/CreateGameAction.java b/backend/src/main/java/org/luxons/sevenwonders/actions/CreateGameAction.java
new file mode 100644
index 00000000..ce1783c0
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/actions/CreateGameAction.java
@@ -0,0 +1,19 @@
+package org.luxons.sevenwonders.actions;
+
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+public class CreateGameAction {
+
+ @NotNull
+ @Size(min=2, max=30)
+ private String gameName;
+
+ public String getGameName() {
+ return gameName;
+ }
+
+ public void setGameName(String gameName) {
+ this.gameName = gameName;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/actions/JoinGameAction.java b/backend/src/main/java/org/luxons/sevenwonders/actions/JoinGameAction.java
new file mode 100644
index 00000000..82bff168
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/actions/JoinGameAction.java
@@ -0,0 +1,17 @@
+package org.luxons.sevenwonders.actions;
+
+import javax.validation.constraints.NotNull;
+
+public class JoinGameAction {
+
+ @NotNull
+ private Long gameId;
+
+ public Long getGameId() {
+ return gameId;
+ }
+
+ public void setGameId(Long gameId) {
+ this.gameId = gameId;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/actions/PrepareCardAction.java b/backend/src/main/java/org/luxons/sevenwonders/actions/PrepareCardAction.java
new file mode 100644
index 00000000..b333d6c1
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/actions/PrepareCardAction.java
@@ -0,0 +1,16 @@
+package org.luxons.sevenwonders.actions;
+
+import org.luxons.sevenwonders.game.api.PlayerMove;
+
+public class PrepareCardAction {
+
+ private PlayerMove move;
+
+ public PlayerMove getMove() {
+ return move;
+ }
+
+ public void setMove(PlayerMove move) {
+ this.move = move;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/actions/ReorderPlayersAction.java b/backend/src/main/java/org/luxons/sevenwonders/actions/ReorderPlayersAction.java
new file mode 100644
index 00000000..803a71d8
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/actions/ReorderPlayersAction.java
@@ -0,0 +1,18 @@
+package org.luxons.sevenwonders.actions;
+
+import java.util.List;
+import javax.validation.constraints.NotNull;
+
+public class ReorderPlayersAction {
+
+ @NotNull
+ private List<String> orderedPlayers;
+
+ public List<String> getOrderedPlayers() {
+ return orderedPlayers;
+ }
+
+ public void setOrderedPlayers(List<String> orderedPlayers) {
+ this.orderedPlayers = orderedPlayers;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/actions/UpdateSettingsAction.java b/backend/src/main/java/org/luxons/sevenwonders/actions/UpdateSettingsAction.java
new file mode 100644
index 00000000..822a5a1c
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/actions/UpdateSettingsAction.java
@@ -0,0 +1,19 @@
+package org.luxons.sevenwonders.actions;
+
+import javax.validation.constraints.NotNull;
+
+import org.luxons.sevenwonders.game.api.CustomizableSettings;
+
+public class UpdateSettingsAction {
+
+ @NotNull
+ private CustomizableSettings settings;
+
+ public CustomizableSettings getSettings() {
+ return settings;
+ }
+
+ public void setSettings(CustomizableSettings settings) {
+ this.settings = settings;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.java b/backend/src/main/java/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.java
new file mode 100644
index 00000000..bebbd477
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/config/AnonymousUsersHandshakeHandler.java
@@ -0,0 +1,24 @@
+package org.luxons.sevenwonders.config;
+
+import java.security.Principal;
+import java.util.Map;
+
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
+
+class AnonymousUsersHandshakeHandler extends DefaultHandshakeHandler {
+
+ private int playerId = 0;
+
+ @Override
+ public Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler,
+ Map<String, Object> attributes) {
+ Principal p = super.determineUser(request, wsHandler, attributes);
+ if (p == null) {
+ p = new UsernamePasswordAuthenticationToken("player" + playerId++, null);
+ }
+ return p;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.java b/backend/src/main/java/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.java
new file mode 100644
index 00000000..f8d92068
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/config/TopicSubscriptionInterceptor.java
@@ -0,0 +1,38 @@
+package org.luxons.sevenwonders.config;
+
+import java.security.Principal;
+
+import org.luxons.sevenwonders.validation.DestinationAccessValidator;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.simp.stomp.StompCommand;
+import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
+import org.springframework.messaging.support.ChannelInterceptorAdapter;
+import org.springframework.stereotype.Component;
+
+@Component
+public class TopicSubscriptionInterceptor extends ChannelInterceptorAdapter {
+
+ private final DestinationAccessValidator destinationAccessValidator;
+
+ @Autowired
+ public TopicSubscriptionInterceptor(DestinationAccessValidator destinationAccessValidator) {
+ this.destinationAccessValidator = destinationAccessValidator;
+ }
+
+ @Override
+ public Message<?> preSend(Message<?> message, MessageChannel channel) {
+ StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);
+ if (StompCommand.SUBSCRIBE.equals(headerAccessor.getCommand())) {
+ Principal userPrincipal = headerAccessor.getUser();
+ if (!destinationAccessValidator.hasAccess(userPrincipal.getName(), headerAccessor.getDestination())) {
+ throw new ForbiddenSubscriptionException();
+ }
+ }
+ return message;
+ }
+
+ private static class ForbiddenSubscriptionException extends RuntimeException {
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/config/WebSocketConfig.java b/backend/src/main/java/org/luxons/sevenwonders/config/WebSocketConfig.java
new file mode 100644
index 00000000..d54d8da4
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/config/WebSocketConfig.java
@@ -0,0 +1,51 @@
+package org.luxons.sevenwonders.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.simp.config.ChannelRegistration;
+import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
+import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
+import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
+import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
+
+@Configuration
+@EnableWebSocketMessageBroker
+public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
+
+ private final TopicSubscriptionInterceptor topicSubscriptionInterceptor;
+
+ @Autowired
+ public WebSocketConfig(TopicSubscriptionInterceptor topicSubscriptionInterceptor) {
+ this.topicSubscriptionInterceptor = topicSubscriptionInterceptor;
+ }
+
+ @Override
+ public void configureMessageBroker(MessageBrokerRegistry config) {
+ // prefixes for all subscriptions
+ config.enableSimpleBroker("/queue", "/topic");
+ config.setUserDestinationPrefix("/user");
+
+ // /app for normal calls, /topic for subscription events
+ config.setApplicationDestinationPrefixes("/app", "/topic");
+ }
+
+ @Override
+ public void registerStompEndpoints(StompEndpointRegistry registry) {
+ registry.addEndpoint("/seven-wonders-websocket")
+ .setHandshakeHandler(handshakeHandler())
+ .setAllowedOrigins("http://localhost:3000") // to allow frontend server proxy requests in dev mode
+ .withSockJS();
+ }
+
+ @Bean
+ public DefaultHandshakeHandler handshakeHandler() {
+ return new AnonymousUsersHandshakeHandler();
+ }
+
+ @Override
+ public void configureClientInboundChannel(ChannelRegistration registration) {
+ registration.setInterceptors(topicSubscriptionInterceptor);
+ }
+} \ No newline at end of file
diff --git a/backend/src/main/java/org/luxons/sevenwonders/controllers/GameController.java b/backend/src/main/java/org/luxons/sevenwonders/controllers/GameController.java
new file mode 100644
index 00000000..0deac4a3
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/controllers/GameController.java
@@ -0,0 +1,63 @@
+package org.luxons.sevenwonders.controllers;
+
+import java.security.Principal;
+import java.util.List;
+
+import org.luxons.sevenwonders.actions.PrepareCardAction;
+import org.luxons.sevenwonders.game.Game;
+import org.luxons.sevenwonders.game.Player;
+import org.luxons.sevenwonders.game.api.PlayerTurnInfo;
+import org.luxons.sevenwonders.game.api.PreparedCard;
+import org.luxons.sevenwonders.repositories.GameRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.messaging.handler.annotation.DestinationVariable;
+import org.springframework.messaging.handler.annotation.MessageMapping;
+import org.springframework.messaging.simp.SimpMessagingTemplate;
+import org.springframework.stereotype.Controller;
+
+@Controller
+public class GameController {
+
+ private static final Logger logger = LoggerFactory.getLogger(GameController.class);
+
+ private final SimpMessagingTemplate template;
+
+ private final GameRepository gameRepository;
+
+ @Autowired
+ public GameController(SimpMessagingTemplate template, GameRepository gameRepository) {
+ this.template = template;
+ this.gameRepository = gameRepository;
+ }
+
+ @MessageMapping("/game/{gameId}/prepare")
+ public void prepareCard(@DestinationVariable long gameId, PrepareCardAction action, Principal principal) {
+ Game game = gameRepository.find(gameId);
+ PreparedCard preparedCard = game.prepareCard(principal.getName(), action.getMove());
+ logger.info("Game '{}': player {} prepared move {}", gameId, principal.getName(), action.getMove());
+
+ if (game.areAllPlayersReady()) {
+ game.playTurn();
+ sendTurnInfo(game);
+ } else {
+ sendPreparedCard(preparedCard, game);
+ }
+ }
+
+ private void sendPreparedCard(PreparedCard preparedCard, Game game) {
+ for (Player player : game.getPlayers()) {
+ String username = player.getUsername();
+ template.convertAndSendToUser(username, "/topic/game/" + game.getId() + "/prepared", preparedCard);
+ }
+ }
+
+ private void sendTurnInfo(Game game) {
+ List<PlayerTurnInfo> turnInfos = game.getTurnInfo();
+ for (PlayerTurnInfo turnInfo : turnInfos) {
+ String username = turnInfo.getPlayer().getUsername();
+ template.convertAndSendToUser(username, "/topic/game/" + game.getId() + "/turn", turnInfo);
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/controllers/LobbyController.java b/backend/src/main/java/org/luxons/sevenwonders/controllers/LobbyController.java
new file mode 100644
index 00000000..996ea361
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/controllers/LobbyController.java
@@ -0,0 +1,170 @@
+package org.luxons.sevenwonders.controllers;
+
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.luxons.sevenwonders.actions.ChooseNameAction;
+import org.luxons.sevenwonders.actions.CreateGameAction;
+import org.luxons.sevenwonders.actions.JoinGameAction;
+import org.luxons.sevenwonders.actions.ReorderPlayersAction;
+import org.luxons.sevenwonders.actions.UpdateSettingsAction;
+import org.luxons.sevenwonders.errors.ApiMisuseException;
+import org.luxons.sevenwonders.game.Game;
+import org.luxons.sevenwonders.game.Lobby;
+import org.luxons.sevenwonders.game.Player;
+import org.luxons.sevenwonders.repositories.GameRepository;
+import org.luxons.sevenwonders.repositories.LobbyRepository;
+import org.luxons.sevenwonders.repositories.PlayerRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.messaging.handler.annotation.MessageMapping;
+import org.springframework.messaging.handler.annotation.SendTo;
+import org.springframework.messaging.simp.SimpMessagingTemplate;
+import org.springframework.messaging.simp.annotation.SendToUser;
+import org.springframework.messaging.simp.annotation.SubscribeMapping;
+import org.springframework.stereotype.Controller;
+import org.springframework.validation.annotation.Validated;
+
+@Controller
+public class LobbyController {
+
+ private static final Logger logger = LoggerFactory.getLogger(LobbyController.class);
+
+ private final LobbyRepository lobbyRepository;
+
+ private final GameRepository gameRepository;
+
+ private final PlayerRepository playerRepository;
+
+ private final SimpMessagingTemplate template;
+
+ @Autowired
+ public LobbyController(LobbyRepository lobbyRepository, GameRepository gameRepository,
+ PlayerRepository playerRepository, SimpMessagingTemplate template) {
+ this.lobbyRepository = lobbyRepository;
+ this.gameRepository = gameRepository;
+ this.playerRepository = playerRepository;
+ this.template = template;
+ }
+
+ @MessageMapping("/chooseName")
+ @SendToUser("/queue/nameChoice")
+ public Player chooseName(@Validated ChooseNameAction action, Principal principal) {
+ String username = principal.getName();
+ Player player = playerRepository.createOrUpdate(username, action.getPlayerName());
+
+ logger.info("Player '{}' chose the name '{}'", username, player.getDisplayName());
+ return player;
+ }
+
+ @SubscribeMapping("/games") // prefix /topic not shown
+ public Collection<Lobby> listGames(Principal principal) {
+ logger.info("Player '{}' subscribed to /topic/games", principal.getName());
+ return lobbyRepository.list();
+ }
+
+ @MessageMapping("/lobby/create")
+ @SendTo("/topic/games")
+ public Collection<Lobby> createGame(@Validated CreateGameAction action, Principal principal) {
+ checkThatUserIsNotInAGame(principal, "cannot create another game");
+
+ Player gameOwner = playerRepository.find(principal.getName());
+ Lobby lobby = lobbyRepository.create(action.getGameName(), gameOwner);
+ gameOwner.setLobby(lobby);
+
+ logger.info("Game '{}' ({}) created by {} ({})", lobby.getName(), lobby.getId(), gameOwner.getDisplayName(),
+ gameOwner.getUsername());
+ return Collections.singletonList(lobby);
+ }
+
+ @MessageMapping("/lobby/join")
+ @SendToUser("/queue/lobby/joined")
+ public Lobby joinGame(@Validated JoinGameAction action, Principal principal) {
+ checkThatUserIsNotInAGame(principal, "cannot join another game");
+
+ Lobby lobby = lobbyRepository.find(action.getGameId());
+ Player newPlayer = playerRepository.find(principal.getName());
+ lobby.addPlayer(newPlayer);
+ newPlayer.setLobby(lobby);
+
+ logger.info("Player '{}' ({}) joined game {}", newPlayer.getDisplayName(), newPlayer.getUsername(),
+ lobby.getName());
+ sendLobbyUpdateToPlayers(lobby);
+ return lobby;
+ }
+
+ private void checkThatUserIsNotInAGame(Principal principal, String impossibleActionDescription) {
+ Lobby lobby = playerRepository.find(principal.getName()).getLobby();
+ if (lobby != null) {
+ throw new UserAlreadyInGameException(lobby.getName(), impossibleActionDescription);
+ }
+ }
+
+ @MessageMapping("/lobby/reorderPlayers")
+ public void reorderPlayers(@Validated ReorderPlayersAction action, Principal principal) {
+ Lobby lobby = getLobby(principal);
+ lobby.reorderPlayers(action.getOrderedPlayers());
+
+ logger.info("Players in game {} reordered to {}", lobby.getName(), action.getOrderedPlayers());
+ sendLobbyUpdateToPlayers(lobby);
+ }
+
+ @MessageMapping("/lobby/updateSettings")
+ public void updateSettings(@Validated UpdateSettingsAction action, Principal principal) {
+ Lobby lobby = getLobby(principal);
+ lobby.setSettings(action.getSettings());
+
+ logger.info("Updated settings of game {}", lobby.getName());
+ sendLobbyUpdateToPlayers(lobby);
+ }
+
+ private void sendLobbyUpdateToPlayers(Lobby lobby) {
+ template.convertAndSend("/topic/lobby/" + lobby.getId() + "/updated", lobby);
+ }
+
+ @MessageMapping("/lobby/start")
+ public void startGame(Principal principal) {
+ Lobby lobby = getOwnedLobby(principal);
+ Game game = lobby.startGame();
+ gameRepository.add(game);
+
+ logger.info("Game {} successfully started", game.getId());
+ template.convertAndSend("/topic/lobby/" + lobby.getId() + "/started", (Object)null);
+ }
+
+ private Lobby getOwnedLobby(Principal principal) {
+ Lobby lobby = getLobby(principal);
+ if (!lobby.isOwner(principal.getName())) {
+ throw new UserIsNotOwnerException(principal.getName());
+ }
+ return lobby;
+ }
+
+ private Lobby getLobby(Principal principal) {
+ Lobby lobby = playerRepository.find(principal.getName()).getLobby();
+ if (lobby == null) {
+ throw new UserNotInLobbyException(principal.getName());
+ }
+ return lobby;
+ }
+
+ private static class UserNotInLobbyException extends ApiMisuseException {
+ UserNotInLobbyException(String username) {
+ super("User " + username + " is not in a lobby, create or join a game first");
+ }
+ }
+
+ private static class UserIsNotOwnerException extends ApiMisuseException {
+ UserIsNotOwnerException(String username) {
+ super("User " + username + " does not own the lobby he's in");
+ }
+ }
+
+ private static class UserAlreadyInGameException extends ApiMisuseException {
+ UserAlreadyInGameException(String gameName, String impossibleActionDescription) {
+ super("Client already in game '" + gameName + "', " + impossibleActionDescription);
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/errors/ApiMisuseException.java b/backend/src/main/java/org/luxons/sevenwonders/errors/ApiMisuseException.java
new file mode 100644
index 00000000..0d7d1a82
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/errors/ApiMisuseException.java
@@ -0,0 +1,8 @@
+package org.luxons.sevenwonders.errors;
+
+public class ApiMisuseException extends RuntimeException {
+
+ public ApiMisuseException(String message) {
+ super(message);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/errors/ErrorType.java b/backend/src/main/java/org/luxons/sevenwonders/errors/ErrorType.java
new file mode 100644
index 00000000..1cd18d09
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/errors/ErrorType.java
@@ -0,0 +1,5 @@
+package org.luxons.sevenwonders.errors;
+
+enum ErrorType {
+ USER_INPUT, VALIDATION, CLIENT, SERVER
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/errors/ExceptionHandler.java b/backend/src/main/java/org/luxons/sevenwonders/errors/ExceptionHandler.java
new file mode 100644
index 00000000..628da4f8
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/errors/ExceptionHandler.java
@@ -0,0 +1,81 @@
+package org.luxons.sevenwonders.errors;
+
+import java.util.List;
+import java.util.Locale;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.MessageSource;
+import org.springframework.messaging.converter.MessageConversionException;
+import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
+import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException;
+import org.springframework.messaging.simp.annotation.SendToUser;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.ObjectError;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+
+@ControllerAdvice
+public class ExceptionHandler {
+
+ private static final Logger logger = LoggerFactory.getLogger(ExceptionHandler.class);
+
+ private static final String ERROR_CODE_VALIDATION = "INVALID_DATA";
+
+ private static final String ERROR_CODE_CONVERSION = "INVALID_MESSAGE_FORMAT";
+
+ private static final String ERROR_MSG_VALIDATION = "Invalid input data";
+
+ private static final String ERROR_MSG_CONVERSION = "Invalid input format";
+
+ private final MessageSource messageSource;
+
+ @Autowired
+ public ExceptionHandler(MessageSource messageSource) {
+ this.messageSource = messageSource;
+ }
+
+ @MessageExceptionHandler
+ @SendToUser("/queue/errors")
+ public UIError handleUserInputError(UserInputException exception) {
+ logger.error("Incorrect user input: " + exception.getMessage());
+ String messageKey = exception.getMessageResourceKey();
+ String message = messageSource.getMessage(messageKey, exception.getParams(), messageKey, Locale.US);
+ return new UIError(messageKey, message, ErrorType.USER_INPUT);
+ }
+
+ @MessageExceptionHandler
+ @SendToUser("/queue/errors")
+ public UIError handleValidationError(MethodArgumentNotValidException exception) {
+ logger.error("Invalid input", exception);
+ UIError uiError = new UIError(ERROR_CODE_VALIDATION, ERROR_MSG_VALIDATION, ErrorType.VALIDATION);
+
+ BindingResult result = exception.getBindingResult();
+ if (result != null) {
+ List<ObjectError> errors = result.getAllErrors();
+ uiError.addDetails(errors);
+ }
+ return uiError;
+ }
+
+ @MessageExceptionHandler
+ @SendToUser("/queue/errors")
+ public UIError handleConversionError(MessageConversionException exception) {
+ logger.error("Error interpreting the message", exception);
+ return new UIError(ERROR_CODE_CONVERSION, ERROR_MSG_CONVERSION, ErrorType.VALIDATION);
+ }
+
+ @MessageExceptionHandler
+ @SendToUser("/queue/errors")
+ public UIError handleApiError(ApiMisuseException exception) {
+ logger.error("Invalid API input", exception);
+ return new UIError(exception.getClass().getSimpleName(), exception.getMessage(), ErrorType.CLIENT);
+ }
+
+ @MessageExceptionHandler
+ @SendToUser("/queue/errors")
+ public UIError handleUnexpectedInternalError(Throwable exception) {
+ logger.error("Uncaught exception thrown during message handling", exception);
+ return new UIError(exception.getClass().getSimpleName(), exception.getMessage(), ErrorType.SERVER);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/errors/UIError.java b/backend/src/main/java/org/luxons/sevenwonders/errors/UIError.java
new file mode 100644
index 00000000..ee5fcbe0
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/errors/UIError.java
@@ -0,0 +1,54 @@
+package org.luxons.sevenwonders.errors;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.validation.FieldError;
+import org.springframework.validation.ObjectError;
+
+public class UIError {
+
+ private final String code;
+
+ private final String message;
+
+ private final ErrorType type;
+
+ private List<UIErrorDetail> details = new ArrayList<>();
+
+ UIError(String code, String message, ErrorType type) {
+ this.code = code;
+ this.message = message;
+ this.type = type;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public ErrorType getType() {
+ return type;
+ }
+
+ public List<UIErrorDetail> getDetails() {
+ return details;
+ }
+
+ void addDetails(List<ObjectError> objectErrors) {
+ for (ObjectError objectError : objectErrors) {
+ this.details.add(convertError(objectError));
+ }
+ }
+
+ private UIErrorDetail convertError(ObjectError objectError) {
+ if (objectError instanceof FieldError) {
+ return new UIErrorDetail((FieldError)objectError);
+ } else {
+ return new UIErrorDetail(objectError);
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/errors/UIErrorDetail.java b/backend/src/main/java/org/luxons/sevenwonders/errors/UIErrorDetail.java
new file mode 100644
index 00000000..dc4250bb
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/errors/UIErrorDetail.java
@@ -0,0 +1,37 @@
+package org.luxons.sevenwonders.errors;
+
+import org.springframework.validation.FieldError;
+import org.springframework.validation.ObjectError;
+
+class UIErrorDetail {
+
+ private final Object rejectedValue;
+
+ private final String path;
+
+ private final String message;
+
+ UIErrorDetail(FieldError error) {
+ rejectedValue = error.getRejectedValue();
+ path = error.getObjectName() + '.' + error.getField();
+ message = "Invalid value for field '" + error.getField() + "': " + error.getDefaultMessage();
+ }
+
+ UIErrorDetail(ObjectError error) {
+ rejectedValue = null;
+ path = error.getObjectName();
+ message = "Invalid value for object '" + error.getObjectName() + "': " + error.getDefaultMessage();
+ }
+
+ public Object getRejectedValue() {
+ return rejectedValue;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/errors/UserInputException.java b/backend/src/main/java/org/luxons/sevenwonders/errors/UserInputException.java
new file mode 100644
index 00000000..4033a696
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/errors/UserInputException.java
@@ -0,0 +1,21 @@
+package org.luxons.sevenwonders.errors;
+
+public class UserInputException extends RuntimeException {
+
+ private final String messageResourceKey;
+
+ private final Object[] params;
+
+ public UserInputException(String messageResourceKey, Object... params) {
+ this.messageResourceKey = messageResourceKey;
+ this.params = params;
+ }
+
+ String getMessageResourceKey() {
+ return messageResourceKey;
+ }
+
+ Object[] getParams() {
+ return params;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/Game.java b/backend/src/main/java/org/luxons/sevenwonders/game/Game.java
new file mode 100644
index 00000000..8aa7d1b9
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/Game.java
@@ -0,0 +1,235 @@
+package org.luxons.sevenwonders.game;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.luxons.sevenwonders.game.api.Action;
+import org.luxons.sevenwonders.game.api.HandCard;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.PlayerTurnInfo;
+import org.luxons.sevenwonders.game.api.PreparedCard;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Card;
+import org.luxons.sevenwonders.game.cards.Decks;
+import org.luxons.sevenwonders.game.cards.Hands;
+import org.luxons.sevenwonders.game.effects.SpecialAbility;
+import org.luxons.sevenwonders.game.moves.Move;
+import org.luxons.sevenwonders.game.scoring.ScoreBoard;
+
+public class Game {
+
+ private static final int LAST_AGE = 3;
+
+ private final long id;
+
+ private final Settings settings;
+
+ private final List<Player> players;
+
+ private final Table table;
+
+ private final Decks decks;
+
+ private final List<Card> discardedCards;
+
+ private final Map<Integer, Move> preparedMoves;
+
+ private Hands hands;
+
+ public Game(long id, Settings settings, List<Player> players, List<Board> boards, Decks decks) {
+ this.id = id;
+ this.settings = settings;
+ this.players = players;
+ this.table = new Table(boards);
+ this.decks = decks;
+ this.discardedCards = new ArrayList<>();
+ this.preparedMoves = new HashMap<>();
+ startNewAge();
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public boolean containsUser(String username) {
+ return players.stream().anyMatch(p -> p.getUsername().equals(username));
+ }
+
+ public List<Player> getPlayers() {
+ return players;
+ }
+
+ private void startNewAge() {
+ table.increaseCurrentAge();
+ hands = decks.deal(table.getCurrentAge(), table.getNbPlayers());
+ }
+
+ public List<PlayerTurnInfo> getTurnInfo() {
+ return players.stream().map(this::createPlayerTurnInfo).collect(Collectors.toList());
+ }
+
+ private PlayerTurnInfo createPlayerTurnInfo(Player player) {
+ PlayerTurnInfo pti = new PlayerTurnInfo(player, table);
+ List<HandCard> hand = hands.createHand(table, player.getIndex());
+ pti.setHand(hand);
+ Action action = determineAction(hand, table.getBoard(player.getIndex()));
+ pti.setAction(action);
+ pti.setMessage(action.getMessage());
+ return pti;
+ }
+
+ private Action determineAction(List<HandCard> hand, Board board) {
+ if (endOfGameReached() && board.hasSpecial(SpecialAbility.COPY_GUILD)) {
+ return Action.PICK_NEIGHBOR_GUILD;
+ } else if (hand.size() == 1 && board.hasSpecial(SpecialAbility.PLAY_LAST_CARD)) {
+ return Action.PLAY_LAST;
+ } else if (hand.size() == 2 && board.hasSpecial(SpecialAbility.PLAY_LAST_CARD)) {
+ return Action.PLAY_2;
+ } else if (hand.isEmpty()) {
+ return Action.WAIT;
+ } else {
+ return Action.PLAY;
+ }
+ }
+
+ public PreparedCard prepareCard(String username, PlayerMove playerMove) throws InvalidMoveException {
+ Player player = getPlayer(username);
+ Card card = decks.getCard(playerMove.getCardName());
+ Move move = playerMove.getType().resolve(player.getIndex(), card, playerMove);
+ validate(move);
+ preparedMoves.put(player.getIndex(), move);
+ return new PreparedCard(player, card.getBack());
+ }
+
+ private Player getPlayer(String username) {
+ return players.stream()
+ .filter(p -> p.getUsername().equals(username))
+ .findAny()
+ .orElseThrow(() -> new UnknownPlayerException(username));
+ }
+
+ private void validate(Move move) throws InvalidMoveException {
+ List<Card> hand = hands.get(move.getPlayerIndex());
+ if (!move.isValid(table, hand)) {
+ throw new InvalidMoveException(
+ "Player " + move.getPlayerIndex() + " cannot play the card " + move.getCard().getName());
+ }
+ }
+
+ public boolean areAllPlayersReady() {
+ return preparedMoves.size() == players.size();
+ }
+
+ public void playTurn() {
+ makeMoves();
+ if (endOfAgeReached()) {
+ executeEndOfAgeEvents();
+ if (!endOfGameReached()) {
+ startNewAge();
+ }
+ } else if (!hands.maxOneCardRemains()) {
+ // we don't rotate hands if some player can play his last card (with the special ability)
+ hands.rotate(table.getHandRotationDirection());
+ }
+ }
+
+ private void makeMoves() {
+ List<Move> playedMoves = mapToList(preparedMoves);
+
+ // all cards from this turn need to be placed before executing any effect
+ // because effects depending on played cards need to take the ones from the current turn into account too
+ placePreparedCards(playedMoves);
+
+ // same goes for the discarded cards during the last turn, which should be available for special actions
+ if (hands.maxOneCardRemains()) {
+ discardLastCardsOfHands();
+ }
+
+ activatePlayedCards(playedMoves);
+
+ table.setLastPlayedMoves(playedMoves);
+ preparedMoves.clear();
+ }
+
+ private static List<Move> mapToList(Map<Integer, Move> movesPerPlayer) {
+ List<Move> moves = new ArrayList<>(movesPerPlayer.size());
+ for (int p = 0; p < movesPerPlayer.size(); p++) {
+ Move move = movesPerPlayer.get(p);
+ if (move == null) {
+ throw new MissingPreparedMoveException(p);
+ }
+ moves.add(move);
+ }
+ return moves;
+ }
+
+ private void placePreparedCards(List<Move> playedMoves) {
+ playedMoves.forEach(move -> {
+ move.place(table, discardedCards, settings);
+ removeFromHand(move.getPlayerIndex(), move.getCard());
+ });
+ }
+
+ private void discardLastCardsOfHands() {
+ for (Player p : players) {
+ Board board = table.getBoard(p.getIndex());
+ if (!board.hasSpecial(SpecialAbility.PLAY_LAST_CARD)) {
+ discardHand(p.getIndex());
+ }
+ }
+ }
+
+ private void discardHand(int playerIndex) {
+ List<Card> hand = hands.get(playerIndex);
+ discardedCards.addAll(hand);
+ hand.clear();
+ }
+
+ private void removeFromHand(int playerIndex, Card card) {
+ hands.get(playerIndex).remove(card);
+ }
+
+ private void activatePlayedCards(List<Move> playedMoves) {
+ playedMoves.forEach(move -> move.activate(table, discardedCards, settings));
+ }
+
+ private boolean endOfAgeReached() {
+ return hands.isEmpty();
+ }
+
+ private void executeEndOfAgeEvents() {
+ table.resolveMilitaryConflicts();
+ }
+
+ private boolean endOfGameReached() {
+ return endOfAgeReached() && table.getCurrentAge() == LAST_AGE;
+ }
+
+ public ScoreBoard computeScore() {
+ ScoreBoard scoreBoard = new ScoreBoard();
+ table.getBoards().stream().map(b -> b.computePoints(table)).forEach(scoreBoard::add);
+ return scoreBoard;
+ }
+
+ private static class MissingPreparedMoveException extends IllegalStateException {
+ MissingPreparedMoveException(int playerIndex) {
+ super("Player " + playerIndex + " is not ready to play");
+ }
+ }
+
+ private static class UnknownPlayerException extends IllegalArgumentException {
+ UnknownPlayerException(String username) {
+ super(username);
+ }
+ }
+
+ private static class InvalidMoveException extends IllegalArgumentException {
+ InvalidMoveException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/Lobby.java b/backend/src/main/java/org/luxons/sevenwonders/game/Lobby.java
new file mode 100644
index 00000000..6975349a
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/Lobby.java
@@ -0,0 +1,138 @@
+package org.luxons.sevenwonders.game;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.api.CustomizableSettings;
+import org.luxons.sevenwonders.game.data.GameDefinition;
+
+public class Lobby {
+
+ private final long id;
+
+ private final String name;
+
+ private final Player owner;
+
+ private final GameDefinition gameDefinition;
+
+ private final List<Player> players;
+
+ private CustomizableSettings settings;
+
+ private State state = State.LOBBY;
+
+ public Lobby(long id, String name, Player owner, GameDefinition gameDefinition) {
+ this.id = id;
+ this.name = name;
+ this.owner = owner;
+ this.gameDefinition = gameDefinition;
+ this.players = new ArrayList<>(gameDefinition.getMinPlayers());
+ this.settings = new CustomizableSettings();
+ players.add(owner);
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public List<Player> getPlayers() {
+ return players;
+ }
+
+ public CustomizableSettings getSettings() {
+ return settings;
+ }
+
+ public void setSettings(CustomizableSettings settings) {
+ this.settings = settings;
+ }
+
+ public synchronized void addPlayer(Player player) throws GameAlreadyStartedException, PlayerOverflowException {
+ if (hasStarted()) {
+ throw new GameAlreadyStartedException();
+ }
+ if (maxPlayersReached()) {
+ throw new PlayerOverflowException();
+ }
+ if (playerNameAlreadyUsed(player.getDisplayName())) {
+ throw new PlayerNameAlreadyUsedException(player.getDisplayName());
+ }
+ player.setIndex(players.size());
+ players.add(player);
+ }
+
+ private boolean hasStarted() {
+ return state != State.LOBBY;
+ }
+
+ private boolean maxPlayersReached() {
+ return players.size() >= gameDefinition.getMaxPlayers();
+ }
+
+ private boolean playerNameAlreadyUsed(String name) {
+ return players.stream().anyMatch(p -> p.getDisplayName().equals(name));
+ }
+
+ public synchronized Game startGame() throws PlayerUnderflowException {
+ if (!hasEnoughPlayers()) {
+ throw new PlayerUnderflowException();
+ }
+ state = State.PLAYING;
+ return gameDefinition.initGame(id, settings, players);
+ }
+
+ private boolean hasEnoughPlayers() {
+ return players.size() >= gameDefinition.getMinPlayers();
+ }
+
+ public void reorderPlayers(List<String> orderedUsernames) {
+ List<Player> formerList = new ArrayList<>(players);
+ players.clear();
+ for (int i = 0; i < orderedUsernames.size(); i++) {
+ Player player = getPlayer(formerList, orderedUsernames.get(i));
+ players.add(player);
+ player.setIndex(i);
+ }
+ }
+
+ private static Player getPlayer(List<Player> players, String username) {
+ return players.stream()
+ .filter(p -> p.getUsername().equals(username))
+ .findAny()
+ .orElseThrow(() -> new UnknownPlayerException(username));
+ }
+
+ public boolean isOwner(String username) {
+ return owner.getUsername().equals(username);
+ }
+
+ public boolean containsUser(String username) {
+ return players.stream().anyMatch(p -> p.getUsername().equals(username));
+ }
+
+ static class GameAlreadyStartedException extends IllegalStateException {
+ }
+
+ static class PlayerOverflowException extends IllegalStateException {
+ }
+
+ static class PlayerUnderflowException extends IllegalStateException {
+ }
+
+ static class PlayerNameAlreadyUsedException extends RuntimeException {
+ PlayerNameAlreadyUsedException(String name) {
+ super(name);
+ }
+ }
+
+ static class UnknownPlayerException extends IllegalArgumentException {
+ UnknownPlayerException(String username) {
+ super(username);
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/Player.java b/backend/src/main/java/org/luxons/sevenwonders/game/Player.java
new file mode 100644
index 00000000..f1095049
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/Player.java
@@ -0,0 +1,59 @@
+package org.luxons.sevenwonders.game;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public class Player {
+
+ private final String username;
+
+ private String displayName;
+
+ private int index;
+
+ private transient Lobby lobby;
+
+ private transient Game game;
+
+ public Player(String username, String displayName) {
+ this.username = username;
+ this.displayName = displayName;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public void setIndex(int index) {
+ this.index = index;
+ }
+
+ @JsonIgnore
+ public Lobby getLobby() {
+ return lobby;
+ }
+
+ public void setLobby(Lobby lobby) {
+ this.lobby = lobby;
+ }
+
+ @JsonIgnore
+ public Game getGame() {
+ return game;
+ }
+
+ public void setGame(Game game) {
+ this.game = game;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/Settings.java b/backend/src/main/java/org/luxons/sevenwonders/game/Settings.java
new file mode 100644
index 00000000..63ef3522
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/Settings.java
@@ -0,0 +1,84 @@
+package org.luxons.sevenwonders.game;
+
+import java.util.Map;
+import java.util.Random;
+
+import org.luxons.sevenwonders.game.api.CustomizableSettings;
+import org.luxons.sevenwonders.game.data.definitions.WonderSide;
+import org.luxons.sevenwonders.game.data.definitions.WonderSidePickMethod;
+
+public class Settings {
+
+ private final Random random;
+
+ private final int nbPlayers;
+
+ private final int initialGold;
+
+ private final int discardedCardGold;
+
+ private final int defaultTradingCost;
+
+ private final int pointsPer3Gold;
+
+ private final WonderSidePickMethod wonderSidePickMethod;
+
+ private WonderSide lastPickedSide = null;
+
+ private final int lostPointsPerDefeat;
+
+ private final Map<Integer, Integer> wonPointsPerVictoryPerAge;
+
+ public Settings(int nbPlayers) {
+ this(nbPlayers, new CustomizableSettings());
+ }
+
+ public Settings(int nbPlayers, CustomizableSettings customSettings) {
+ long seed = customSettings.getRandomSeedForTests();
+ this.random = seed > 0 ? new Random(seed) : new Random();
+ this.nbPlayers = nbPlayers;
+ this.initialGold = customSettings.getInitialGold();
+ this.discardedCardGold = customSettings.getDiscardedCardGold();
+ this.defaultTradingCost = customSettings.getDefaultTradingCost();
+ this.pointsPer3Gold = customSettings.getPointsPer3Gold();
+ this.wonderSidePickMethod = customSettings.getWonderSidePickMethod();
+ this.lostPointsPerDefeat = customSettings.getLostPointsPerDefeat();
+ this.wonPointsPerVictoryPerAge = customSettings.getWonPointsPerVictoryPerAge();
+ }
+
+ public Random getRandom() {
+ return random;
+ }
+
+ public int getNbPlayers() {
+ return nbPlayers;
+ }
+
+ public int getInitialGold() {
+ return initialGold;
+ }
+
+ public int getDiscardedCardGold() {
+ return discardedCardGold;
+ }
+
+ public int getDefaultTradingCost() {
+ return defaultTradingCost;
+ }
+
+ public int getPointsPer3Gold() {
+ return pointsPer3Gold;
+ }
+
+ public WonderSide pickWonderSide() {
+ return lastPickedSide = wonderSidePickMethod.pickSide(getRandom(), lastPickedSide);
+ }
+
+ public int getLostPointsPerDefeat() {
+ return lostPointsPerDefeat;
+ }
+
+ public Map<Integer, Integer> getWonPointsPerVictoryPerAge() {
+ return wonPointsPerVictoryPerAge;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/State.java b/backend/src/main/java/org/luxons/sevenwonders/game/State.java
new file mode 100644
index 00000000..0bd71d3a
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/State.java
@@ -0,0 +1,6 @@
+package org.luxons.sevenwonders.game;
+
+public enum State {
+ LOBBY,
+ PLAYING
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/api/Action.java b/backend/src/main/java/org/luxons/sevenwonders/game/api/Action.java
new file mode 100644
index 00000000..88e392f9
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/api/Action.java
@@ -0,0 +1,20 @@
+package org.luxons.sevenwonders.game.api;
+
+public enum Action {
+ PLAY("Pick the card you want to play or discard."),
+ PLAY_2("Pick the first card you want to play or discard. Note that you have the ability to play these 2 last cards."
+ + " You will choose how to play the last one during your next turn."),
+ PLAY_LAST("You have the special ability to play your last card. Choose how you want to play it."),
+ PICK_NEIGHBOR_GUILD("Choose a Guild card (purple) that you want to copy from one of your neighbours."),
+ WAIT("Please wait for other players to perform extra actions.");
+
+ private final String message;
+
+ Action(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/api/CustomizableSettings.java b/backend/src/main/java/org/luxons/sevenwonders/game/api/CustomizableSettings.java
new file mode 100644
index 00000000..c270a2af
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/api/CustomizableSettings.java
@@ -0,0 +1,95 @@
+package org.luxons.sevenwonders.game.api;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.luxons.sevenwonders.game.data.definitions.WonderSidePickMethod;
+
+public class CustomizableSettings {
+
+ private long randomSeedForTests = -1;
+
+ private WonderSidePickMethod wonderSidePickMethod = WonderSidePickMethod.EACH_RANDOM;
+
+ private int initialGold = 3;
+
+ private int discardedCardGold = 3;
+
+ private int defaultTradingCost = 2;
+
+ private int pointsPer3Gold = 1;
+
+ private int lostPointsPerDefeat = 1;
+
+ private Map<Integer, Integer> wonPointsPerVictoryPerAge = new HashMap<>();
+
+ public CustomizableSettings() {
+ wonPointsPerVictoryPerAge.put(1, 1);
+ wonPointsPerVictoryPerAge.put(2, 3);
+ wonPointsPerVictoryPerAge.put(3, 5);
+ }
+
+ public long getRandomSeedForTests() {
+ return randomSeedForTests;
+ }
+
+ public void setRandomSeedForTests(long randomSeedForTests) {
+ this.randomSeedForTests = randomSeedForTests;
+ }
+
+ public int getInitialGold() {
+ return initialGold;
+ }
+
+ public void setInitialGold(int initialGold) {
+ this.initialGold = initialGold;
+ }
+
+ public int getDiscardedCardGold() {
+ return discardedCardGold;
+ }
+
+ public void setDiscardedCardGold(int discardedCardGold) {
+ this.discardedCardGold = discardedCardGold;
+ }
+
+ public int getDefaultTradingCost() {
+ return defaultTradingCost;
+ }
+
+ public void setDefaultTradingCost(int defaultTradingCost) {
+ this.defaultTradingCost = defaultTradingCost;
+ }
+
+ public int getPointsPer3Gold() {
+ return pointsPer3Gold;
+ }
+
+ public void setPointsPer3Gold(int pointsPer3Gold) {
+ this.pointsPer3Gold = pointsPer3Gold;
+ }
+
+ public WonderSidePickMethod getWonderSidePickMethod() {
+ return wonderSidePickMethod;
+ }
+
+ public void setWonderSidePickMethod(WonderSidePickMethod wonderSidePickMethod) {
+ this.wonderSidePickMethod = wonderSidePickMethod;
+ }
+
+ public int getLostPointsPerDefeat() {
+ return lostPointsPerDefeat;
+ }
+
+ public void setLostPointsPerDefeat(int lostPointsPerDefeat) {
+ this.lostPointsPerDefeat = lostPointsPerDefeat;
+ }
+
+ public Map<Integer, Integer> getWonPointsPerVictoryPerAge() {
+ return wonPointsPerVictoryPerAge;
+ }
+
+ public void setWonPointsPerVictoryPerAge(Map<Integer, Integer> wonPointsPerVictoryPerAge) {
+ this.wonPointsPerVictoryPerAge = wonPointsPerVictoryPerAge;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/api/HandCard.java b/backend/src/main/java/org/luxons/sevenwonders/game/api/HandCard.java
new file mode 100644
index 00000000..54045607
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/api/HandCard.java
@@ -0,0 +1,44 @@
+package org.luxons.sevenwonders.game.api;
+
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Card;
+
+/**
+ * A card with contextual information relative to the hand it is sitting in. The extra information is especially
+ * useful because it frees the client from a painful business logic implementation.
+ */
+public class HandCard {
+
+ private final Card card;
+
+ private final boolean chainable;
+
+ private final boolean free;
+
+ private final boolean playable;
+
+ public HandCard(Card card, Table table, int playerIndex) {
+ Board board = table.getBoard(playerIndex);
+ this.card = card;
+ this.chainable = card.isChainableOn(board);
+ this.free = card.isAffordedBy(board) && card.getRequirements().getGold() == 0;
+ this.playable = card.isPlayable(table, playerIndex);
+ }
+
+ public Card getCard() {
+ return card;
+ }
+
+ public boolean isChainable() {
+ return chainable;
+ }
+
+ public boolean isFree() {
+ return free;
+ }
+
+ public boolean isPlayable() {
+ return playable;
+ }
+
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/api/PlayerMove.java b/backend/src/main/java/org/luxons/sevenwonders/game/api/PlayerMove.java
new file mode 100644
index 00000000..6d2889e0
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/api/PlayerMove.java
@@ -0,0 +1,45 @@
+package org.luxons.sevenwonders.game.api;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.moves.MoveType;
+import org.luxons.sevenwonders.game.resources.BoughtResources;
+
+public class PlayerMove {
+
+ private String cardName;
+
+ private MoveType type;
+
+ private List<BoughtResources> boughtResources = new ArrayList<>();
+
+ public String getCardName() {
+ return cardName;
+ }
+
+ public void setCardName(String cardName) {
+ this.cardName = cardName;
+ }
+
+ public MoveType getType() {
+ return type;
+ }
+
+ public void setType(MoveType type) {
+ this.type = type;
+ }
+
+ public List<BoughtResources> getBoughtResources() {
+ return boughtResources;
+ }
+
+ public void setBoughtResources(List<BoughtResources> boughtResources) {
+ this.boughtResources = boughtResources;
+ }
+
+ @Override
+ public String toString() {
+ return type + " '" + cardName + '\'';
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/api/PlayerTurnInfo.java b/backend/src/main/java/org/luxons/sevenwonders/game/api/PlayerTurnInfo.java
new file mode 100644
index 00000000..1ff6f541
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/api/PlayerTurnInfo.java
@@ -0,0 +1,76 @@
+package org.luxons.sevenwonders.game.api;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Player;
+import org.luxons.sevenwonders.game.cards.HandRotationDirection;
+
+public class PlayerTurnInfo {
+
+ private final Player player;
+
+ private final Table table;
+
+ private int currentAge;
+
+ private HandRotationDirection handRotationDirection;
+
+ private Action action;
+
+ private List<HandCard> hand;
+
+ private String message;
+
+ public PlayerTurnInfo(Player player, Table table) {
+ this.player = player;
+ this.table = table;
+ }
+
+ public Player getPlayer() {
+ return player;
+ }
+
+ public Table getTable() {
+ return table;
+ }
+
+ public int getCurrentAge() {
+ return currentAge;
+ }
+
+ public void setCurrentAge(int currentAge) {
+ this.currentAge = currentAge;
+ }
+
+ public HandRotationDirection getHandRotationDirection() {
+ return handRotationDirection;
+ }
+
+ public void setHandRotationDirection(HandRotationDirection handRotationDirection) {
+ this.handRotationDirection = handRotationDirection;
+ }
+
+ public List<HandCard> getHand() {
+ return hand;
+ }
+
+ public void setHand(List<HandCard> hand) {
+ this.hand = hand;
+ }
+
+ public Action getAction() {
+ return action;
+ }
+
+ public void setAction(Action action) {
+ this.action = action;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/api/PreparedCard.java b/backend/src/main/java/org/luxons/sevenwonders/game/api/PreparedCard.java
new file mode 100644
index 00000000..85cac1de
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/api/PreparedCard.java
@@ -0,0 +1,24 @@
+package org.luxons.sevenwonders.game.api;
+
+import org.luxons.sevenwonders.game.Player;
+import org.luxons.sevenwonders.game.cards.CardBack;
+
+public class PreparedCard {
+
+ private final Player player;
+
+ private final CardBack cardBack;
+
+ public PreparedCard(Player player, CardBack cardBack) {
+ this.player = player;
+ this.cardBack = cardBack;
+ }
+
+ public Player getPlayer() {
+ return player;
+ }
+
+ public CardBack getCardBack() {
+ return cardBack;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/api/Table.java b/backend/src/main/java/org/luxons/sevenwonders/game/api/Table.java
new file mode 100644
index 00000000..8b831527
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/api/Table.java
@@ -0,0 +1,84 @@
+package org.luxons.sevenwonders.game.api;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition;
+import org.luxons.sevenwonders.game.cards.HandRotationDirection;
+import org.luxons.sevenwonders.game.moves.Move;
+
+/**
+ * The table contains what is visible by all the players in the game: the boards and their played cards, and the
+ * players' information.
+ */
+public class Table {
+
+ private final int nbPlayers;
+
+ private final List<Board> boards;
+
+ private int currentAge = 0;
+
+ private List<Move> lastPlayedMoves;
+
+ public Table(List<Board> boards) {
+ this.nbPlayers = boards.size();
+ this.boards = boards;
+ }
+
+ public int getNbPlayers() {
+ return nbPlayers;
+ }
+
+ public List<Board> getBoards() {
+ return boards;
+ }
+
+ public Board getBoard(int playerIndex) {
+ return boards.get(playerIndex);
+ }
+
+ public Board getBoard(int playerIndex, RelativeBoardPosition position) {
+ return boards.get(position.getIndexFrom(playerIndex, nbPlayers));
+ }
+
+ public List<Move> getLastPlayedMoves() {
+ return lastPlayedMoves;
+ }
+
+ public void setLastPlayedMoves(List<Move> lastPlayedMoves) {
+ this.lastPlayedMoves = lastPlayedMoves;
+ }
+
+ public int getCurrentAge() {
+ return currentAge;
+ }
+
+ public void increaseCurrentAge() {
+ this.currentAge++;
+ }
+
+ public HandRotationDirection getHandRotationDirection() {
+ return HandRotationDirection.forAge(currentAge);
+ }
+
+ public void resolveMilitaryConflicts() {
+ for (int i = 0; i < nbPlayers; i++) {
+ Board board1 = getBoard(i);
+ Board board2 = getBoard((i + 1) % nbPlayers);
+ resolveConflict(board1, board2, currentAge);
+ }
+ }
+
+ private static void resolveConflict(Board board1, Board board2, int age) {
+ int shields1 = board1.getMilitary().getNbShields();
+ int shields2 = board2.getMilitary().getNbShields();
+ if (shields1 < shields2) {
+ board2.getMilitary().victory(age);
+ board1.getMilitary().defeat();
+ } else if (shields1 > shields2) {
+ board1.getMilitary().victory(age);
+ board2.getMilitary().defeat();
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/boards/Board.java b/backend/src/main/java/org/luxons/sevenwonders/game/boards/Board.java
new file mode 100644
index 00000000..ab557d38
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/boards/Board.java
@@ -0,0 +1,173 @@
+package org.luxons.sevenwonders.game.boards;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.luxons.sevenwonders.game.Player;
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.cards.Card;
+import org.luxons.sevenwonders.game.cards.Color;
+import org.luxons.sevenwonders.game.effects.SpecialAbility;
+import org.luxons.sevenwonders.game.resources.Production;
+import org.luxons.sevenwonders.game.resources.TradingRules;
+import org.luxons.sevenwonders.game.scoring.PlayerScore;
+import org.luxons.sevenwonders.game.scoring.ScoreCategory;
+import org.luxons.sevenwonders.game.wonders.Wonder;
+
+public class Board {
+
+ private final Wonder wonder;
+
+ private final Player player;
+
+ private final List<Card> playedCards = new ArrayList<>();
+
+ private final Production production = new Production();
+
+ private final Science science = new Science();
+
+ private final TradingRules tradingRules;
+
+ private final Military military;
+
+ private final Set<SpecialAbility> specialAbilities = EnumSet.noneOf(SpecialAbility.class);
+
+ private Map<Integer, Boolean> consumedFreeCards = new HashMap<>();
+
+ private Card copiedGuild;
+
+ private int gold;
+
+ private int pointsPer3Gold;
+
+ public Board(Wonder wonder, Player player, Settings settings) {
+ this.wonder = wonder;
+ this.player = player;
+ this.gold = settings.getInitialGold();
+ this.tradingRules = new TradingRules(settings.getDefaultTradingCost());
+ this.military = new Military(settings);
+ this.pointsPer3Gold = settings.getPointsPer3Gold();
+ this.production.addFixedResource(wonder.getInitialResource(), 1);
+ }
+
+ public Wonder getWonder() {
+ return wonder;
+ }
+
+ public Player getPlayer() {
+ return player;
+ }
+
+ public List<Card> getPlayedCards() {
+ return playedCards;
+ }
+
+ public void addCard(Card card) {
+ playedCards.add(card);
+ }
+
+ public int getNbCardsOfColor(List<Color> colorFilter) {
+ return (int) playedCards.stream().filter(c -> colorFilter.contains(c.getColor())).count();
+ }
+
+ public boolean isPlayed(String cardName) {
+ return getPlayedCards().stream().map(Card::getName).filter(name -> name.equals(cardName)).count() > 0;
+ }
+
+ public Production getProduction() {
+ return production;
+ }
+
+ public TradingRules getTradingRules() {
+ return tradingRules;
+ }
+
+ public Science getScience() {
+ return science;
+ }
+
+ public int getGold() {
+ return gold;
+ }
+
+ public void setGold(int amount) {
+ this.gold = amount;
+ }
+
+ public void addGold(int amount) {
+ this.gold += amount;
+ }
+
+ public void removeGold(int amount) {
+ if (gold < amount) {
+ throw new InsufficientFundsException(gold, amount);
+ }
+ this.gold -= amount;
+ }
+
+ public Military getMilitary() {
+ return military;
+ }
+
+ public void addSpecial(SpecialAbility specialAbility) {
+ specialAbilities.add(specialAbility);
+ }
+
+ public boolean hasSpecial(SpecialAbility specialAbility) {
+ return specialAbilities.contains(specialAbility);
+ }
+
+ public boolean canPlayFreeCard(int age) {
+ return hasSpecial(SpecialAbility.ONE_FREE_PER_AGE) && !consumedFreeCards.getOrDefault(age, false);
+ }
+
+ public void consumeFreeCard(int age) {
+ consumedFreeCards.put(age, true);
+ }
+
+ public void setCopiedGuild(Card copiedGuild) {
+ if (copiedGuild.getColor() != Color.PURPLE) {
+ throw new IllegalArgumentException("The given card '" + copiedGuild + "' is not a Guild card");
+ }
+ this.copiedGuild = copiedGuild;
+ }
+
+ public Card getCopiedGuild() {
+ return copiedGuild;
+ }
+
+ public PlayerScore computePoints(Table table) {
+ PlayerScore score = new PlayerScore(player, gold);
+ score.put(ScoreCategory.CIVIL, computePointsForCards(table, Color.BLUE));
+ score.put(ScoreCategory.MILITARY, military.getTotalPoints());
+ score.put(ScoreCategory.SCIENCE, science.computePoints());
+ score.put(ScoreCategory.TRADE, computePointsForCards(table, Color.YELLOW));
+ score.put(ScoreCategory.GUILD, computePointsForCards(table, Color.PURPLE));
+ score.put(ScoreCategory.WONDER, wonder.computePoints(table, player.getIndex()));
+ score.put(ScoreCategory.GOLD, computeGoldPoints());
+ return score;
+ }
+
+ private int computePointsForCards(Table table, Color color) {
+ return playedCards.stream()
+ .filter(c -> c.getColor() == color)
+ .flatMap(c -> c.getEffects().stream())
+ .mapToInt(e -> e.computePoints(table, player.getIndex()))
+ .sum();
+ }
+
+ private int computeGoldPoints() {
+ return gold / 3 * pointsPer3Gold;
+ }
+
+ static class InsufficientFundsException extends RuntimeException {
+ InsufficientFundsException(int current, int required) {
+ super(String.format("Current balance is %d gold, but %d are required", current, required));
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/boards/BoardElementType.java b/backend/src/main/java/org/luxons/sevenwonders/game/boards/BoardElementType.java
new file mode 100644
index 00000000..e50f4ea0
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/boards/BoardElementType.java
@@ -0,0 +1,28 @@
+package org.luxons.sevenwonders.game.boards;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.cards.Color;
+
+public enum BoardElementType {
+ CARD {
+ @Override
+ public int getElementCount(Board board, List<Color> colors) {
+ return board.getNbCardsOfColor(colors);
+ }
+ },
+ BUILT_WONDER_STAGES {
+ @Override
+ public int getElementCount(Board board, List<Color> colors) {
+ return board.getWonder().getNbBuiltStages();
+ }
+ },
+ DEFEAT_TOKEN {
+ @Override
+ public int getElementCount(Board board, List<Color> colors) {
+ return board.getMilitary().getNbDefeatTokens();
+ }
+ };
+
+ public abstract int getElementCount(Board board, List<Color> colors);
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/boards/Military.java b/backend/src/main/java/org/luxons/sevenwonders/game/boards/Military.java
new file mode 100644
index 00000000..fb93fa96
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/boards/Military.java
@@ -0,0 +1,54 @@
+package org.luxons.sevenwonders.game.boards;
+
+import org.luxons.sevenwonders.game.Settings;
+
+public class Military {
+
+ private final Settings settings;
+
+ private int nbShields = 0;
+
+ private int totalPoints = 0;
+
+ private int nbDefeatTokens = 0;
+
+ Military(Settings settings) {
+ this.settings = settings;
+ }
+
+ public int getNbShields() {
+ return nbShields;
+ }
+
+ public void addShields(int nbShields) {
+ this.nbShields += nbShields;
+ }
+
+ public int getTotalPoints() {
+ return totalPoints;
+ }
+
+ public int getNbDefeatTokens() {
+ return nbDefeatTokens;
+ }
+
+ public void victory(int age) {
+ Integer wonPoints = settings.getWonPointsPerVictoryPerAge().get(age);
+ if (wonPoints == null) {
+ throw new UnknownAgeException(age);
+ }
+ totalPoints += wonPoints;
+ }
+
+ public void defeat() {
+ int lostPoints = settings.getLostPointsPerDefeat();
+ totalPoints -= lostPoints;
+ nbDefeatTokens++;
+ }
+
+ static final class UnknownAgeException extends IllegalArgumentException {
+ UnknownAgeException(int unknownAge) {
+ super(String.valueOf(unknownAge));
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/boards/RelativeBoardPosition.java b/backend/src/main/java/org/luxons/sevenwonders/game/boards/RelativeBoardPosition.java
new file mode 100644
index 00000000..16b2f3a9
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/boards/RelativeBoardPosition.java
@@ -0,0 +1,28 @@
+package org.luxons.sevenwonders.game.boards;
+
+public enum RelativeBoardPosition {
+ LEFT {
+ @Override
+ public int getIndexFrom(int playerIndex, int nbPlayers) {
+ return wrapIndex(playerIndex - 1, nbPlayers);
+ }
+ },
+ SELF {
+ @Override
+ public int getIndexFrom(int playerIndex, int nbPlayers) {
+ return playerIndex;
+ }
+ },
+ RIGHT {
+ @Override
+ public int getIndexFrom(int playerIndex, int nbPlayers) {
+ return wrapIndex(playerIndex + 1, nbPlayers);
+ }
+ };
+
+ public abstract int getIndexFrom(int playerIndex, int nbPlayers);
+
+ int wrapIndex(int index, int nbPlayers) {
+ return Math.floorMod(index, nbPlayers);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/boards/Science.java b/backend/src/main/java/org/luxons/sevenwonders/game/boards/Science.java
new file mode 100644
index 00000000..34928bcc
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/boards/Science.java
@@ -0,0 +1,65 @@
+package org.luxons.sevenwonders.game.boards;
+
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.Map;
+
+public class Science {
+
+ private Map<ScienceType, Integer> quantities = new EnumMap<>(ScienceType.class);
+
+ private int jokers;
+
+ public void add(ScienceType type, int quantity) {
+ quantities.merge(type, quantity, (x, y) -> x + y);
+ }
+
+ public void addJoker(int quantity) {
+ jokers += quantity;
+ }
+
+ public int getJokers() {
+ return jokers;
+ }
+
+ public void addAll(Science science) {
+ science.quantities.forEach(this::add);
+ jokers += science.jokers;
+ }
+
+ public int getQuantity(ScienceType type) {
+ return quantities.getOrDefault(type, 0);
+ }
+
+ public int size() {
+ return quantities.values().stream().mapToInt(q -> q).sum() + jokers;
+ }
+
+ public int computePoints() {
+ ScienceType[] types = ScienceType.values();
+ Integer[] values = new Integer[types.length];
+ for (int i = 0; i < types.length; i++) {
+ values[i] = quantities.getOrDefault(types[i], 0);
+ }
+ return computePoints(values, jokers);
+ }
+
+ private static int computePoints(Integer[] values, int jokers) {
+ if (jokers == 0) {
+ return computePointsNoJoker(values);
+ }
+ int maxPoints = 0;
+ for (int i = 0; i < values.length; i++) {
+ values[i]++;
+ maxPoints = Math.max(maxPoints, computePoints(values, jokers - 1));
+ values[i]--;
+ }
+ return maxPoints;
+ }
+
+ private static int computePointsNoJoker(Integer[] values) {
+ int independentSquaresSum = Arrays.stream(values).mapToInt(i -> i * i).sum();
+ int nbGroupsOfAll = Arrays.stream(values).mapToInt(i -> i).min().orElse(0);
+ return independentSquaresSum + nbGroupsOfAll * 7;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/boards/ScienceType.java b/backend/src/main/java/org/luxons/sevenwonders/game/boards/ScienceType.java
new file mode 100644
index 00000000..06408b9e
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/boards/ScienceType.java
@@ -0,0 +1,5 @@
+package org.luxons.sevenwonders.game.boards;
+
+public enum ScienceType {
+ COMPASS, WHEEL, TABLET
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/cards/Card.java b/backend/src/main/java/org/luxons/sevenwonders/game/cards/Card.java
new file mode 100644
index 00000000..de674011
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/cards/Card.java
@@ -0,0 +1,116 @@
+package org.luxons.sevenwonders.game.cards;
+
+import java.util.List;
+import java.util.Objects;
+
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.effects.Effect;
+import org.luxons.sevenwonders.game.resources.BoughtResources;
+
+public class Card {
+
+ private final String name;
+
+ private final Color color;
+
+ private final Requirements requirements;
+
+ private final List<Effect> effects;
+
+ private final String chainParent;
+
+ private final List<String> chainChildren;
+
+ private final String image;
+
+ private CardBack back;
+
+ public Card(String name, Color color, Requirements requirements, List<Effect> effects, String chainParent,
+ List<String> chainChildren, String image) {
+ this.name = name;
+ this.color = color;
+ this.requirements = requirements;
+ this.chainParent = chainParent;
+ this.effects = effects;
+ this.chainChildren = chainChildren;
+ this.image = image;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Color getColor() {
+ return color;
+ }
+
+ public String getChainParent() {
+ return chainParent;
+ }
+
+ public Requirements getRequirements() {
+ return requirements;
+ }
+
+ public List<Effect> getEffects() {
+ return effects;
+ }
+
+ public List<String> getChainChildren() {
+ return chainChildren;
+ }
+
+ public String getImage() {
+ return image;
+ }
+
+ public CardBack getBack() {
+ return back;
+ }
+
+ public void setBack(CardBack back) {
+ this.back = back;
+ }
+
+ public boolean isChainableOn(Board board) {
+ return board.isPlayed(chainParent);
+ }
+
+ public boolean isAffordedBy(Board board) {
+ return requirements.isAffordedBy(board);
+ }
+
+ public boolean isPlayable(Table table, int playerIndex) {
+ Board board = table.getBoard(playerIndex);
+ if (board.isPlayed(name)) {
+ return false; // cannot play twice the same card
+ }
+ return isChainableOn(board) || requirements.couldBeAffordedBy(table, playerIndex);
+ }
+
+ public void applyTo(Table table, int playerIndex, List<BoughtResources> boughtResources) {
+ Board playerBoard = table.getBoard(playerIndex);
+ if (!isChainableOn(playerBoard)) {
+ requirements.pay(table, playerIndex, boughtResources);
+ }
+ effects.forEach(e -> e.apply(table, playerIndex));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Card card = (Card)o;
+ return Objects.equals(name, card.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/cards/CardBack.java b/backend/src/main/java/org/luxons/sevenwonders/game/cards/CardBack.java
new file mode 100644
index 00000000..f925b6c4
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/cards/CardBack.java
@@ -0,0 +1,14 @@
+package org.luxons.sevenwonders.game.cards;
+
+public class CardBack {
+
+ private final String image;
+
+ public CardBack(String image) {
+ this.image = image;
+ }
+
+ public String getImage() {
+ return image;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/cards/Color.java b/backend/src/main/java/org/luxons/sevenwonders/game/cards/Color.java
new file mode 100644
index 00000000..5b4e4473
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/cards/Color.java
@@ -0,0 +1,5 @@
+package org.luxons.sevenwonders.game.cards;
+
+public enum Color {
+ BROWN, GREY, YELLOW, BLUE, GREEN, RED, PURPLE
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/cards/Decks.java b/backend/src/main/java/org/luxons/sevenwonders/game/cards/Decks.java
new file mode 100644
index 00000000..aa2b00bf
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/cards/Decks.java
@@ -0,0 +1,65 @@
+package org.luxons.sevenwonders.game.cards;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Decks {
+
+ private Map<Integer, List<Card>> cardsPerAge = new HashMap<>();
+
+ public Decks(Map<Integer, List<Card>> cardsPerAge) {
+ this.cardsPerAge = cardsPerAge;
+ }
+
+ public Card getCard(String cardName) throws CardNotFoundException {
+ return cardsPerAge.values()
+ .stream()
+ .flatMap(List::stream)
+ .filter(c -> c.getName().equals(cardName))
+ .findAny()
+ .orElseThrow(() -> new CardNotFoundException(cardName));
+ }
+
+ public Hands deal(int age, int nbPlayers) {
+ List<Card> deck = getDeck(age);
+ validateNbCards(deck, nbPlayers);
+ return deal(deck, nbPlayers);
+ }
+
+ private List<Card> getDeck(int age) {
+ List<Card> deck = cardsPerAge.get(age);
+ if (deck == null) {
+ throw new IllegalArgumentException("No deck found for age " + age);
+ }
+ return deck;
+ }
+
+ private void validateNbCards(List<Card> deck, int nbPlayers) {
+ if (nbPlayers == 0) {
+ throw new IllegalArgumentException("Cannot deal cards between 0 players");
+ }
+ if (deck.size() % nbPlayers != 0) {
+ throw new IllegalArgumentException(
+ String.format("Cannot deal %d cards evenly between %d players", deck.size(), nbPlayers));
+ }
+ }
+
+ private Hands deal(List<Card> deck, int nbPlayers) {
+ Map<Integer, List<Card>> hands = new HashMap<>(nbPlayers);
+ for (int i = 0; i < nbPlayers; i++) {
+ hands.put(i, new ArrayList<>());
+ }
+ for (int i = 0; i < deck.size(); i++) {
+ hands.get(i % nbPlayers).add(deck.get(i));
+ }
+ return new Hands(hands, nbPlayers);
+ }
+
+ class CardNotFoundException extends RuntimeException {
+ CardNotFoundException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/cards/HandRotationDirection.java b/backend/src/main/java/org/luxons/sevenwonders/game/cards/HandRotationDirection.java
new file mode 100644
index 00000000..9c4f4b02
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/cards/HandRotationDirection.java
@@ -0,0 +1,20 @@
+package org.luxons.sevenwonders.game.cards;
+
+public enum HandRotationDirection {
+ LEFT(-1), RIGHT(1);
+
+ private final int indexOffset;
+
+ HandRotationDirection(int i) {
+ this.indexOffset = i;
+ }
+
+ public int getIndexOffset() {
+ return indexOffset;
+ }
+
+ public static HandRotationDirection forAge(int age) {
+ // clockwise (pass to the left) at age 1, and alternating
+ return age % 2 == 0 ? HandRotationDirection.RIGHT : HandRotationDirection.LEFT;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/cards/Hands.java b/backend/src/main/java/org/luxons/sevenwonders/game/cards/Hands.java
new file mode 100644
index 00000000..4a8bc143
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/cards/Hands.java
@@ -0,0 +1,64 @@
+package org.luxons.sevenwonders.game.cards;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.luxons.sevenwonders.game.api.HandCard;
+import org.luxons.sevenwonders.game.api.Table;
+
+public class Hands {
+
+ private final int nbPlayers;
+
+ private Map<Integer, List<Card>> hands;
+
+ Hands(Map<Integer, List<Card>> hands, int nbPlayers) {
+ this.hands = hands;
+ this.nbPlayers = nbPlayers;
+ }
+
+ public List<Card> get(int playerIndex) {
+ if (!hands.containsKey(playerIndex)) {
+ throw new PlayerIndexOutOfBoundsException(playerIndex);
+ }
+ return hands.get(playerIndex);
+ }
+
+ public List<HandCard> createHand(Table table, int playerIndex) {
+ return hands.get(playerIndex)
+ .stream()
+ .map(c -> new HandCard(c, table, playerIndex))
+ .collect(Collectors.toList());
+ }
+
+ public Hands rotate(HandRotationDirection direction) {
+ Map<Integer, List<Card>> newHands = new HashMap<>(hands.size());
+ for (int i = 0; i < nbPlayers; i++) {
+ int newIndex = Math.floorMod(i + direction.getIndexOffset(), nbPlayers);
+ newHands.put(newIndex, hands.get(i));
+ }
+ return new Hands(newHands, nbPlayers);
+ }
+
+ public boolean isEmpty() {
+ return hands.values().stream().allMatch(List::isEmpty);
+ }
+
+ public boolean maxOneCardRemains() {
+ return hands.values().stream().mapToInt(List::size).max().orElse(0) <= 1;
+ }
+
+ public List<Card> gatherAndClear() {
+ List<Card> remainingCards = hands.values().stream().flatMap(List::stream).collect(Collectors.toList());
+ hands.clear();
+ return remainingCards;
+ }
+
+ class PlayerIndexOutOfBoundsException extends ArrayIndexOutOfBoundsException {
+ PlayerIndexOutOfBoundsException(int index) {
+ super(index);
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/cards/Requirements.java b/backend/src/main/java/org/luxons/sevenwonders/game/cards/Requirements.java
new file mode 100644
index 00000000..f6d7934c
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/cards/Requirements.java
@@ -0,0 +1,88 @@
+package org.luxons.sevenwonders.game.cards;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition;
+import org.luxons.sevenwonders.game.resources.BoughtResources;
+import org.luxons.sevenwonders.game.resources.Resources;
+
+public class Requirements {
+
+ private int gold;
+
+ private Resources resources = new Resources();
+
+ public int getGold() {
+ return gold;
+ }
+
+ public void setGold(int gold) {
+ this.gold = gold;
+ }
+
+ public Resources getResources() {
+ return resources;
+ }
+
+ public void setResources(Resources resources) {
+ this.resources = resources;
+ }
+
+ boolean isAffordedBy(Board board) {
+ return board.getGold() >= gold && board.getProduction().contains(resources);
+ }
+
+ public boolean couldBeAffordedBy(Table table, int playerIndex) {
+ Board board = table.getBoard(playerIndex);
+ if (board.getGold() < gold) {
+ return false;
+ }
+ if (board.getProduction().contains(resources)) {
+ return true;
+ }
+ Resources leftToPay = resources.minus(board.getProduction().getFixedResources());
+ // TODO take into account resources buyable from neighbours
+ return true;
+ }
+
+ public boolean isAffordedBy(Table table, int playerIndex, List<BoughtResources> boughtResources) {
+ Board board = table.getBoard(playerIndex);
+ if (isAffordedBy(board)) {
+ return true;
+ }
+ int totalPrice = board.getTradingRules().computeCost(boughtResources);
+ if (board.getGold() < totalPrice) {
+ return false;
+ }
+ Resources totalBoughtResources = getTotalResources(boughtResources);
+ Resources remainingResources = this.resources.minus(totalBoughtResources);
+ return board.getProduction().contains(remainingResources);
+ }
+
+ private Resources getTotalResources(List<BoughtResources> boughtResources) {
+ return boughtResources.stream().map(BoughtResources::getResources).reduce(new Resources(), (r1, r2) -> {
+ r1.addAll(r2);
+ return r1;
+ });
+ }
+
+ void pay(Table table, int playerIndex, List<BoughtResources> boughtResources) {
+ table.getBoard(playerIndex).removeGold(gold);
+ payBoughtResources(table, playerIndex, boughtResources);
+ }
+
+ private void payBoughtResources(Table table, int playerIndex, List<BoughtResources> boughtResourcesList) {
+ boughtResourcesList.forEach(res -> payBoughtResources(table, playerIndex, res));
+ }
+
+ private void payBoughtResources(Table table, int playerIndex, BoughtResources boughtResources) {
+ Board board = table.getBoard(playerIndex);
+ int price = board.getTradingRules().computeCost(boughtResources);
+ board.removeGold(price);
+ RelativeBoardPosition providerPosition = boughtResources.getProvider().getBoardPosition();
+ Board providerBoard = table.getBoard(playerIndex, providerPosition);
+ providerBoard.addGold(price);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/GameDefinition.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/GameDefinition.java
new file mode 100644
index 00000000..4c63718b
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/GameDefinition.java
@@ -0,0 +1,68 @@
+package org.luxons.sevenwonders.game.data;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Game;
+import org.luxons.sevenwonders.game.Player;
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.CustomizableSettings;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Decks;
+import org.luxons.sevenwonders.game.data.definitions.DecksDefinition;
+import org.luxons.sevenwonders.game.data.definitions.WonderDefinition;
+import org.luxons.sevenwonders.game.wonders.Wonder;
+
+public class GameDefinition {
+
+ /**
+ * This value is heavily dependent on the JSON data. Any change must be carefully thought through.
+ */
+ private static final int MIN_PLAYERS = 3;
+
+ /**
+ * This value is heavily dependent on the JSON data. Any change must be carefully thought through.
+ */
+ private static final int MAX_PLAYERS = 7;
+
+ private WonderDefinition[] wonders;
+
+ private DecksDefinition decksDefinition;
+
+ GameDefinition(WonderDefinition[] wonders, DecksDefinition decksDefinition) {
+ this.wonders = wonders;
+ this.decksDefinition = decksDefinition;
+ }
+
+ public int getMinPlayers() {
+ return MIN_PLAYERS;
+ }
+
+ public int getMaxPlayers() {
+ return MAX_PLAYERS;
+ }
+
+ public Game initGame(long id, CustomizableSettings customSettings, List<Player> orderedPlayers) {
+ Settings settings = new Settings(orderedPlayers.size(), customSettings);
+ List<Board> boards = assignBoards(settings, orderedPlayers);
+ Decks decks = decksDefinition.create(settings);
+ return new Game(id, settings, orderedPlayers, boards, decks);
+ }
+
+ private List<Board> assignBoards(Settings settings, List<Player> orderedPlayers) {
+ List<WonderDefinition> randomizedWonders = Arrays.asList(wonders);
+ Collections.shuffle(randomizedWonders, settings.getRandom());
+
+ List<Board> boards = new ArrayList<>(orderedPlayers.size());
+ for (int i = 0; i < orderedPlayers.size(); i++) {
+ Player player = orderedPlayers.get(i);
+ WonderDefinition def = randomizedWonders.get(i);
+ Wonder w = def.create(settings);
+ Board b = new Board(w, player, settings);
+ boards.add(b);
+ }
+ return boards;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/GameDefinitionLoader.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/GameDefinitionLoader.java
new file mode 100644
index 00000000..30457d86
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/GameDefinitionLoader.java
@@ -0,0 +1,84 @@
+package org.luxons.sevenwonders.game.data;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.data.definitions.DecksDefinition;
+import org.luxons.sevenwonders.game.data.definitions.WonderDefinition;
+import org.luxons.sevenwonders.game.data.serializers.NumericEffectSerializer;
+import org.luxons.sevenwonders.game.data.serializers.ProductionIncreaseSerializer;
+import org.luxons.sevenwonders.game.data.serializers.ResourceTypeSerializer;
+import org.luxons.sevenwonders.game.data.serializers.ResourceTypesSerializer;
+import org.luxons.sevenwonders.game.data.serializers.ResourcesSerializer;
+import org.luxons.sevenwonders.game.data.serializers.ScienceProgressSerializer;
+import org.luxons.sevenwonders.game.effects.GoldIncrease;
+import org.luxons.sevenwonders.game.effects.MilitaryReinforcements;
+import org.luxons.sevenwonders.game.effects.ProductionIncrease;
+import org.luxons.sevenwonders.game.effects.RawPointsIncrease;
+import org.luxons.sevenwonders.game.effects.ScienceProgress;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.resources.Resources;
+import org.springframework.stereotype.Component;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+
+@Component
+public class GameDefinitionLoader {
+
+ private static final String BASE_PACKAGE = GameDefinitionLoader.class.getPackage().getName();
+
+ private static final String BASE_PACKAGE_PATH = '/' + BASE_PACKAGE.replace('.', '/');
+
+ private static final String CARDS_FILE = "cards.json";
+
+ private static final String WONDERS_FILE = "wonders.json";
+
+ private final GameDefinition gameDefinition;
+
+ public GameDefinitionLoader() {
+ gameDefinition = load();
+ }
+
+ public GameDefinition getGameDefinition() {
+ return gameDefinition;
+ }
+
+ private static GameDefinition load() {
+ return new GameDefinition(loadWonders(), loadDecks());
+ }
+
+ private static WonderDefinition[] loadWonders() {
+ return readJsonFile(WONDERS_FILE, WonderDefinition[].class);
+ }
+
+ private static DecksDefinition loadDecks() {
+ return readJsonFile(CARDS_FILE, DecksDefinition.class);
+ }
+
+ private static <T> T readJsonFile(String filename, Class<T> clazz) {
+ InputStream in = GameDefinitionLoader.class.getResourceAsStream(BASE_PACKAGE_PATH + '/' + filename);
+ Reader reader = new BufferedReader(new InputStreamReader(in));
+ Gson gson = createGson();
+ return gson.fromJson(reader, clazz);
+ }
+
+ private static Gson createGson() {
+ Type resourceTypeList = new TypeToken<List<ResourceType>>() {}.getType();
+ return new GsonBuilder().disableHtmlEscaping()
+ .registerTypeAdapter(Resources.class, new ResourcesSerializer())
+ .registerTypeAdapter(ResourceType.class, new ResourceTypeSerializer())
+ .registerTypeAdapter(resourceTypeList, new ResourceTypesSerializer())
+ .registerTypeAdapter(ProductionIncrease.class, new ProductionIncreaseSerializer())
+ .registerTypeAdapter(MilitaryReinforcements.class, new NumericEffectSerializer())
+ .registerTypeAdapter(RawPointsIncrease.class, new NumericEffectSerializer())
+ .registerTypeAdapter(GoldIncrease.class, new NumericEffectSerializer())
+ .registerTypeAdapter(ScienceProgress.class, new ScienceProgressSerializer())
+ .create();
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/CardDefinition.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/CardDefinition.java
new file mode 100644
index 00000000..621bed2c
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/CardDefinition.java
@@ -0,0 +1,38 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import java.util.List;
+import java.util.Map;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.cards.Card;
+import org.luxons.sevenwonders.game.cards.Color;
+import org.luxons.sevenwonders.game.cards.Requirements;
+
+@SuppressWarnings("unused") // the fields are injected by Gson
+public class CardDefinition implements Definition<Card> {
+
+ private String name;
+
+ private Color color;
+
+ private Requirements requirements;
+
+ private EffectsDefinition effect;
+
+ private String chainParent;
+
+ private List<String> chainChildren;
+
+ private Map<Integer, Integer> countPerNbPlayer;
+
+ private String image;
+
+ @Override
+ public Card create(Settings settings) {
+ return new Card(name, color, requirements, effect.create(settings), chainParent, chainChildren, image);
+ }
+
+ Map<Integer, Integer> getCountPerNbPlayer() {
+ return countPerNbPlayer;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/DecksDefinition.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/DecksDefinition.java
new file mode 100644
index 00000000..6f97e55f
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/DecksDefinition.java
@@ -0,0 +1,76 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.cards.Card;
+import org.luxons.sevenwonders.game.cards.CardBack;
+import org.luxons.sevenwonders.game.cards.Decks;
+
+@SuppressWarnings("unused,MismatchedQueryAndUpdateOfCollection") // the fields are injected by Gson
+public class DecksDefinition implements Definition<Decks> {
+
+ private List<CardDefinition> age1;
+
+ private List<CardDefinition> age2;
+
+ private List<CardDefinition> age3;
+
+ private String age1Back;
+
+ private String age2Back;
+
+ private String age3Back;
+
+ private List<CardDefinition> guildCards;
+
+ @Override
+ public Decks create(Settings settings) {
+ Map<Integer, List<Card>> cardsPerAge = new HashMap<>();
+ cardsPerAge.put(1, prepareStandardDeck(age1, settings, age1Back));
+ cardsPerAge.put(2, prepareStandardDeck(age2, settings, age2Back));
+ cardsPerAge.put(3, prepareAge3Deck(settings));
+ return new Decks(cardsPerAge);
+ }
+
+ private static List<Card> prepareStandardDeck(List<CardDefinition> defs, Settings settings, String backImage) {
+ CardBack back = new CardBack(backImage);
+ List<Card> cards = createDeck(defs, settings, back);
+ Collections.shuffle(cards, settings.getRandom());
+ return cards;
+ }
+
+ private List<Card> prepareAge3Deck(Settings settings) {
+ CardBack back = new CardBack(age3Back);
+ List<Card> age3deck = createDeck(age3, settings, back);
+ age3deck.addAll(createGuildCards(settings, back));
+ Collections.shuffle(age3deck, settings.getRandom());
+ return age3deck;
+ }
+
+ private static List<Card> createDeck(List<CardDefinition> defs, Settings settings, CardBack back) {
+ List<Card> cards = new ArrayList<>();
+ for (CardDefinition def : defs) {
+ for (int i = 0; i < def.getCountPerNbPlayer().get(settings.getNbPlayers()); i++) {
+ Card card = def.create(settings);
+ card.setBack(back);
+ cards.add(card);
+ }
+ }
+ return cards;
+ }
+
+ private List<Card> createGuildCards(Settings settings, CardBack back) {
+ List<Card> guild = guildCards.stream()
+ .map((def) -> def.create(settings))
+ .peek(c -> c.setBack(back))
+ .collect(Collectors.toList());
+ Collections.shuffle(guild, settings.getRandom());
+ return guild.subList(0, settings.getNbPlayers() + 2);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/Definition.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/Definition.java
new file mode 100644
index 00000000..6c6b4b19
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/Definition.java
@@ -0,0 +1,24 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import org.luxons.sevenwonders.game.Settings;
+
+/**
+ * Represents a deserialized JSON definition of some data about the game. It is settings-agnostic. An instance of
+ * in-game data can be generated from this, given specific game settings.
+ *
+ * @param <T>
+ * the type of in-game object that can be generated from this definition
+ */
+public interface Definition<T> {
+
+ /**
+ * Creates a T object from the given settings. This method mustn't mutate this Definition as it may be called
+ * multiple times with different settings.
+ *
+ * @param settings
+ * the game settings to use to generate a game-specific object from this definition
+ *
+ * @return the new game-specific object created from this definition
+ */
+ T create(Settings settings);
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/EffectsDefinition.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/EffectsDefinition.java
new file mode 100644
index 00000000..e35463d4
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/EffectsDefinition.java
@@ -0,0 +1,66 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.effects.BonusPerBoardElement;
+import org.luxons.sevenwonders.game.effects.Discount;
+import org.luxons.sevenwonders.game.effects.Effect;
+import org.luxons.sevenwonders.game.effects.GoldIncrease;
+import org.luxons.sevenwonders.game.effects.MilitaryReinforcements;
+import org.luxons.sevenwonders.game.effects.ProductionIncrease;
+import org.luxons.sevenwonders.game.effects.RawPointsIncrease;
+import org.luxons.sevenwonders.game.effects.ScienceProgress;
+import org.luxons.sevenwonders.game.effects.SpecialAbility;
+import org.luxons.sevenwonders.game.effects.SpecialAbilityActivation;
+
+@SuppressWarnings("unused") // the fields are injected by Gson
+public class EffectsDefinition implements Definition<List<Effect>> {
+
+ private GoldIncrease gold;
+
+ private MilitaryReinforcements military;
+
+ private ScienceProgress science;
+
+ private Discount discount;
+
+ private BonusPerBoardElement perBoardElement;
+
+ private ProductionIncrease production;
+
+ private RawPointsIncrease points;
+
+ private SpecialAbility action;
+
+ @Override
+ public List<Effect> create(Settings settings) {
+ List<Effect> effects = new ArrayList<>();
+ if (gold != null) {
+ effects.add(gold);
+ }
+ if (military != null) {
+ effects.add(military);
+ }
+ if (science != null) {
+ effects.add(science);
+ }
+ if (discount != null) {
+ effects.add(discount);
+ }
+ if (perBoardElement != null) {
+ effects.add(perBoardElement);
+ }
+ if (production != null) {
+ effects.add(production);
+ }
+ if (points != null) {
+ effects.add(points);
+ }
+ if (action != null) {
+ effects.add(new SpecialAbilityActivation(action));
+ }
+ return effects;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderDefinition.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderDefinition.java
new file mode 100644
index 00000000..a972a517
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderDefinition.java
@@ -0,0 +1,27 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import java.util.Map;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.wonders.Wonder;
+
+@SuppressWarnings("unused,MismatchedQueryAndUpdateOfCollection") // the fields are injected by Gson
+public class WonderDefinition implements Definition<Wonder> {
+
+ private String name;
+
+ private Map<WonderSide, WonderSideDefinition> sides;
+
+ @Override
+ public Wonder create(Settings settings) {
+ Wonder wonder = new Wonder();
+ wonder.setName(name);
+
+ WonderSideDefinition wonderSideDef = sides.get(settings.pickWonderSide());
+ wonder.setInitialResource(wonderSideDef.getInitialResource());
+ wonder.setStages(wonderSideDef.createStages(settings));
+ wonder.setImage(wonderSideDef.getImage());
+ return wonder;
+ }
+
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSide.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSide.java
new file mode 100644
index 00000000..08c85f57
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSide.java
@@ -0,0 +1,5 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+public enum WonderSide {
+ A, B
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSideDefinition.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSideDefinition.java
new file mode 100644
index 00000000..9b2bc2d5
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSideDefinition.java
@@ -0,0 +1,30 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.wonders.WonderStage;
+
+@SuppressWarnings("unused,MismatchedQueryAndUpdateOfCollection") // the fields are injected by Gson
+class WonderSideDefinition {
+
+ private ResourceType initialResource;
+
+ private List<WonderStageDefinition> stages;
+
+ private String image;
+
+ ResourceType getInitialResource() {
+ return initialResource;
+ }
+
+ List<WonderStage> createStages(Settings settings) {
+ return stages.stream().map(def -> def.create(settings)).collect(Collectors.toList());
+ }
+
+ String getImage() {
+ return image;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethod.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethod.java
new file mode 100644
index 00000000..08aaad14
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethod.java
@@ -0,0 +1,36 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import java.util.Random;
+
+public enum WonderSidePickMethod {
+ ALL_A {
+ @Override
+ public WonderSide pickSide(Random random, WonderSide lastPickedSide) {
+ return WonderSide.A;
+ }
+ },
+ ALL_B {
+ @Override
+ public WonderSide pickSide(Random random, WonderSide lastPickedSide) {
+ return WonderSide.B;
+ }
+ },
+ EACH_RANDOM {
+ @Override
+ public WonderSide pickSide(Random random, WonderSide lastPickedSide) {
+ return random.nextBoolean() ? WonderSide.A : WonderSide.B;
+ }
+ },
+ SAME_RANDOM_FOR_ALL {
+ @Override
+ public WonderSide pickSide(Random random, WonderSide lastPickedSide) {
+ if (lastPickedSide == null) {
+ return random.nextBoolean() ? WonderSide.A : WonderSide.B;
+ } else {
+ return lastPickedSide;
+ }
+ }
+ };
+
+ public abstract WonderSide pickSide(Random random, WonderSide lastPickedSide);
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderStageDefinition.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderStageDefinition.java
new file mode 100644
index 00000000..887b414a
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/definitions/WonderStageDefinition.java
@@ -0,0 +1,21 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.cards.Requirements;
+import org.luxons.sevenwonders.game.wonders.WonderStage;
+
+@SuppressWarnings("unused") // the fields are injected by Gson
+public class WonderStageDefinition implements Definition<WonderStage> {
+
+ private Requirements requirements;
+
+ private EffectsDefinition effects;
+
+ @Override
+ public WonderStage create(Settings settings) {
+ WonderStage stage = new WonderStage();
+ stage.setRequirements(requirements);
+ stage.setEffects(effects.create(settings));
+ return stage;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializer.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializer.java
new file mode 100644
index 00000000..c1a51f24
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializer.java
@@ -0,0 +1,48 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+
+import org.luxons.sevenwonders.game.effects.Effect;
+import org.luxons.sevenwonders.game.effects.GoldIncrease;
+import org.luxons.sevenwonders.game.effects.MilitaryReinforcements;
+import org.luxons.sevenwonders.game.effects.RawPointsIncrease;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+public class NumericEffectSerializer implements JsonSerializer<Effect>, JsonDeserializer<Effect> {
+
+ @Override
+ public JsonElement serialize(Effect effect, Type typeOfSrc, JsonSerializationContext context) {
+ int value;
+ if (MilitaryReinforcements.class.equals(typeOfSrc)) {
+ value = ((MilitaryReinforcements)effect).getCount();
+ } else if (GoldIncrease.class.equals(typeOfSrc)) {
+ value = ((GoldIncrease)effect).getAmount();
+ } else if (RawPointsIncrease.class.equals(typeOfSrc)) {
+ value = ((RawPointsIncrease)effect).getPoints();
+ } else {
+ throw new IllegalArgumentException("Unknown numeric effect " + typeOfSrc.getTypeName());
+ }
+ return new JsonPrimitive(value);
+ }
+
+ @Override
+ public Effect deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws
+ JsonParseException {
+ int value = json.getAsInt();
+ if (MilitaryReinforcements.class.equals(typeOfT)) {
+ return new MilitaryReinforcements(value);
+ } else if (GoldIncrease.class.equals(typeOfT)) {
+ return new GoldIncrease(value);
+ } else if (RawPointsIncrease.class.equals(typeOfT)) {
+ return new RawPointsIncrease(value);
+ }
+ throw new IllegalArgumentException("Unknown numeric effet " + typeOfT.getTypeName());
+ }
+} \ No newline at end of file
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializer.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializer.java
new file mode 100644
index 00000000..6c70a44d
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializer.java
@@ -0,0 +1,84 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.luxons.sevenwonders.game.effects.ProductionIncrease;
+import org.luxons.sevenwonders.game.resources.Production;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.resources.Resources;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+public class ProductionIncreaseSerializer implements JsonSerializer<ProductionIncrease>,
+ JsonDeserializer<ProductionIncrease> {
+
+ @Override
+ public JsonElement serialize(ProductionIncrease productionIncrease, Type typeOfSrc,
+ JsonSerializationContext context) {
+ Production production = productionIncrease.getProduction();
+ Resources fixedResources = production.getFixedResources();
+ List<Set<ResourceType>> choices = production.getAlternativeResources();
+ if (fixedResources.isEmpty()) {
+ return serializeAsChoice(choices, context);
+ } else if (choices.isEmpty()) {
+ return serializeAsResources(fixedResources, context);
+ } else {
+ throw new IllegalArgumentException("Cannot serialize a production with mixed fixed resources and choices");
+ }
+ }
+
+ private JsonElement serializeAsResources(Resources fixedResources, JsonSerializationContext context) {
+ return context.serialize(fixedResources);
+ }
+
+ private JsonElement serializeAsChoice(List<Set<ResourceType>> choices, JsonSerializationContext context) {
+ if (choices.isEmpty()) {
+ return JsonNull.INSTANCE;
+ }
+ if (choices.size() > 1) {
+ throw new IllegalArgumentException("Cannot serialize a production with more than one choice");
+ }
+ String str = choices.get(0).stream()
+ .map(ResourceType::getSymbol)
+ .map(Object::toString)
+ .collect(Collectors.joining("/"));
+ return context.serialize(str);
+ }
+
+ @Override
+ public ProductionIncrease deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws
+ JsonParseException {
+ String s = json.getAsString();
+ ProductionIncrease productionIncrease = new ProductionIncrease();
+ Production production = new Production();
+ if (s.contains("/")) {
+ production.addChoice(createChoice(s));
+ } else {
+ Resources fixedResources = context.deserialize(json, Resources.class);
+ production.addAll(fixedResources);
+ }
+ productionIncrease.setProduction(production);
+ return productionIncrease;
+ }
+
+ private ResourceType[] createChoice(String choiceStr) {
+ String[] symbols = choiceStr.split("/");
+ ResourceType[] choice = new ResourceType[symbols.length];
+ for (int i = 0; i < symbols.length; i++) {
+ if (symbols[i].length() != 1) {
+ throw new IllegalArgumentException("Choice elements must be resource types, got " + symbols[i]);
+ }
+ choice[i] = ResourceType.fromSymbol(symbols[i].charAt(0));
+ }
+ return choice;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializer.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializer.java
new file mode 100644
index 00000000..145063eb
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializer.java
@@ -0,0 +1,30 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+
+public class ResourceTypeSerializer implements JsonSerializer<ResourceType>, JsonDeserializer<ResourceType> {
+
+ @Override
+ public JsonElement serialize(ResourceType type, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive(type.getSymbol());
+ }
+
+ @Override
+ public ResourceType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws
+ JsonParseException {
+ String str = json.getAsString();
+ if (str.isEmpty()) {
+ throw new IllegalArgumentException("Empty string is not a valid resource type");
+ }
+ return ResourceType.fromSymbol(str.charAt(0));
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializer.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializer.java
new file mode 100644
index 00000000..8aca5561
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializer.java
@@ -0,0 +1,38 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+
+public class ResourceTypesSerializer implements JsonSerializer<List<ResourceType>>, JsonDeserializer<List<ResourceType>> {
+
+ @Override
+ public JsonElement serialize(List<ResourceType> resources, Type typeOfSrc, JsonSerializationContext context) {
+ String s = resources.stream()
+ .map(ResourceType::getSymbol)
+ .map(Object::toString)
+ .collect(Collectors.joining());
+ return new JsonPrimitive(s);
+ }
+
+ @Override
+ public List<ResourceType> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws
+ JsonParseException {
+ String s = json.getAsString();
+ List<ResourceType> resources = new ArrayList<>();
+ for (char c : s.toCharArray()) {
+ resources.add(ResourceType.fromSymbol(c));
+ }
+ return resources;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.java
new file mode 100644
index 00000000..efeafd15
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializer.java
@@ -0,0 +1,41 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.resources.Resources;
+
+public class ResourcesSerializer implements JsonSerializer<Resources>, JsonDeserializer<Resources> {
+
+ @Override
+ public JsonElement serialize(Resources resources, Type typeOfSrc, JsonSerializationContext context) {
+ String s = resources.getQuantities()
+ .entrySet()
+ .stream()
+ .flatMap(e -> Stream.generate(() -> e.getKey().getSymbol()).limit(e.getValue()))
+ .map(Object::toString)
+ .collect(Collectors.joining());
+ return s.isEmpty() ? JsonNull.INSTANCE : new JsonPrimitive(s);
+ }
+
+ @Override
+ public Resources deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws
+ JsonParseException {
+ String s = json.getAsString();
+ Resources resources = new Resources();
+ for (char c : s.toCharArray()) {
+ resources.add(ResourceType.fromSymbol(c), 1);
+ }
+ return resources;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializer.java b/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializer.java
new file mode 100644
index 00000000..b6e38540
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializer.java
@@ -0,0 +1,64 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+
+import org.luxons.sevenwonders.game.boards.Science;
+import org.luxons.sevenwonders.game.boards.ScienceType;
+import org.luxons.sevenwonders.game.effects.ScienceProgress;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+public class ScienceProgressSerializer implements JsonSerializer<ScienceProgress>, JsonDeserializer<ScienceProgress> {
+
+ @Override
+ public JsonElement serialize(ScienceProgress scienceProgress, Type typeOfSrc, JsonSerializationContext context) {
+ Science science = scienceProgress.getScience();
+
+ if (science.size() > 1) {
+ throw new UnsupportedOperationException("Cannot serialize science containing more than one element");
+ }
+
+ for (ScienceType type : ScienceType.values()) {
+ int quantity = science.getQuantity(type);
+ if (quantity == 1) {
+ return context.serialize(type);
+ }
+ }
+
+ if (science.getJokers() == 1) {
+ return new JsonPrimitive("any");
+ }
+
+ return JsonNull.INSTANCE;
+ }
+
+ @Override
+ public ScienceProgress deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws
+ JsonParseException {
+ String s = json.getAsString();
+ ScienceProgress scienceProgress = new ScienceProgress();
+ Science science = new Science();
+ if ("any".equals(s)) {
+ science.addJoker(1);
+ } else {
+ science.add(deserializeScienceType(json, context), 1);
+ }
+ scienceProgress.setScience(science);
+ return scienceProgress;
+ }
+
+ private ScienceType deserializeScienceType(JsonElement json, JsonDeserializationContext context) {
+ ScienceType type = context.deserialize(json, ScienceType.class);
+ if (type == null) {
+ throw new IllegalArgumentException("Invalid science type " + json.getAsString());
+ }
+ return type;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/effects/BonusPerBoardElement.java b/backend/src/main/java/org/luxons/sevenwonders/game/effects/BonusPerBoardElement.java
new file mode 100644
index 00000000..e9f9fe5f
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/effects/BonusPerBoardElement.java
@@ -0,0 +1,86 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.boards.BoardElementType;
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition;
+import org.luxons.sevenwonders.game.cards.Color;
+
+public class BonusPerBoardElement implements Effect {
+
+ private List<RelativeBoardPosition> boards;
+
+ private int gold;
+
+ private int points;
+
+ private BoardElementType type;
+
+ // only relevant if type=CARD
+ private List<Color> colors;
+
+ public List<RelativeBoardPosition> getBoards() {
+ return boards;
+ }
+
+ public void setBoards(List<RelativeBoardPosition> boards) {
+ this.boards = boards;
+ }
+
+ public int getGold() {
+ return gold;
+ }
+
+ public void setGold(int gold) {
+ this.gold = gold;
+ }
+
+ public int getPoints() {
+ return points;
+ }
+
+ public void setPoints(int points) {
+ this.points = points;
+ }
+
+ public BoardElementType getType() {
+ return type;
+ }
+
+ public void setType(BoardElementType type) {
+ this.type = type;
+ }
+
+ public List<Color> getColors() {
+ return colors;
+ }
+
+ public void setColors(List<Color> colors) {
+ this.colors = colors;
+ }
+
+ @Override
+ public void apply(Table table, int playerIndex) {
+ int goldGain = gold * computeNbOfMatchingElementsIn(table, playerIndex);
+ Board board = table.getBoard(playerIndex);
+ board.addGold(goldGain);
+ }
+
+ @Override
+ public int computePoints(Table table, int playerIndex) {
+ return points * computeNbOfMatchingElementsIn(table, playerIndex);
+ }
+
+ private int computeNbOfMatchingElementsIn(Table table, int playerIndex) {
+ return boards.stream()
+ .map(pos -> table.getBoard(playerIndex, pos))
+ .mapToInt(this::computeNbOfMatchingElementsIn)
+ .sum();
+ }
+
+ private int computeNbOfMatchingElementsIn(Board board) {
+ return type.getElementCount(board, colors);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/effects/Discount.java b/backend/src/main/java/org/luxons/sevenwonders/game/effects/Discount.java
new file mode 100644
index 00000000..3a44574b
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/effects/Discount.java
@@ -0,0 +1,44 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.resources.Provider;
+import org.luxons.sevenwonders.game.resources.TradingRules;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+
+public class Discount extends InstantOwnBoardEffect {
+
+ private final List<ResourceType> resourceTypes = new ArrayList<>();
+
+ private final List<Provider> providers = new ArrayList<>();
+
+ private int discountedPrice = 1;
+
+ public List<ResourceType> getResourceTypes() {
+ return resourceTypes;
+ }
+
+ public List<Provider> getProviders() {
+ return providers;
+ }
+
+ public int getDiscountedPrice() {
+ return discountedPrice;
+ }
+
+ public void setDiscountedPrice(int discountedPrice) {
+ this.discountedPrice = discountedPrice;
+ }
+
+ @Override
+ public void apply(Board board) {
+ TradingRules rules = board.getTradingRules();
+ for (ResourceType type : resourceTypes) {
+ for (Provider provider : providers) {
+ rules.setCost(type, provider, discountedPrice);
+ }
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/effects/Effect.java b/backend/src/main/java/org/luxons/sevenwonders/game/effects/Effect.java
new file mode 100644
index 00000000..692eaea0
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/effects/Effect.java
@@ -0,0 +1,15 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.luxons.sevenwonders.game.api.Table;
+
+/**
+ * Represents an effect than can be applied to a player's board when playing a card or building his wonder. The effect
+ * may affect (or depend on) the adjacent boards. It can have an instantaneous effect on the board, or be postponed to
+ * the end of game where point calculations take place.
+ */
+public interface Effect {
+
+ void apply(Table table, int playerIndex);
+
+ int computePoints(Table table, int playerIndex);
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/effects/EndGameEffect.java b/backend/src/main/java/org/luxons/sevenwonders/game/effects/EndGameEffect.java
new file mode 100644
index 00000000..1bae16a6
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/effects/EndGameEffect.java
@@ -0,0 +1,11 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.luxons.sevenwonders.game.api.Table;
+
+public abstract class EndGameEffect implements Effect {
+
+ @Override
+ public void apply(Table table, int playerIndex) {
+ // EndGameEffects don't do anything when applied to the board, they simply give more points in the end
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/effects/GoldIncrease.java b/backend/src/main/java/org/luxons/sevenwonders/game/effects/GoldIncrease.java
new file mode 100644
index 00000000..79e7bd1a
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/effects/GoldIncrease.java
@@ -0,0 +1,40 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.Objects;
+
+import org.luxons.sevenwonders.game.boards.Board;
+
+public class GoldIncrease extends InstantOwnBoardEffect {
+
+ private final int amount;
+
+ public int getAmount() {
+ return amount;
+ }
+
+ public GoldIncrease(int amount) {
+ this.amount = amount;
+ }
+
+ @Override
+ public void apply(Board board) {
+ board.addGold(amount);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ GoldIncrease that = (GoldIncrease)o;
+ return amount == that.amount;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(amount);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/effects/InstantOwnBoardEffect.java b/backend/src/main/java/org/luxons/sevenwonders/game/effects/InstantOwnBoardEffect.java
new file mode 100644
index 00000000..8f4340cf
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/effects/InstantOwnBoardEffect.java
@@ -0,0 +1,20 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+
+public abstract class InstantOwnBoardEffect implements Effect {
+
+ @Override
+ public void apply(Table table, int playerIndex) {
+ apply(table.getBoard(playerIndex));
+ }
+
+ protected abstract void apply(Board board);
+
+ @Override
+ public int computePoints(Table table, int playerIndex) {
+ // InstantEffects are only important when applied to the board, they don't give extra points in the end
+ return 0;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/effects/MilitaryReinforcements.java b/backend/src/main/java/org/luxons/sevenwonders/game/effects/MilitaryReinforcements.java
new file mode 100644
index 00000000..b08e2f59
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/effects/MilitaryReinforcements.java
@@ -0,0 +1,40 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.Objects;
+
+import org.luxons.sevenwonders.game.boards.Board;
+
+public class MilitaryReinforcements extends InstantOwnBoardEffect {
+
+ private final int count;
+
+ public int getCount() {
+ return count;
+ }
+
+ public MilitaryReinforcements(int count) {
+ this.count = count;
+ }
+
+ @Override
+ public void apply(Board board) {
+ board.getMilitary().addShields(count);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ MilitaryReinforcements that = (MilitaryReinforcements)o;
+ return count == that.count;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(count);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/effects/ProductionIncrease.java b/backend/src/main/java/org/luxons/sevenwonders/game/effects/ProductionIncrease.java
new file mode 100644
index 00000000..9724dfcd
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/effects/ProductionIncrease.java
@@ -0,0 +1,41 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.Objects;
+
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.resources.Production;
+
+public class ProductionIncrease extends InstantOwnBoardEffect {
+
+ private Production production = new Production();
+
+ public Production getProduction() {
+ return production;
+ }
+
+ public void setProduction(Production production) {
+ this.production = production;
+ }
+
+ @Override
+ public void apply(Board board) {
+ board.getProduction().addAll(production);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ProductionIncrease that = (ProductionIncrease)o;
+ return Objects.equals(production, that.production);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(production);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/effects/RawPointsIncrease.java b/backend/src/main/java/org/luxons/sevenwonders/game/effects/RawPointsIncrease.java
new file mode 100644
index 00000000..0d117cec
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/effects/RawPointsIncrease.java
@@ -0,0 +1,40 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.Objects;
+
+import org.luxons.sevenwonders.game.api.Table;
+
+public class RawPointsIncrease extends EndGameEffect {
+
+ private final int points;
+
+ public int getPoints() {
+ return points;
+ }
+
+ public RawPointsIncrease(int points) {
+ this.points = points;
+ }
+
+ @Override
+ public int computePoints(Table table, int playerIndex) {
+ return points;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ RawPointsIncrease that = (RawPointsIncrease)o;
+ return points == that.points;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(points);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/effects/ScienceProgress.java b/backend/src/main/java/org/luxons/sevenwonders/game/effects/ScienceProgress.java
new file mode 100644
index 00000000..4e6764ee
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/effects/ScienceProgress.java
@@ -0,0 +1,22 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.boards.Science;
+
+public class ScienceProgress extends InstantOwnBoardEffect {
+
+ private Science science;
+
+ public Science getScience() {
+ return science;
+ }
+
+ public void setScience(Science science) {
+ this.science = science;
+ }
+
+ @Override
+ public void apply(Board board) {
+ board.getScience().addAll(science);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbility.java b/backend/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbility.java
new file mode 100644
index 00000000..5de87784
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbility.java
@@ -0,0 +1,46 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public enum SpecialAbility {
+ /**
+ * The player can play the last card of each age instead of discarding it. This card can be played by paying its
+ * cost, discarded to gain 3 coins or used in the construction of his or her Wonder.
+ */
+ PLAY_LAST_CARD,
+
+ /**
+ * Once per age, a player can construct a building from his or her hand for free.
+ */
+ ONE_FREE_PER_AGE,
+
+ /**
+ * The player can look at all cards discarded since the beginning of the game, pick one and build it for free.
+ */
+ PLAY_DISCARDED,
+
+ /**
+ * The player can, at the end of the game, “copy” a Guild of his or her choice (purple card), built by one of his or
+ * her two neighboring cities.
+ */
+ COPY_GUILD {
+ @Override
+ public int computePoints(Table table, int playerIndex) {
+ Card copiedGuild = table.getBoard(playerIndex).getCopiedGuild();
+ if (copiedGuild == null) {
+ throw new IllegalStateException("The copied Guild has not been chosen, cannot compute points");
+ }
+ return copiedGuild.getEffects().stream().mapToInt(e -> e.computePoints(table, playerIndex)).sum();
+ }
+ };
+
+ protected void apply(Board board) {
+ board.addSpecial(this);
+ }
+
+ public int computePoints(Table table, int playerIndex) {
+ return 0;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbilityActivation.java b/backend/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbilityActivation.java
new file mode 100644
index 00000000..a5953c2f
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/effects/SpecialAbilityActivation.java
@@ -0,0 +1,26 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.luxons.sevenwonders.game.api.Table;
+
+public class SpecialAbilityActivation implements Effect {
+
+ private final SpecialAbility specialAbility;
+
+ public SpecialAbilityActivation(SpecialAbility specialAbility) {
+ this.specialAbility = specialAbility;
+ }
+
+ public SpecialAbility getSpecialAbility() {
+ return specialAbility;
+ }
+
+ @Override
+ public void apply(Table table, int playerIndex) {
+ specialAbility.apply(table.getBoard(playerIndex));
+ }
+
+ @Override
+ public int computePoints(Table table, int playerIndex) {
+ return specialAbility.computePoints(table, playerIndex);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/moves/BuildWonderMove.java b/backend/src/main/java/org/luxons/sevenwonders/game/moves/BuildWonderMove.java
new file mode 100644
index 00000000..bddd6ec6
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/moves/BuildWonderMove.java
@@ -0,0 +1,38 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public class BuildWonderMove extends CardFromHandMove {
+
+ BuildWonderMove(int playerIndex, Card card, PlayerMove move) {
+ super(playerIndex, card, move);
+ }
+
+ @Override
+ public boolean isValid(Table table, List<Card> playerHand) {
+ if (!super.isValid(table, playerHand)) {
+ return false;
+ }
+ Board board = table.getBoard(getPlayerIndex());
+ return board.getWonder().isNextStageBuildable(table, getPlayerIndex(), getBoughtResources());
+ }
+
+ @Override
+ public void place(Table table, List<Card> discardedCards, Settings settings) {
+ Board board = table.getBoard(getPlayerIndex());
+ board.getWonder().buildLevel(getCard().getBack());
+ }
+
+ @Override
+ public void activate(Table table, List<Card> discardedCards, Settings settings) {
+ int playerIndex = getPlayerIndex();
+ Board board = table.getBoard(playerIndex);
+ board.getWonder().activateLastBuiltStage(table, playerIndex, getBoughtResources());
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/moves/CardFromHandMove.java b/backend/src/main/java/org/luxons/sevenwonders/game/moves/CardFromHandMove.java
new file mode 100644
index 00000000..7bbee1e5
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/moves/CardFromHandMove.java
@@ -0,0 +1,20 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public abstract class CardFromHandMove extends Move {
+
+ CardFromHandMove(int playerIndex, Card card, PlayerMove move) {
+ super(playerIndex, card, move);
+ }
+
+ @Override
+ public boolean isValid(Table table, List<Card> playerHand) {
+ return playerHand.contains(getCard());
+ }
+
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/moves/CopyGuildMove.java b/backend/src/main/java/org/luxons/sevenwonders/game/moves/CopyGuildMove.java
new file mode 100644
index 00000000..5ebde772
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/moves/CopyGuildMove.java
@@ -0,0 +1,49 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition;
+import org.luxons.sevenwonders.game.cards.Card;
+import org.luxons.sevenwonders.game.cards.Color;
+import org.luxons.sevenwonders.game.effects.SpecialAbility;
+
+public class CopyGuildMove extends Move {
+
+ CopyGuildMove(int playerIndex, Card card, PlayerMove move) {
+ super(playerIndex, card, move);
+ }
+
+ @Override
+ public boolean isValid(Table table, List<Card> playerHand) {
+ Board board = table.getBoard(getPlayerIndex());
+ if (!board.hasSpecial(SpecialAbility.COPY_GUILD)) {
+ return false;
+ }
+ if (getCard().getColor() != Color.PURPLE) {
+ return false;
+ }
+ boolean leftNeighbourHasIt = neighbourHasTheCard(table, RelativeBoardPosition.LEFT);
+ boolean rightNeighbourHasIt = neighbourHasTheCard(table, RelativeBoardPosition.RIGHT);
+ return leftNeighbourHasIt || rightNeighbourHasIt;
+ }
+
+ private boolean neighbourHasTheCard(Table table, RelativeBoardPosition position) {
+ Board neighbourBoard = table.getBoard(getPlayerIndex(), position);
+ return neighbourBoard.getPlayedCards().contains(getCard());
+ }
+
+ @Override
+ public void place(Table table, List<Card> discardedCards, Settings settings) {
+ // nothing special to do here
+ }
+
+ @Override
+ public void activate(Table table, List<Card> discardedCards, Settings settings) {
+ Board board = table.getBoard(getPlayerIndex());
+ board.setCopiedGuild(getCard());
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/moves/DiscardMove.java b/backend/src/main/java/org/luxons/sevenwonders/game/moves/DiscardMove.java
new file mode 100644
index 00000000..076a593c
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/moves/DiscardMove.java
@@ -0,0 +1,27 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public class DiscardMove extends CardFromHandMove {
+
+ DiscardMove(int playerIndex, Card card, PlayerMove move) {
+ super(playerIndex, card, move);
+ }
+
+ @Override
+ public void place(Table table, List<Card> discardedCards, Settings settings) {
+ discardedCards.add(getCard());
+ }
+
+ @Override
+ public void activate(Table table, List<Card> discardedCards, Settings settings) {
+ Board board = table.getBoard(getPlayerIndex());
+ board.addGold(settings.getDiscardedCardGold());
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/moves/Move.java b/backend/src/main/java/org/luxons/sevenwonders/game/moves/Move.java
new file mode 100644
index 00000000..8b6b60a8
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/moves/Move.java
@@ -0,0 +1,50 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.cards.Card;
+import org.luxons.sevenwonders.game.resources.BoughtResources;
+
+public abstract class Move {
+
+ private int playerIndex;
+
+ private Card card;
+
+ private MoveType type;
+
+ private List<BoughtResources> boughtResources = new ArrayList<>();
+
+ Move(int playerIndex, Card card, PlayerMove move) {
+ this.playerIndex = playerIndex;
+ this.card = card;
+ this.type = move.getType();
+ this.boughtResources = move.getBoughtResources();
+ }
+
+ public int getPlayerIndex() {
+ return playerIndex;
+ }
+
+ public Card getCard() {
+ return card;
+ }
+
+ public MoveType getType() {
+ return type;
+ }
+
+ public List<BoughtResources> getBoughtResources() {
+ return boughtResources;
+ }
+
+ public abstract boolean isValid(Table table, List<Card> playerHand);
+
+ public abstract void place(Table table, List<Card> discardedCards, Settings settings);
+
+ public abstract void activate(Table table, List<Card> discardedCards, Settings settings);
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/moves/MoveType.java b/backend/src/main/java/org/luxons/sevenwonders/game/moves/MoveType.java
new file mode 100644
index 00000000..bf64344d
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/moves/MoveType.java
@@ -0,0 +1,39 @@
+package org.luxons.sevenwonders.game.moves;
+
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public enum MoveType {
+ PLAY {
+ @Override
+ public Move resolve(int playerIndex, Card card, PlayerMove move) {
+ return new PlayCardMove(playerIndex, card, move);
+ }
+ },
+ PLAY_FREE {
+ @Override
+ public Move resolve(int playerIndex, Card card, PlayerMove move) {
+ return new PlayFreeCardMove(playerIndex, card, move);
+ }
+ },
+ UPGRADE_WONDER {
+ @Override
+ public Move resolve(int playerIndex, Card card, PlayerMove move) {
+ return new BuildWonderMove(playerIndex, card, move);
+ }
+ },
+ DISCARD {
+ @Override
+ public Move resolve(int playerIndex, Card card, PlayerMove move) {
+ return new DiscardMove(playerIndex, card, move);
+ }
+ },
+ COPY_GUILD {
+ @Override
+ public Move resolve(int playerIndex, Card card, PlayerMove move) {
+ return new CopyGuildMove(playerIndex, card, move);
+ }
+ };
+
+ public abstract Move resolve(int playerIndex, Card card, PlayerMove move);
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/moves/PlayCardMove.java b/backend/src/main/java/org/luxons/sevenwonders/game/moves/PlayCardMove.java
new file mode 100644
index 00000000..affebc4a
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/moves/PlayCardMove.java
@@ -0,0 +1,35 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public class PlayCardMove extends CardFromHandMove {
+
+ PlayCardMove(int playerIndex, Card card, PlayerMove move) {
+ super(playerIndex, card, move);
+ }
+
+ @Override
+ public boolean isValid(Table table, List<Card> playerHand) {
+ if (!super.isValid(table, playerHand)) {
+ return false;
+ }
+ return getCard().getRequirements().isAffordedBy(table, getPlayerIndex(), getBoughtResources());
+ }
+
+ @Override
+ public void place(Table table, List<Card> discardedCards, Settings settings) {
+ Board board = table.getBoard(getPlayerIndex());
+ board.addCard(getCard());
+ }
+
+ @Override
+ public void activate(Table table, List<Card> discardedCards, Settings settings) {
+ getCard().applyTo(table, getPlayerIndex(), getBoughtResources());
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/moves/PlayFreeCardMove.java b/backend/src/main/java/org/luxons/sevenwonders/game/moves/PlayFreeCardMove.java
new file mode 100644
index 00000000..fb28b09c
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/moves/PlayFreeCardMove.java
@@ -0,0 +1,39 @@
+package org.luxons.sevenwonders.game.moves;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.PlayerMove;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.cards.Card;
+
+public class PlayFreeCardMove extends CardFromHandMove {
+
+ PlayFreeCardMove(int playerIndex, Card card, PlayerMove move) {
+ super(playerIndex, card, move);
+ }
+
+ @Override
+ public boolean isValid(Table table, List<Card> playerHand) {
+ if (!super.isValid(table, playerHand)) {
+ return false;
+ }
+ Board board = table.getBoard(getPlayerIndex());
+ return board.canPlayFreeCard(table.getCurrentAge());
+ }
+
+ @Override
+ public void place(Table table, List<Card> discardedCards, Settings settings) {
+ Board board = table.getBoard(getPlayerIndex());
+ board.addCard(getCard());
+ }
+
+ @Override
+ public void activate(Table table, List<Card> discardedCards, Settings settings) {
+ // only apply effects, without paying the cost
+ getCard().getEffects().forEach(e -> e.apply(table, getPlayerIndex()));
+ Board board = table.getBoard(getPlayerIndex());
+ board.consumeFreeCard(table.getCurrentAge());
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/resources/BoughtResources.java b/backend/src/main/java/org/luxons/sevenwonders/game/resources/BoughtResources.java
new file mode 100644
index 00000000..ec261c8c
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/resources/BoughtResources.java
@@ -0,0 +1,24 @@
+package org.luxons.sevenwonders.game.resources;
+
+public class BoughtResources {
+
+ private Provider provider;
+
+ private Resources resources;
+
+ public Provider getProvider() {
+ return provider;
+ }
+
+ public void setProvider(Provider provider) {
+ this.provider = provider;
+ }
+
+ public Resources getResources() {
+ return resources;
+ }
+
+ public void setResources(Resources resources) {
+ this.resources = resources;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/resources/Production.java b/backend/src/main/java/org/luxons/sevenwonders/game/resources/Production.java
new file mode 100644
index 00000000..b7701c27
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/resources/Production.java
@@ -0,0 +1,103 @@
+package org.luxons.sevenwonders.game.resources;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+
+public class Production {
+
+ private final Resources fixedResources = new Resources();
+
+ private final List<Set<ResourceType>> alternativeResources = new ArrayList<>();
+
+ public void addFixedResource(ResourceType type, int quantity) {
+ fixedResources.add(type, quantity);
+ }
+
+ public void addChoice(ResourceType... options) {
+ EnumSet<ResourceType> optionSet = EnumSet.noneOf(ResourceType.class);
+ optionSet.addAll(Arrays.asList(options));
+ alternativeResources.add(optionSet);
+ }
+
+ public void addAll(Resources resources) {
+ fixedResources.addAll(resources);
+ }
+
+ public void addAll(Production production) {
+ fixedResources.addAll(production.getFixedResources());
+ alternativeResources.addAll(production.getAlternativeResources());
+ }
+
+ public Resources getFixedResources() {
+ return fixedResources;
+ }
+
+ public List<Set<ResourceType>> getAlternativeResources() {
+ return alternativeResources;
+ }
+
+ public boolean contains(Resources resources) {
+ if (fixedResources.contains(resources)) {
+ return true;
+ }
+ Resources remaining = resources.minus(fixedResources);
+ return containedInAlternatives(remaining);
+ }
+
+ private boolean containedInAlternatives(Resources resources) {
+ return containedInAlternatives(resources, alternativeResources);
+ }
+
+ private static boolean containedInAlternatives(Resources resources, List<Set<ResourceType>> alternatives) {
+ if (resources.isEmpty()) {
+ return true;
+ }
+ for (Entry<ResourceType, Integer> entry : resources.getQuantities().entrySet()) {
+ ResourceType type = entry.getKey();
+ int count = entry.getValue();
+ if (count <= 0) {
+ continue;
+ }
+ Set<ResourceType> candidate = findFirstAlternativeContaining(alternatives, type);
+ if (candidate == null) {
+ return false; // no alternative produces the resource of this entry
+ }
+ entry.setValue(count - 1);
+ alternatives.remove(candidate);
+ boolean remainingAreContainedToo = containedInAlternatives(resources, alternatives);
+ entry.setValue(count);
+ alternatives.add(candidate);
+ if (remainingAreContainedToo) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static Set<ResourceType> findFirstAlternativeContaining(List<Set<ResourceType>> alternatives, ResourceType type) {
+ return alternatives.stream().filter(a -> a.contains(type)).findAny().orElse(null);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Production that = (Production)o;
+ return Objects.equals(fixedResources, that.fixedResources) && Objects.equals(alternativeResources,
+ that.alternativeResources);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(fixedResources, alternativeResources);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/resources/Provider.java b/backend/src/main/java/org/luxons/sevenwonders/game/resources/Provider.java
new file mode 100644
index 00000000..9c4aa3f9
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/resources/Provider.java
@@ -0,0 +1,18 @@
+package org.luxons.sevenwonders.game.resources;
+
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition;
+
+public enum Provider {
+ LEFT_PLAYER(RelativeBoardPosition.LEFT),
+ RIGHT_PLAYER(RelativeBoardPosition.RIGHT);
+
+ private final RelativeBoardPosition boardPosition;
+
+ Provider(RelativeBoardPosition boardPosition) {
+ this.boardPosition = boardPosition;
+ }
+
+ public RelativeBoardPosition getBoardPosition() {
+ return boardPosition;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/resources/ResourceType.java b/backend/src/main/java/org/luxons/sevenwonders/game/resources/ResourceType.java
new file mode 100644
index 00000000..46d60123
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/resources/ResourceType.java
@@ -0,0 +1,39 @@
+package org.luxons.sevenwonders.game.resources;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public enum ResourceType {
+ WOOD('W'),
+ STONE('S'),
+ ORE('O'),
+ CLAY('C'),
+ GLASS('G'),
+ PAPYRUS('P'),
+ LOOM('L');
+
+ private static final Map<Character, ResourceType> typesPerSymbol = new HashMap<>(7);
+ static {
+ for (ResourceType type : values()) {
+ typesPerSymbol.put(type.symbol, type);
+ }
+ }
+
+ private final Character symbol;
+
+ ResourceType(Character symbol) {
+ this.symbol = symbol;
+ }
+
+ public static ResourceType fromSymbol(Character symbol) {
+ ResourceType type = typesPerSymbol.get(symbol);
+ if (type == null) {
+ throw new IllegalArgumentException(String.format("Unknown resource type symbol '%s'", symbol));
+ }
+ return type;
+ }
+
+ public Character getSymbol() {
+ return symbol;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/resources/Resources.java b/backend/src/main/java/org/luxons/sevenwonders/game/resources/Resources.java
new file mode 100644
index 00000000..5bf6f269
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/resources/Resources.java
@@ -0,0 +1,65 @@
+package org.luxons.sevenwonders.game.resources;
+
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+
+public class Resources {
+
+ private final Map<ResourceType, Integer> quantities = new EnumMap<>(ResourceType.class);
+
+ public void add(ResourceType type, int quantity) {
+ quantities.merge(type, quantity, (x, y) -> x + y);
+ }
+
+ public void addAll(Resources resources) {
+ resources.getQuantities().forEach(this::add);
+ }
+
+ public int getQuantity(ResourceType type) {
+ return quantities.getOrDefault(type, 0);
+ }
+
+ public Map<ResourceType, Integer> getQuantities() {
+ return quantities;
+ }
+
+ public boolean contains(Resources resources) {
+ return resources.quantities.entrySet().stream().allMatch(this::hasAtLeast);
+ }
+
+ private boolean hasAtLeast(Entry<ResourceType, Integer> quantity) {
+ return quantity.getValue() <= getQuantity(quantity.getKey());
+ }
+
+ public Resources minus(Resources resources) {
+ Resources diff = new Resources();
+ quantities.forEach((type, count) -> {
+ int remainder = count - resources.getQuantity(type);
+ diff.quantities.put(type, Math.max(0, remainder));
+ });
+ return diff;
+ }
+
+ public boolean isEmpty() {
+ return quantities.values().stream().reduce(0, Integer::sum) == 0;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Resources resources = (Resources)o;
+ return Objects.equals(quantities, resources.quantities);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(quantities);
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/resources/TradingRules.java b/backend/src/main/java/org/luxons/sevenwonders/game/resources/TradingRules.java
new file mode 100644
index 00000000..19409844
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/resources/TradingRules.java
@@ -0,0 +1,40 @@
+package org.luxons.sevenwonders.game.resources;
+
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+public class TradingRules {
+
+ private final Map<ResourceType, Map<Provider, Integer>> costs = new EnumMap<>(ResourceType.class);
+
+ private final int defaultCost;
+
+ public TradingRules(int defaultCost) {
+ this.defaultCost = defaultCost;
+ }
+
+ private int getCost(ResourceType type, Provider provider) {
+ return costs.computeIfAbsent(type, t -> new EnumMap<>(Provider.class)).getOrDefault(provider, defaultCost);
+ }
+
+ public void setCost(ResourceType type, Provider provider, int cost) {
+ costs.computeIfAbsent(type, t -> new EnumMap<>(Provider.class)).put(provider, cost);
+ }
+
+ public int computeCost(List<BoughtResources> boughtResources) {
+ return boughtResources.stream().mapToInt(this::computeCost).sum();
+ }
+
+ public int computeCost(BoughtResources boughtResources) {
+ Resources resources = boughtResources.getResources();
+ int total = 0;
+ for (Entry<ResourceType, Integer> entry : resources.getQuantities().entrySet()) {
+ ResourceType type = entry.getKey();
+ int count = entry.getValue();
+ total += getCost(type, boughtResources.getProvider()) * count;
+ }
+ return total;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/scoring/PlayerScore.java b/backend/src/main/java/org/luxons/sevenwonders/game/scoring/PlayerScore.java
new file mode 100644
index 00000000..42acec54
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/scoring/PlayerScore.java
@@ -0,0 +1,33 @@
+package org.luxons.sevenwonders.game.scoring;
+
+import java.util.HashMap;
+
+import org.luxons.sevenwonders.game.Player;
+
+public class PlayerScore extends HashMap<ScoreCategory, Integer> {
+
+ private final Player player;
+
+ private final int boardGold;
+
+ private int totalPoints = 0;
+
+ public PlayerScore(Player player, int boardGold) {
+ this.player = player;
+ this.boardGold = boardGold;
+ }
+
+ @Override
+ public Integer put(ScoreCategory category, Integer points) {
+ totalPoints += points;
+ return super.put(category, points);
+ }
+
+ public int getTotalPoints() {
+ return totalPoints;
+ }
+
+ public int getBoardGold() {
+ return boardGold;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreBoard.java b/backend/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreBoard.java
new file mode 100644
index 00000000..26b5f8ba
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreBoard.java
@@ -0,0 +1,24 @@
+package org.luxons.sevenwonders.game.scoring;
+
+import java.util.Comparator;
+import java.util.PriorityQueue;
+
+public class ScoreBoard {
+
+ private static final Comparator<PlayerScore> comparator = Comparator.comparing(PlayerScore::getTotalPoints)
+ .thenComparing(PlayerScore::getBoardGold);
+
+ private PriorityQueue<PlayerScore> scores;
+
+ public ScoreBoard() {
+ scores = new PriorityQueue<>(comparator);
+ }
+
+ public void add(PlayerScore score) {
+ scores.add(score);
+ }
+
+ public PriorityQueue<PlayerScore> getScores() {
+ return scores;
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreCategory.java b/backend/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreCategory.java
new file mode 100644
index 00000000..54976072
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/scoring/ScoreCategory.java
@@ -0,0 +1,5 @@
+package org.luxons.sevenwonders.game.scoring;
+
+public enum ScoreCategory {
+ CIVIL, SCIENCE, MILITARY, TRADE, GUILD, WONDER, GOLD
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/wonders/Wonder.java b/backend/src/main/java/org/luxons/sevenwonders/game/wonders/Wonder.java
new file mode 100644
index 00000000..3ddddd30
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/wonders/Wonder.java
@@ -0,0 +1,102 @@
+package org.luxons.sevenwonders.game.wonders;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.cards.CardBack;
+import org.luxons.sevenwonders.game.resources.BoughtResources;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+
+public class Wonder {
+
+ private String name;
+
+ private ResourceType initialResource;
+
+ private List<WonderStage> stages;
+
+ private String image;
+
+ public Wonder() {
+ }
+
+ public Wonder(String name, ResourceType initialResource, WonderStage... stages) {
+ this.name = name;
+ this.initialResource = initialResource;
+ this.stages = Arrays.asList(stages);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public ResourceType getInitialResource() {
+ return initialResource;
+ }
+
+ public void setInitialResource(ResourceType initialResource) {
+ this.initialResource = initialResource;
+ }
+
+ public List<WonderStage> getStages() {
+ return stages;
+ }
+
+ public void setStages(List<WonderStage> stages) {
+ this.stages = stages;
+ }
+
+ public int getNbBuiltStages() {
+ return (int)stages.stream().filter(WonderStage::isBuilt).count();
+ }
+
+ public String getImage() {
+ return image;
+ }
+
+ public void setImage(String image) {
+ this.image = image;
+ }
+
+ public boolean isNextStageBuildable(Table table, int playerIndex, List<BoughtResources> boughtResources) {
+ int nextLevel = getNbBuiltStages();
+ if (nextLevel == stages.size()) {
+ return false;
+ }
+ return getNextStage().isBuildable(table, playerIndex, boughtResources);
+ }
+
+ public void buildLevel(CardBack cardBack) {
+ getNextStage().build(cardBack);
+ }
+
+ private WonderStage getNextStage() {
+ int nextLevel = getNbBuiltStages();
+ if (nextLevel == stages.size()) {
+ throw new IllegalStateException("This wonder has already reached its maximum level");
+ }
+ return stages.get(nextLevel);
+ }
+
+ public void activateLastBuiltStage(Table table, int playerIndex, List<BoughtResources> boughtResources) {
+ getLastBuiltStage().activate(table, playerIndex, boughtResources);
+ }
+
+ private WonderStage getLastBuiltStage() {
+ int lastLevel = getNbBuiltStages() - 1;
+ return stages.get(lastLevel);
+ }
+
+ public int computePoints(Table table, int playerIndex) {
+ return stages.stream()
+ .filter(WonderStage::isBuilt)
+ .flatMap(c -> c.getEffects().stream())
+ .mapToInt(e -> e.computePoints(table, playerIndex))
+ .sum();
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/game/wonders/WonderStage.java b/backend/src/main/java/org/luxons/sevenwonders/game/wonders/WonderStage.java
new file mode 100644
index 00000000..64d506fc
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/game/wonders/WonderStage.java
@@ -0,0 +1,50 @@
+package org.luxons.sevenwonders.game.wonders;
+
+import java.util.List;
+
+import org.luxons.sevenwonders.game.resources.BoughtResources;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.cards.CardBack;
+import org.luxons.sevenwonders.game.cards.Requirements;
+import org.luxons.sevenwonders.game.effects.Effect;
+
+public class WonderStage {
+
+ private Requirements requirements;
+
+ private List<Effect> effects;
+
+ private CardBack cardBack;
+
+ public Requirements getRequirements() {
+ return requirements;
+ }
+
+ public void setRequirements(Requirements requirements) {
+ this.requirements = requirements;
+ }
+
+ public List<Effect> getEffects() {
+ return effects;
+ }
+
+ public void setEffects(List<Effect> effects) {
+ this.effects = effects;
+ }
+
+ public boolean isBuilt() {
+ return cardBack != null;
+ }
+
+ public boolean isBuildable(Table table, int playerIndex, List<BoughtResources> boughtResources) {
+ return requirements.isAffordedBy(table, playerIndex, boughtResources);
+ }
+
+ void build(CardBack cardBack) {
+ this.cardBack = cardBack;
+ }
+
+ void activate(Table table, int playerIndex, List<BoughtResources> boughtResources) {
+ effects.forEach(e -> e.apply(table, playerIndex));
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/repositories/GameRepository.java b/backend/src/main/java/org/luxons/sevenwonders/repositories/GameRepository.java
new file mode 100644
index 00000000..efe39b85
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/repositories/GameRepository.java
@@ -0,0 +1,49 @@
+package org.luxons.sevenwonders.repositories;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.luxons.sevenwonders.errors.ApiMisuseException;
+import org.luxons.sevenwonders.game.Game;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class GameRepository {
+
+ private Map<Long, Game> games = new HashMap<>();
+
+ public void add(Game game) throws GameAlreadyExistsException {
+ if (games.containsKey(game.getId())) {
+ throw new GameAlreadyExistsException(game.getId());
+ }
+ games.put(game.getId(), game);
+ }
+
+ public Game find(long gameId) throws GameNotFoundException {
+ Game game = games.get(gameId);
+ if (game == null) {
+ throw new GameNotFoundException(gameId);
+ }
+ return game;
+ }
+
+ public Game remove(long gameId) throws GameNotFoundException {
+ Game game = games.remove(gameId);
+ if (game == null) {
+ throw new GameNotFoundException(gameId);
+ }
+ return game;
+ }
+
+ public static class GameNotFoundException extends ApiMisuseException {
+ GameNotFoundException(long id) {
+ super("Game " + id + " doesn't exist");
+ }
+ }
+
+ static class GameAlreadyExistsException extends ApiMisuseException {
+ GameAlreadyExistsException(long id) {
+ super("Game " + id + " already exists");
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/repositories/LobbyRepository.java b/backend/src/main/java/org/luxons/sevenwonders/repositories/LobbyRepository.java
new file mode 100644
index 00000000..8f305791
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/repositories/LobbyRepository.java
@@ -0,0 +1,59 @@
+package org.luxons.sevenwonders.repositories;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.luxons.sevenwonders.game.Lobby;
+import org.luxons.sevenwonders.game.Player;
+import org.luxons.sevenwonders.game.data.GameDefinitionLoader;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class LobbyRepository {
+
+ private final GameDefinitionLoader gameDefinitionLoader;
+
+ private Map<Long, Lobby> lobbies = new HashMap<>();
+
+ private long lastGameId = 0;
+
+ @Autowired
+ public LobbyRepository(GameDefinitionLoader gameDefinitionLoader) {
+ this.gameDefinitionLoader = gameDefinitionLoader;
+ }
+
+ public Collection<Lobby> list() {
+ return lobbies.values();
+ }
+
+ public Lobby create(String gameName, Player owner) {
+ long id = lastGameId++;
+ Lobby lobby = new Lobby(id, gameName, owner, gameDefinitionLoader.getGameDefinition());
+ lobbies.put(id, lobby);
+ return lobby;
+ }
+
+ public Lobby find(long lobbyId) throws LobbyNotFoundException {
+ Lobby lobby = lobbies.get(lobbyId);
+ if (lobby == null) {
+ throw new LobbyNotFoundException(lobbyId);
+ }
+ return lobby;
+ }
+
+ public Lobby remove(long lobbyId) throws LobbyNotFoundException {
+ Lobby lobby = lobbies.remove(lobbyId);
+ if (lobby == null) {
+ throw new LobbyNotFoundException(lobbyId);
+ }
+ return lobby;
+ }
+
+ public static class LobbyNotFoundException extends RuntimeException {
+ LobbyNotFoundException(long id) {
+ super("Lobby not found for id '" + id + "'");
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/repositories/PlayerRepository.java b/backend/src/main/java/org/luxons/sevenwonders/repositories/PlayerRepository.java
new file mode 100644
index 00000000..049c5ef9
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/repositories/PlayerRepository.java
@@ -0,0 +1,60 @@
+package org.luxons.sevenwonders.repositories;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.luxons.sevenwonders.errors.ApiMisuseException;
+import org.luxons.sevenwonders.game.Player;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class PlayerRepository {
+
+ private Map<String, Player> players = new HashMap<>();
+
+ public boolean contains(String username) {
+ return players.containsKey(username);
+ }
+
+ public Player createOrUpdate(String username, String displayName) {
+ if (players.containsKey(username)) {
+ return update(username, displayName);
+ } else {
+ return create(username, displayName);
+ }
+ }
+
+ private Player create(String username, String displayName) {
+ Player player = new Player(username, displayName);
+ players.put(username, player);
+ return player;
+ }
+
+ private Player update(String username, String displayName) throws PlayerNotFoundException {
+ Player player = find(username);
+ player.setDisplayName(displayName);
+ return player;
+ }
+
+ public Player find(String username) throws PlayerNotFoundException {
+ Player player = players.get(username);
+ if (player == null) {
+ throw new PlayerNotFoundException(username);
+ }
+ return player;
+ }
+
+ public Player remove(String username) {
+ Player player = players.remove(username);
+ if (player == null) {
+ throw new PlayerNotFoundException(username);
+ }
+ return player;
+ }
+
+ static class PlayerNotFoundException extends ApiMisuseException {
+ PlayerNotFoundException(String username) {
+ super("Player '" + username + "' doesn't exist");
+ }
+ }
+}
diff --git a/backend/src/main/java/org/luxons/sevenwonders/validation/DestinationAccessValidator.java b/backend/src/main/java/org/luxons/sevenwonders/validation/DestinationAccessValidator.java
new file mode 100644
index 00000000..65b3623c
--- /dev/null
+++ b/backend/src/main/java/org/luxons/sevenwonders/validation/DestinationAccessValidator.java
@@ -0,0 +1,76 @@
+package org.luxons.sevenwonders.validation;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.luxons.sevenwonders.game.Game;
+import org.luxons.sevenwonders.game.Lobby;
+import org.luxons.sevenwonders.repositories.GameRepository;
+import org.luxons.sevenwonders.repositories.LobbyRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DestinationAccessValidator {
+
+ private static final Pattern lobbyDestination = Pattern.compile(".*?/lobby/(?<id>\\d+?)(/.*)?");
+
+ private static final Pattern gameDestination = Pattern.compile(".*?/game/(?<id>\\d+?)(/.*)?");
+
+ private final LobbyRepository lobbyRepository;
+
+ private final GameRepository gameRepository;
+
+ @Autowired
+ public DestinationAccessValidator(LobbyRepository lobbyRepository, GameRepository gameRepository) {
+ this.lobbyRepository = lobbyRepository;
+ this.gameRepository = gameRepository;
+ }
+
+ public boolean hasAccess(String username, String destination) {
+ if (username == null) {
+ // unnamed user cannot belong to anything
+ return false;
+ }
+ if (hasForbiddenGameReference(username, destination)) {
+ return false;
+ }
+ if (hasForbiddenLobbyReference(username, destination)) {
+ return false;
+ }
+ return true;
+ }
+
+ private boolean hasForbiddenGameReference(String username, String destination) {
+ Matcher gameMatcher = gameDestination.matcher(destination);
+ if (!gameMatcher.matches()) {
+ return false; // no game reference is always OK
+ }
+ int gameId = extractId(gameMatcher);
+ return !isUserInGame(username, gameId);
+ }
+
+ private boolean hasForbiddenLobbyReference(String username, String destination) {
+ Matcher lobbyMatcher = lobbyDestination.matcher(destination);
+ if (!lobbyMatcher.matches()) {
+ return false; // no lobby reference is always OK
+ }
+ int lobbyId = extractId(lobbyMatcher);
+ return !isUserInLobby(username, lobbyId);
+ }
+
+ private boolean isUserInGame(String username, int gameId) {
+ Game game = gameRepository.find(gameId);
+ return game.containsUser(username);
+ }
+
+ private boolean isUserInLobby(String username, int lobbyId) {
+ Lobby lobby = lobbyRepository.find(lobbyId);
+ return lobby.containsUser(username);
+ }
+
+ private static int extractId(Matcher matcher) {
+ String id = matcher.group("id");
+ return Integer.parseInt(id);
+ }
+}
diff --git a/backend/src/main/resources/org/luxons/sevenwonders/game/data/cards.json b/backend/src/main/resources/org/luxons/sevenwonders/game/data/cards.json
new file mode 100644
index 00000000..bf48e95a
--- /dev/null
+++ b/backend/src/main/resources/org/luxons/sevenwonders/game/data/cards.json
@@ -0,0 +1,1719 @@
+{
+ "age1Back": "age1.png",
+ "age2Back": "age2.png",
+ "age3Back": "age3.png",
+ "age1": [
+ {
+ "name": "Clay Pit",
+ "color": "BROWN",
+ "effect": {
+ "production": "O/C"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 1
+ },
+ "image": "claypit.png"
+ },
+ {
+ "name": "Clay Pool",
+ "color": "BROWN",
+ "effect": {
+ "production": "C"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "claypool.png"
+ },
+ {
+ "name": "Excavation",
+ "color": "BROWN",
+ "effect": {
+ "production": "S/C"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 1
+ },
+ "image": "excavation.png"
+ },
+ {
+ "name": "Forest Cave",
+ "color": "BROWN",
+ "effect": {
+ "production": "W/O"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 0,
+ "5": 1,
+ "6": 1,
+ "7": 1
+ },
+ "image": "forestcave.png"
+ },
+ {
+ "name": "Lumber Yard",
+ "color": "BROWN",
+ "effect": {
+ "production": "W"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "lumberyard.png"
+ },
+ {
+ "name": "Mine",
+ "color": "BROWN",
+ "effect": {
+ "production": "S/O"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 0,
+ "5": 0,
+ "6": 1,
+ "7": 1
+ },
+ "image": "mine.png"
+ },
+ {
+ "name": "Ore Vein",
+ "color": "BROWN",
+ "effect": {
+ "production": "O"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "orevein.png"
+ },
+ {
+ "name": "Stone Pit",
+ "color": "BROWN",
+ "effect": {
+ "production": "S"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "stonepit.png"
+ },
+ {
+ "name": "Timber Yard",
+ "color": "BROWN",
+ "effect": {
+ "production": "W/S"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 1
+ },
+ "image": "timberyard.png"
+ },
+ {
+ "name": "Tree Farm",
+ "color": "BROWN",
+ "effect": {
+ "production": "W/C"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 0,
+ "5": 0,
+ "6": 1,
+ "7": 1
+ },
+ "image": "treefarm.png"
+ },
+ {
+ "name": "Glassworks",
+ "color": "GREY",
+ "effect": {
+ "production": "G"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "glassworks.png"
+ },
+ {
+ "name": "Loom",
+ "color": "GREY",
+ "effect": {
+ "production": "L"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "loom.png"
+ },
+ {
+ "name": "Press",
+ "color": "GREY",
+ "effect": {
+ "production": "P"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "press.png"
+ },
+ {
+ "name": "East Trading Post",
+ "color": "YELLOW",
+ "effect": {
+ "discount": {
+ "resourceTypes": "CSOW",
+ "providers": [
+ "RIGHT_PLAYER"
+ ],
+ "discountedPrice": 1
+ }
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [
+ "Forum"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "easttradingpost.png"
+ },
+ {
+ "name": "Marketplace",
+ "color": "YELLOW",
+ "effect": {
+ "discount": {
+ "resourceTypes": "LGP",
+ "providers": [
+ "LEFT_PLAYER",
+ "RIGHT_PLAYER"
+ ],
+ "discountedPrice": 1
+ }
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [
+ "Caravansery"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "marketplace.png"
+ },
+ {
+ "name": "Tavern",
+ "color": "YELLOW",
+ "effect": {
+ "gold": 5
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 3
+ },
+ "image": "tavern.png"
+ },
+ {
+ "name": "West Trading Post",
+ "color": "YELLOW",
+ "effect": {
+ "discount": {
+ "resourceTypes": "CSOW",
+ "providers": [
+ "LEFT_PLAYER"
+ ],
+ "discountedPrice": 1
+ }
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [
+ "Forum"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "westtradingpost.png"
+ },
+ {
+ "name": "Altar",
+ "color": "BLUE",
+ "effect": {
+ "points": 2
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [
+ "Temple"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "altar.png"
+ },
+ {
+ "name": "Baths",
+ "color": "BLUE",
+ "effect": {
+ "points": 3
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "S"
+ },
+ "chainChildren": [
+ "Aquaduct"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "baths.png"
+ },
+ {
+ "name": "Pawnshop",
+ "color": "BLUE",
+ "effect": {
+ "points": 3
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "pawnshop.png"
+ },
+ {
+ "name": "Theater",
+ "color": "BLUE",
+ "effect": {
+ "points": 2
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [
+ "Statue"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "theater.png"
+ },
+ {
+ "name": "Apothecary",
+ "color": "GREEN",
+ "effect": {
+ "science": "COMPASS"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "L"
+ },
+ "chainChildren": [
+ "Stables",
+ "Dispensary"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "apothecary.png"
+ },
+ {
+ "name": "Scriptorium",
+ "color": "GREEN",
+ "effect": {
+ "science": "TABLET"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "P"
+ },
+ "chainChildren": [
+ "Courthouse",
+ "Library"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "scriptorium.png"
+ },
+ {
+ "name": "Workshop",
+ "color": "GREEN",
+ "effect": {
+ "science": "WHEEL"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "G"
+ },
+ "chainChildren": [
+ "Archery Range",
+ "Laboratory"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "workshop.png"
+ },
+ {
+ "name": "Barracks",
+ "color": "RED",
+ "effect": {
+ "military": 1
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "O"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "barracks.png"
+ },
+ {
+ "name": "Guard Tower",
+ "color": "RED",
+ "effect": {
+ "military": 1
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "C"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "guardtower.png"
+ },
+ {
+ "name": "Stockade",
+ "color": "RED",
+ "effect": {
+ "military": 1
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "W"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "stockade.png"
+ }
+ ],
+ "age2": [
+ {
+ "name": "Brickyard",
+ "color": "BROWN",
+ "effect": {
+ "production": "CC"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "brickyard.png"
+ },
+ {
+ "name": "Foundry",
+ "color": "BROWN",
+ "effect": {
+ "production": "OO"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "foundry.png"
+ },
+ {
+ "name": "Quarry",
+ "color": "BROWN",
+ "effect": {
+ "production": "SS"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "quarry.png"
+ },
+ {
+ "name": "Sawmill",
+ "color": "BROWN",
+ "effect": {
+ "production": "WW"
+ },
+ "requirements": {
+ "gold": 1
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "sawmill.png"
+ },
+ {
+ "name": "Glassworks",
+ "color": "GREY",
+ "effect": {
+ "production": "G"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "glassworks.png"
+ },
+ {
+ "name": "Loom",
+ "color": "GREY",
+ "effect": {
+ "production": "L"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "loom.png"
+ },
+ {
+ "name": "Press",
+ "color": "GREY",
+ "effect": {
+ "production": "P"
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "press.png"
+ },
+ {
+ "name": "Bazar",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF",
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 2,
+ "type": "CARD",
+ "colors": [
+ "GREY"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "bazar.png"
+ },
+ {
+ "name": "Caravansery",
+ "color": "YELLOW",
+ "effect": {
+ "production": "W/S/O/C"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "chainParent": "Marketplace",
+ "chainChildren": [
+ "Lighthouse"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 3,
+ "7": 3
+ },
+ "image": "caravansery.png"
+ },
+ {
+ "name": "Forum",
+ "color": "YELLOW",
+ "effect": {
+ "production": "G/P/L"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CC"
+ },
+ "chainParent": "East Trading Post",
+ "chainChildren": [
+ "Haven"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 3
+ },
+ "image": "forum.png"
+ },
+ {
+ "name": "Vineyard",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF",
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BROWN"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "vineyard.png"
+ },
+ {
+ "name": "Aqueduct",
+ "color": "BLUE",
+ "effect": {
+ "points": 5
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSS"
+ },
+ "chainParent": "Baths",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "aqueduct.png"
+ },
+ {
+ "name": "Courthouse",
+ "color": "BLUE",
+ "effect": {
+ "points": 4
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CCL"
+ },
+ "chainParent": "Scriptorium",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "courthouse.png"
+ },
+ {
+ "name": "Statue",
+ "color": "BLUE",
+ "effect": {
+ "points": 4
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WOO"
+ },
+ "chainParent": "Theater",
+ "chainChildren": [
+ "Gardens"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "statue.png"
+ },
+ {
+ "name": "Temple",
+ "color": "BLUE",
+ "effect": {
+ "points": 3
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WCG"
+ },
+ "chainParent": "Altar",
+ "chainChildren": [
+ "Pantheon"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "temple.png"
+ },
+ {
+ "name": "Dispensary",
+ "color": "GREEN",
+ "effect": {
+ "science": "COMPASS"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "OOG"
+ },
+ "chainParent": "Apothecary",
+ "chainChildren": [
+ "Arena",
+ "Lodge"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "dispensary.png"
+ },
+ {
+ "name": "Laboratory",
+ "color": "GREEN",
+ "effect": {
+ "science": "WHEEL"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CCP"
+ },
+ "chainParent": "Workshop",
+ "chainChildren": [
+ "Siege Workshop",
+ "Observatory"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "laboratory.png"
+ },
+ {
+ "name": "Library",
+ "color": "GREEN",
+ "effect": {
+ "science": "TABLET"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSL"
+ },
+ "chainParent": "Scriptorium",
+ "chainChildren": [
+ "Senate",
+ "University"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "library.png"
+ },
+ {
+ "name": "School",
+ "color": "GREEN",
+ "effect": {
+ "science": "TABLET"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WP"
+ },
+ "chainChildren": [
+ "Academy",
+ "Study"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "school.png"
+ },
+ {
+ "name": "Archery Range",
+ "color": "RED",
+ "effect": {
+ "military": 2
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWO"
+ },
+ "chainParent": "Workshop",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "archeryrange.png"
+ },
+ {
+ "name": "Stables",
+ "color": "RED",
+ "effect": {
+ "military": 2
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WOC"
+ },
+ "chainParent": "Apothecary",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "stables.png"
+ },
+ {
+ "name": "Training Ground",
+ "color": "RED",
+ "effect": {
+ "military": 2
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WOO"
+ },
+ "chainChildren": [
+ "Circus"
+ ],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 3
+ },
+ "image": "trainingground.png"
+ },
+ {
+ "name": "Walls",
+ "color": "RED",
+ "effect": {
+ "military": 2
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSS"
+ },
+ "chainChildren": [
+ "Fortifications"
+ ],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "walls.png"
+ }
+ ],
+ "age3": [
+ {
+ "name": "Arena",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 3,
+ "points": 1,
+ "type": "WONDER_LEVEL"
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSO"
+ },
+ "chainParent": "Dispensary",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 3
+ },
+ "image": "arena.png"
+ },
+ {
+ "name": "Chamber of Commerce",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 2,
+ "points": 2,
+ "type": "CARD",
+ "colors": [
+ "GREY"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CCP"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "chamberofcommerce.png"
+ },
+ {
+ "name": "Haven",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 1,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BROWN"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WOL"
+ },
+ "chainParent": "Forum",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "haven.png"
+ },
+ {
+ "name": "Lighthouse",
+ "color": "YELLOW",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 1,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "GREY"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SG"
+ },
+ "chainParent": "Caravansery",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "lighthouse.png"
+ },
+ {
+ "name": "Gardens",
+ "color": "BLUE",
+ "effect": {
+ "points": 5
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WCC"
+ },
+ "chainParent": "Statue",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "gardens.png"
+ },
+ {
+ "name": "Palace",
+ "color": "BLUE",
+ "effect": {
+ "points": 8
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WSOCGPL"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "palace.png"
+ },
+ {
+ "name": "Pantheon",
+ "color": "BLUE",
+ "effect": {
+ "points": 7
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "OCCGPL"
+ },
+ "chainParent": "Temple",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "pantheon.png"
+ },
+ {
+ "name": "Senate",
+ "color": "BLUE",
+ "effect": {
+ "points": 6
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWSO"
+ },
+ "chainParent": "Library",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "senate.png"
+ },
+ {
+ "name": "Town Hall",
+ "color": "BLUE",
+ "effect": {
+ "points": 6
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSOG"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 3,
+ "7": 3
+ },
+ "image": "townhall.png"
+ },
+ {
+ "name": "Academy",
+ "color": "GREEN",
+ "effect": {
+ "science": "COMPASS"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSSG"
+ },
+ "chainParent": "School",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "academy.png"
+ },
+ {
+ "name": "Lodge",
+ "color": "GREEN",
+ "effect": {
+ "science": "COMPASS"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CCPL"
+ },
+ "chainParent": "Dispensary",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 2,
+ "7": 2
+ },
+ "image": "lodge.png"
+ },
+ {
+ "name": "Observatory",
+ "color": "GREEN",
+ "effect": {
+ "science": "WHEEL"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "OOGL"
+ },
+ "chainParent": "Laboratory",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "observatory.png"
+ },
+ {
+ "name": "Study",
+ "color": "GREEN",
+ "effect": {
+ "science": "WHEEL"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WPL"
+ },
+ "chainParent": "School",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "study.png"
+ },
+ {
+ "name": "University",
+ "color": "GREEN",
+ "effect": {
+ "science": "TABLET"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWGP"
+ },
+ "chainParent": "Library",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "university.png"
+ },
+ {
+ "name": "Arsenal",
+ "color": "RED",
+ "effect": {
+ "military": 3
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWOL"
+ },
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 2,
+ "5": 2,
+ "6": 2,
+ "7": 3
+ },
+ "image": "arsenal.png"
+ },
+ {
+ "name": "Circus",
+ "color": "RED",
+ "effect": {
+ "military": 3
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSSO"
+ },
+ "chainParent": "Training Ground",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 0,
+ "4": 1,
+ "5": 2,
+ "6": 3,
+ "7": 3
+ },
+ "image": "circus.png"
+ },
+ {
+ "name": "Fortifications",
+ "color": "RED",
+ "effect": {
+ "military": 3
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SOOO"
+ },
+ "chainParent": "Walls",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 1,
+ "6": 1,
+ "7": 2
+ },
+ "image": "fortifications.png"
+ },
+ {
+ "name": "Siege Workshop",
+ "color": "RED",
+ "effect": {
+ "military": 3
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WCCC"
+ },
+ "chainParent": "Laboratory",
+ "chainChildren": [],
+ "countPerNbPlayer": {
+ "3": 1,
+ "4": 1,
+ "5": 2,
+ "6": 2,
+ "7": 2
+ },
+ "image": "siegeworkshop.png"
+ }
+ ],
+ "guildCards": [
+ {
+ "name": "Builders Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "SELF",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "WONDER_LEVEL"
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSCCG"
+ },
+ "chainChildren": [],
+ "image": "buildersguild.png"
+ },
+ {
+ "name": "Craftsmens Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 2,
+ "type": "CARD",
+ "colors": [
+ "GREY"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SSOO"
+ },
+ "chainChildren": [],
+ "image": "craftsmensguild.png"
+ },
+ {
+ "name": "Magistrates Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BLUE"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWWSL"
+ },
+ "chainChildren": [],
+ "image": "magistratesguild.png"
+ },
+ {
+ "name": "Philosophers Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "GREEN"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CCCPL"
+ },
+ "chainChildren": [],
+ "image": "philosophersguild.png"
+ },
+ {
+ "name": "Scientists Guild",
+ "color": "PURPLE",
+ "effect": {
+ "science": "any"
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWOOP"
+ },
+ "chainChildren": [],
+ "image": "scientistsguild.png"
+ },
+ {
+ "name": "Shipowners Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "SELF"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BROWN",
+ "GREY",
+ "PURPLE"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WWWGP"
+ },
+ "chainChildren": [],
+ "image": "shipownersguild.png"
+ },
+ {
+ "name": "Spies Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "RED"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "CCCG"
+ },
+ "chainChildren": [],
+ "image": "spiesguild.png"
+ },
+ {
+ "name": "Strategists Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "DEFEAT_TOKEN"
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "SOOL"
+ },
+ "chainChildren": [],
+ "image": "strategistsguild.png"
+ },
+ {
+ "name": "Traders Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "YELLOW"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "GPL"
+ },
+ "chainChildren": [],
+ "image": "tradersguild.png"
+ },
+ {
+ "name": "Workers Guild",
+ "color": "PURPLE",
+ "effect": {
+ "perBoardElement": {
+ "boards": [
+ "LEFT",
+ "RIGHT"
+ ],
+ "gold": 0,
+ "points": 1,
+ "type": "CARD",
+ "colors": [
+ "BROWN"
+ ]
+ }
+ },
+ "requirements": {
+ "gold": 0,
+ "resources": "WSOOC"
+ },
+ "chainChildren": [],
+ "image": "workersguild.png"
+ }
+ ]
+} \ No newline at end of file
diff --git a/backend/src/main/resources/org/luxons/sevenwonders/game/data/wonders.json b/backend/src/main/resources/org/luxons/sevenwonders/game/data/wonders.json
new file mode 100644
index 00000000..9b4d0587
--- /dev/null
+++ b/backend/src/main/resources/org/luxons/sevenwonders/game/data/wonders.json
@@ -0,0 +1,515 @@
+[
+ {
+ "name": "alexandria",
+ "sides": {
+ "A": {
+ "initialResource": "G",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SS"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OO"
+ },
+ "effects": {
+ "production": "W/S/O/C"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "GG"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "alexandriaA.png"
+ },
+ "B": {
+ "initialResource": "G",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CC"
+ },
+ "effects": {
+ "production": "W/S/O/C"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "production": "G/P/L"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SSS"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "alexandriaB.png"
+ }
+ }
+ },
+ {
+ "name": "babylon",
+ "sides": {
+ "A": {
+ "initialResource": "C",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CC"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WWW"
+ },
+ "effects": {
+ "science": "any"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CCCC"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "babylonA.png"
+ },
+ "B": {
+ "initialResource": "C",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CL"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WWG"
+ },
+ "effects": {
+ "action": "PLAY_LAST_CARD"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CCCP"
+ },
+ "effects": {
+ "science": "any"
+ }
+ }
+ ],
+ "image": "babylonB.png"
+ }
+ }
+ },
+ {
+ "name": "ephesos",
+ "sides": {
+ "A": {
+ "initialResource": "P",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SS"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "gold": 9
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "PP"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "ephesosA.png"
+ },
+ "B": {
+ "initialResource": "P",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SS"
+ },
+ "effects": {
+ "gold": 4,
+ "points": 2
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "gold": 4,
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "GPL"
+ },
+ "effects": {
+ "gold": 4,
+ "points": 5
+ }
+ }
+ ],
+ "image": "ephesosB.png"
+ }
+ }
+ },
+ {
+ "name": "gizah",
+ "sides": {
+ "A": {
+ "initialResource": "S",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SS"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WWW"
+ },
+ "effects": {
+ "points": 5
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SSSS"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "gizahA.png"
+ },
+ "B": {
+ "initialResource": "S",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SSS"
+ },
+ "effects": {
+ "points": 5
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CCC"
+ },
+ "effects": {
+ "points": 5
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SSSSP"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "gizahB.png"
+ }
+ }
+ },
+ {
+ "name": "halikarnassus",
+ "sides": {
+ "A": {
+ "initialResource": "L",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CC"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OOO"
+ },
+ "effects": {
+ "action": "PLAY_DISCARDED"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "LL"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "halikarnassusA.png"
+ },
+ "B": {
+ "initialResource": "L",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OO"
+ },
+ "effects": {
+ "points": 2,
+ "action": "PLAY_DISCARDED"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CCC"
+ },
+ "effects": {
+ "points": 1,
+ "action": "PLAY_DISCARDED"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "GPL"
+ },
+ "effects": {
+ "action": "PLAY_DISCARDED"
+ }
+ }
+ ],
+ "image": "halikarnassusB.png"
+ }
+ }
+ },
+ {
+ "name": "olympia",
+ "sides": {
+ "A": {
+ "initialResource": "W",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SS"
+ },
+ "effects": {
+ "action": "ONE_FREE"
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OO"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "olympiaA.png"
+ },
+ "B": {
+ "initialResource": "W",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "discount": {
+ "resourceTypes": "WSOC",
+ "providers": [
+ "LEFT_PLAYER",
+ "RIGHT_PLAYER"
+ ],
+ "discountedPrice": 1
+ }
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SS"
+ },
+ "effects": {
+ "points": 5
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OOL"
+ },
+ "effects": {
+ "action": "COPY_GUILD"
+ }
+ }
+ ],
+ "image": "olympiaB.png"
+ }
+ }
+ },
+ {
+ "name": "rhodos",
+ "sides": {
+ "A": {
+ "initialResource": "O",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "WW"
+ },
+ "effects": {
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "CCC"
+ },
+ "effects": {
+ "military": 2
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OOOO"
+ },
+ "effects": {
+ "points": 7
+ }
+ }
+ ],
+ "image": "rhodosA.png"
+ },
+ "B": {
+ "initialResource": "O",
+ "stages": [
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "SSS"
+ },
+ "effects": {
+ "gold": 3,
+ "military": 1,
+ "points": 3
+ }
+ },
+ {
+ "requirements": {
+ "gold": 0,
+ "resources": "OOOO"
+ },
+ "effects": {
+ "gold": 4,
+ "military": 1,
+ "points": 4
+ }
+ }
+ ],
+ "image": "rhodosB.png"
+ }
+ }
+ }
+] \ No newline at end of file
diff --git a/backend/src/main/resources/static/app.js b/backend/src/main/resources/static/app.js
new file mode 100644
index 00000000..0d68d2ef
--- /dev/null
+++ b/backend/src/main/resources/static/app.js
@@ -0,0 +1,90 @@
+var stompClient = null;
+
+function setConnected(connected) {
+ $("#connect").prop("disabled", connected);
+ $("#disconnect").prop("disabled", !connected);
+ if (connected) {
+ $("#game-list").show();
+ } else {
+ $("#game-list").hide();
+ }
+ $("#greetings").html("");
+}
+
+function connect() {
+ var socket = new SockJS('/seven-wonders-websocket');
+ stompClient = Stomp.over(socket);
+ stompClient.connect({}, function (frame) {
+ setConnected(true);
+ console.log('Connected: ' + frame);
+
+ stompClient.subscribe('/user/queue/errors', function (msg) {
+ var error = JSON.parse(msg.body);
+ console.error(error);
+ });
+
+ stompClient.subscribe('/topic/games', function (msg) {
+ var games = JSON.parse(msg.body);
+ console.log("Received new games: " + games);
+ for (var i = 0; i < games.length; i++) {
+ addNewGame(games[i]);
+ }
+ });
+
+ stompClient.subscribe('/user/queue/join-game', function (msg) {
+ var game = JSON.parse(msg.body);
+ console.log("Joined game: " + game);
+ addNewPlayer(game);
+ });
+ });
+}
+
+function disconnect() {
+ if (stompClient !== null) {
+ stompClient.disconnect();
+ }
+ setConnected(false);
+ console.log("Disconnected");
+}
+
+function sendCreateGame(gameName, playerName) {
+ stompClient.send("/app/lobby/create-game", {}, JSON.stringify({
+ 'gameName': gameName,
+ 'playerName': playerName
+ }));
+}
+
+function sendJoinGame(gameName, playerName) {
+ stompClient.send("/app/lobby/join-game", {}, JSON.stringify({
+ 'gameName': gameName,
+ 'playerName': playerName
+ }));
+}
+
+function addNewGame(game) {
+ console.log(game);
+ $("#game-list-content").append('<tr><td>' + game.name + '</td><td><button id="join-' + game.id +
+ '" type="submit">Join</button></td></tr>');
+ $("#join-" + game.id).click(function () {
+ sendJoinGame(game.name, $("#player-name-field").val());
+ });
+}
+
+function addNewPlayer(player) {
+ console.log(player);
+}
+
+$(function () {
+ $("form").on('submit', function (e) {
+ e.preventDefault();
+ });
+ $("#connect").click(function () {
+ connect();
+ });
+ $("#disconnect").click(function () {
+ disconnect();
+ });
+ $("#create-game").click(function () {
+ sendCreateGame($("#game-name-field").val(), $("#player-name-field").val());
+ });
+}); \ No newline at end of file
diff --git a/backend/src/main/resources/static/images/background.jpg b/backend/src/main/resources/static/images/background.jpg
new file mode 100644
index 00000000..57bdffcf
--- /dev/null
+++ b/backend/src/main/resources/static/images/background.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/academy.png b/backend/src/main/resources/static/images/cards/academy.png
new file mode 100644
index 00000000..d2a75075
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/academy.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/age1.png b/backend/src/main/resources/static/images/cards/age1.png
new file mode 100644
index 00000000..a06332d7
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/age1.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/age2.png b/backend/src/main/resources/static/images/cards/age2.png
new file mode 100644
index 00000000..9b52aa4e
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/age2.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/age3.png b/backend/src/main/resources/static/images/cards/age3.png
new file mode 100644
index 00000000..86c983ee
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/age3.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/altar.png b/backend/src/main/resources/static/images/cards/altar.png
new file mode 100644
index 00000000..bbde8f2f
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/altar.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/apothecary.png b/backend/src/main/resources/static/images/cards/apothecary.png
new file mode 100644
index 00000000..01804c0a
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/apothecary.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/aqueduct.png b/backend/src/main/resources/static/images/cards/aqueduct.png
new file mode 100644
index 00000000..c29d9566
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/aqueduct.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/archeryrange.png b/backend/src/main/resources/static/images/cards/archeryrange.png
new file mode 100644
index 00000000..15c6edda
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/archeryrange.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/arena.png b/backend/src/main/resources/static/images/cards/arena.png
new file mode 100644
index 00000000..7dc76961
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/arena.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/arsenal.png b/backend/src/main/resources/static/images/cards/arsenal.png
new file mode 100644
index 00000000..fc3f4a27
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/arsenal.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/barracks.png b/backend/src/main/resources/static/images/cards/barracks.png
new file mode 100644
index 00000000..f5a68c17
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/barracks.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/baths.png b/backend/src/main/resources/static/images/cards/baths.png
new file mode 100644
index 00000000..3d99d59d
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/baths.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/bazar.png b/backend/src/main/resources/static/images/cards/bazar.png
new file mode 100644
index 00000000..f36e25c2
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/bazar.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/brickyard.png b/backend/src/main/resources/static/images/cards/brickyard.png
new file mode 100644
index 00000000..ae0b7e9b
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/brickyard.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/buildersguild.png b/backend/src/main/resources/static/images/cards/buildersguild.png
new file mode 100644
index 00000000..f5402611
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/buildersguild.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/caravansery.png b/backend/src/main/resources/static/images/cards/caravansery.png
new file mode 100644
index 00000000..997bb102
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/caravansery.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/chamberofcommerce.png b/backend/src/main/resources/static/images/cards/chamberofcommerce.png
new file mode 100644
index 00000000..44b5af28
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/chamberofcommerce.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/circus.png b/backend/src/main/resources/static/images/cards/circus.png
new file mode 100644
index 00000000..b1ec4d8b
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/circus.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/claypit.png b/backend/src/main/resources/static/images/cards/claypit.png
new file mode 100644
index 00000000..5442248e
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/claypit.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/claypool.png b/backend/src/main/resources/static/images/cards/claypool.png
new file mode 100644
index 00000000..873cad47
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/claypool.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/courthouse.png b/backend/src/main/resources/static/images/cards/courthouse.png
new file mode 100644
index 00000000..394901f2
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/courthouse.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/craftsmensguild.png b/backend/src/main/resources/static/images/cards/craftsmensguild.png
new file mode 100644
index 00000000..09bff60e
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/craftsmensguild.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/dispensary.png b/backend/src/main/resources/static/images/cards/dispensary.png
new file mode 100644
index 00000000..4917166b
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/dispensary.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/easttradingpost.png b/backend/src/main/resources/static/images/cards/easttradingpost.png
new file mode 100644
index 00000000..0c67cc78
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/easttradingpost.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/excavation.png b/backend/src/main/resources/static/images/cards/excavation.png
new file mode 100644
index 00000000..0fe1b01f
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/excavation.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/forestcave.png b/backend/src/main/resources/static/images/cards/forestcave.png
new file mode 100644
index 00000000..262fffc6
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/forestcave.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/fortifications.png b/backend/src/main/resources/static/images/cards/fortifications.png
new file mode 100644
index 00000000..3e113473
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/fortifications.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/forum.png b/backend/src/main/resources/static/images/cards/forum.png
new file mode 100644
index 00000000..d6262158
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/forum.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/foundry.png b/backend/src/main/resources/static/images/cards/foundry.png
new file mode 100644
index 00000000..da95a48e
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/foundry.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/gardens.png b/backend/src/main/resources/static/images/cards/gardens.png
new file mode 100644
index 00000000..9a49a0ad
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/gardens.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/glassworks.png b/backend/src/main/resources/static/images/cards/glassworks.png
new file mode 100644
index 00000000..285d7d54
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/glassworks.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/guardtower.png b/backend/src/main/resources/static/images/cards/guardtower.png
new file mode 100644
index 00000000..524b06f3
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/guardtower.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/haven.png b/backend/src/main/resources/static/images/cards/haven.png
new file mode 100644
index 00000000..e0b345b2
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/haven.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/laboratory.png b/backend/src/main/resources/static/images/cards/laboratory.png
new file mode 100644
index 00000000..4c29e81f
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/laboratory.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/library.png b/backend/src/main/resources/static/images/cards/library.png
new file mode 100644
index 00000000..7495a2ca
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/library.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/lighthouse.png b/backend/src/main/resources/static/images/cards/lighthouse.png
new file mode 100644
index 00000000..2124811b
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/lighthouse.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/lodge.png b/backend/src/main/resources/static/images/cards/lodge.png
new file mode 100644
index 00000000..22758688
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/lodge.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/loom.png b/backend/src/main/resources/static/images/cards/loom.png
new file mode 100644
index 00000000..70bdf375
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/loom.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/lumberyard.png b/backend/src/main/resources/static/images/cards/lumberyard.png
new file mode 100644
index 00000000..8558af1a
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/lumberyard.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/magistratesguild.png b/backend/src/main/resources/static/images/cards/magistratesguild.png
new file mode 100644
index 00000000..d7deabb3
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/magistratesguild.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/marketplace.png b/backend/src/main/resources/static/images/cards/marketplace.png
new file mode 100644
index 00000000..cd3676d4
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/marketplace.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/mine.png b/backend/src/main/resources/static/images/cards/mine.png
new file mode 100644
index 00000000..4062775c
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/mine.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/observatory.png b/backend/src/main/resources/static/images/cards/observatory.png
new file mode 100644
index 00000000..1da3d7b4
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/observatory.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/orevein.png b/backend/src/main/resources/static/images/cards/orevein.png
new file mode 100644
index 00000000..fabea674
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/orevein.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/palace.png b/backend/src/main/resources/static/images/cards/palace.png
new file mode 100644
index 00000000..1a24890e
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/palace.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/pantheon.png b/backend/src/main/resources/static/images/cards/pantheon.png
new file mode 100644
index 00000000..264bae02
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/pantheon.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/pawnshop.png b/backend/src/main/resources/static/images/cards/pawnshop.png
new file mode 100644
index 00000000..30bb3807
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/pawnshop.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/philosophersguild.png b/backend/src/main/resources/static/images/cards/philosophersguild.png
new file mode 100644
index 00000000..f72590f6
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/philosophersguild.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/press.png b/backend/src/main/resources/static/images/cards/press.png
new file mode 100644
index 00000000..c932df06
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/press.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/quarry.png b/backend/src/main/resources/static/images/cards/quarry.png
new file mode 100644
index 00000000..8cdbdb22
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/quarry.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/sawmill.png b/backend/src/main/resources/static/images/cards/sawmill.png
new file mode 100644
index 00000000..5abff473
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/sawmill.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/school.png b/backend/src/main/resources/static/images/cards/school.png
new file mode 100644
index 00000000..ab2218d0
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/school.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/scientistsguild.png b/backend/src/main/resources/static/images/cards/scientistsguild.png
new file mode 100644
index 00000000..7ee639e3
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/scientistsguild.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/scriptorium.png b/backend/src/main/resources/static/images/cards/scriptorium.png
new file mode 100644
index 00000000..36dca27a
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/scriptorium.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/senate.png b/backend/src/main/resources/static/images/cards/senate.png
new file mode 100644
index 00000000..ee878ea6
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/senate.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/shipownersguild.png b/backend/src/main/resources/static/images/cards/shipownersguild.png
new file mode 100644
index 00000000..3eecd2da
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/shipownersguild.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/siegeworkshop.png b/backend/src/main/resources/static/images/cards/siegeworkshop.png
new file mode 100644
index 00000000..bacf8309
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/siegeworkshop.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/spiesguild.png b/backend/src/main/resources/static/images/cards/spiesguild.png
new file mode 100644
index 00000000..85e28d9e
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/spiesguild.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/stables.png b/backend/src/main/resources/static/images/cards/stables.png
new file mode 100644
index 00000000..48c963f0
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/stables.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/statue.png b/backend/src/main/resources/static/images/cards/statue.png
new file mode 100644
index 00000000..55aaa5cb
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/statue.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/stockade.png b/backend/src/main/resources/static/images/cards/stockade.png
new file mode 100644
index 00000000..37741429
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/stockade.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/stonepit.png b/backend/src/main/resources/static/images/cards/stonepit.png
new file mode 100644
index 00000000..724900c7
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/stonepit.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/strategistsguild.png b/backend/src/main/resources/static/images/cards/strategistsguild.png
new file mode 100644
index 00000000..ae186a4b
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/strategistsguild.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/study.png b/backend/src/main/resources/static/images/cards/study.png
new file mode 100644
index 00000000..d8b9ebf9
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/study.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/tavern.png b/backend/src/main/resources/static/images/cards/tavern.png
new file mode 100644
index 00000000..418b0fb2
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/tavern.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/temple.png b/backend/src/main/resources/static/images/cards/temple.png
new file mode 100644
index 00000000..9a8d89dc
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/temple.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/theater.png b/backend/src/main/resources/static/images/cards/theater.png
new file mode 100644
index 00000000..0d5b2b01
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/theater.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/timberyard.png b/backend/src/main/resources/static/images/cards/timberyard.png
new file mode 100644
index 00000000..0f20547f
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/timberyard.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/townhall.png b/backend/src/main/resources/static/images/cards/townhall.png
new file mode 100644
index 00000000..d0638739
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/townhall.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/tradersguild.png b/backend/src/main/resources/static/images/cards/tradersguild.png
new file mode 100644
index 00000000..15777e77
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/tradersguild.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/trainingground.png b/backend/src/main/resources/static/images/cards/trainingground.png
new file mode 100644
index 00000000..d59ef4f8
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/trainingground.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/treefarm.png b/backend/src/main/resources/static/images/cards/treefarm.png
new file mode 100644
index 00000000..18cf228f
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/treefarm.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/university.png b/backend/src/main/resources/static/images/cards/university.png
new file mode 100644
index 00000000..c9ca8a80
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/university.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/vineyard.png b/backend/src/main/resources/static/images/cards/vineyard.png
new file mode 100644
index 00000000..58fa8ee1
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/vineyard.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/walls.png b/backend/src/main/resources/static/images/cards/walls.png
new file mode 100644
index 00000000..3823c62f
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/walls.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/westtradingpost.png b/backend/src/main/resources/static/images/cards/westtradingpost.png
new file mode 100644
index 00000000..b536269f
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/westtradingpost.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/workersguild.png b/backend/src/main/resources/static/images/cards/workersguild.png
new file mode 100644
index 00000000..de4f452f
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/workersguild.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/cards/workshop.png b/backend/src/main/resources/static/images/cards/workshop.png
new file mode 100644
index 00000000..8f585d61
--- /dev/null
+++ b/backend/src/main/resources/static/images/cards/workshop.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/buy.png b/backend/src/main/resources/static/images/tokens/buy.png
new file mode 100644
index 00000000..07af65a3
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/buy.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/card.png b/backend/src/main/resources/static/images/tokens/card.png
new file mode 100644
index 00000000..fcdbc068
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/card.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/check.png b/backend/src/main/resources/static/images/tokens/check.png
new file mode 100644
index 00000000..98db5be0
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/check.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/clay.png b/backend/src/main/resources/static/images/tokens/clay.png
new file mode 100644
index 00000000..72fc0b0e
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/clay.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/coin.png b/backend/src/main/resources/static/images/tokens/coin.png
new file mode 100644
index 00000000..f4813042
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/coin.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/coin1.png b/backend/src/main/resources/static/images/tokens/coin1.png
new file mode 100644
index 00000000..dd57e5f0
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/coin1.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/coin3.png b/backend/src/main/resources/static/images/tokens/coin3.png
new file mode 100644
index 00000000..546d41b6
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/coin3.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/free.png b/backend/src/main/resources/static/images/tokens/free.png
new file mode 100644
index 00000000..1c8d0782
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/free.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/glass.png b/backend/src/main/resources/static/images/tokens/glass.png
new file mode 100644
index 00000000..61fd2be5
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/glass.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/linen.png b/backend/src/main/resources/static/images/tokens/linen.png
new file mode 100644
index 00000000..294adcb2
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/linen.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/no.png b/backend/src/main/resources/static/images/tokens/no.png
new file mode 100644
index 00000000..78d09fea
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/no.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/ore.png b/backend/src/main/resources/static/images/tokens/ore.png
new file mode 100644
index 00000000..c2149daa
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/ore.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/paper.png b/backend/src/main/resources/static/images/tokens/paper.png
new file mode 100644
index 00000000..91a59221
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/paper.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/pyramid-stage0.png b/backend/src/main/resources/static/images/tokens/pyramid-stage0.png
new file mode 100644
index 00000000..b6a3977f
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/pyramid-stage0.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/pyramid-stage1.png b/backend/src/main/resources/static/images/tokens/pyramid-stage1.png
new file mode 100644
index 00000000..ead4a34e
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/pyramid-stage1.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/pyramid-stage2.png b/backend/src/main/resources/static/images/tokens/pyramid-stage2.png
new file mode 100644
index 00000000..7239a3a4
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/pyramid-stage2.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/pyramid-stage3.png b/backend/src/main/resources/static/images/tokens/pyramid-stage3.png
new file mode 100644
index 00000000..cab9912b
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/pyramid-stage3.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/pyramid.png b/backend/src/main/resources/static/images/tokens/pyramid.png
new file mode 100644
index 00000000..074247da
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/pyramid.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/stone.png b/backend/src/main/resources/static/images/tokens/stone.png
new file mode 100644
index 00000000..674c40db
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/stone.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/trash.png b/backend/src/main/resources/static/images/tokens/trash.png
new file mode 100644
index 00000000..086df817
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/trash.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/victory1.png b/backend/src/main/resources/static/images/tokens/victory1.png
new file mode 100644
index 00000000..6b9aff29
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/victory1.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/victory3.png b/backend/src/main/resources/static/images/tokens/victory3.png
new file mode 100644
index 00000000..474cb30c
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/victory3.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/victory5.png b/backend/src/main/resources/static/images/tokens/victory5.png
new file mode 100644
index 00000000..ad042119
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/victory5.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/victoryminus1.png b/backend/src/main/resources/static/images/tokens/victoryminus1.png
new file mode 100644
index 00000000..00a615c7
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/victoryminus1.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/tokens/wood.png b/backend/src/main/resources/static/images/tokens/wood.png
new file mode 100644
index 00000000..09a4ede8
--- /dev/null
+++ b/backend/src/main/resources/static/images/tokens/wood.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/alexandriaA.png b/backend/src/main/resources/static/images/wonders/alexandriaA.png
new file mode 100644
index 00000000..416d534e
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/alexandriaA.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/alexandriaB.png b/backend/src/main/resources/static/images/wonders/alexandriaB.png
new file mode 100644
index 00000000..205a5256
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/alexandriaB.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/babylonA.png b/backend/src/main/resources/static/images/wonders/babylonA.png
new file mode 100644
index 00000000..f8e3725e
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/babylonA.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/babylonB.png b/backend/src/main/resources/static/images/wonders/babylonB.png
new file mode 100644
index 00000000..53f6f045
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/babylonB.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/ephesosA.png b/backend/src/main/resources/static/images/wonders/ephesosA.png
new file mode 100644
index 00000000..285c8edf
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/ephesosA.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/ephesosB.png b/backend/src/main/resources/static/images/wonders/ephesosB.png
new file mode 100644
index 00000000..1e0e2541
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/ephesosB.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/extra/agrigentoA.jpg b/backend/src/main/resources/static/images/wonders/extra/agrigentoA.jpg
new file mode 100644
index 00000000..76ba8195
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/extra/agrigentoA.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/extra/angkorwatA.jpg b/backend/src/main/resources/static/images/wonders/extra/angkorwatA.jpg
new file mode 100644
index 00000000..32f52514
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/extra/angkorwatA.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/extra/angkorwatB.jpg b/backend/src/main/resources/static/images/wonders/extra/angkorwatB.jpg
new file mode 100644
index 00000000..c3f4304e
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/extra/angkorwatB.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/extra/avalonA.jpg b/backend/src/main/resources/static/images/wonders/extra/avalonA.jpg
new file mode 100644
index 00000000..7f7f0678
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/extra/avalonA.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/extra/ctesiphonB.jpg b/backend/src/main/resources/static/images/wonders/extra/ctesiphonB.jpg
new file mode 100644
index 00000000..c00b40ac
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/extra/ctesiphonB.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/extra/iramA.jpg b/backend/src/main/resources/static/images/wonders/extra/iramA.jpg
new file mode 100644
index 00000000..d2c24e95
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/extra/iramA.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/extra/persepolisA.jpg b/backend/src/main/resources/static/images/wonders/extra/persepolisA.jpg
new file mode 100644
index 00000000..2caa4f89
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/extra/persepolisA.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/extra/romaA.jpg b/backend/src/main/resources/static/images/wonders/extra/romaA.jpg
new file mode 100644
index 00000000..c54bc820
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/extra/romaA.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/extra/sangri-laA.jpg b/backend/src/main/resources/static/images/wonders/extra/sangri-laA.jpg
new file mode 100644
index 00000000..1c5dad97
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/extra/sangri-laA.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/extra/spahanA.jpg b/backend/src/main/resources/static/images/wonders/extra/spahanA.jpg
new file mode 100644
index 00000000..ab2cfc84
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/extra/spahanA.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/extra/the-great-wallA.jpg b/backend/src/main/resources/static/images/wonders/extra/the-great-wallA.jpg
new file mode 100644
index 00000000..4aacd39b
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/extra/the-great-wallA.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/extra/veniseA.jpg b/backend/src/main/resources/static/images/wonders/extra/veniseA.jpg
new file mode 100644
index 00000000..55ec00b5
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/extra/veniseA.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/extra/veniseB.jpg b/backend/src/main/resources/static/images/wonders/extra/veniseB.jpg
new file mode 100644
index 00000000..e18f3a12
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/extra/veniseB.jpg
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/gizahA.png b/backend/src/main/resources/static/images/wonders/gizahA.png
new file mode 100644
index 00000000..5e755594
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/gizahA.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/gizahB.png b/backend/src/main/resources/static/images/wonders/gizahB.png
new file mode 100644
index 00000000..60b90fed
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/gizahB.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/halikarnassusA.png b/backend/src/main/resources/static/images/wonders/halikarnassusA.png
new file mode 100644
index 00000000..5e6acc36
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/halikarnassusA.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/halikarnassusB.png b/backend/src/main/resources/static/images/wonders/halikarnassusB.png
new file mode 100644
index 00000000..42d67786
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/halikarnassusB.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/olympiaA.png b/backend/src/main/resources/static/images/wonders/olympiaA.png
new file mode 100644
index 00000000..315c090b
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/olympiaA.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/olympiaB.png b/backend/src/main/resources/static/images/wonders/olympiaB.png
new file mode 100644
index 00000000..a6c81af6
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/olympiaB.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/rhodosA.png b/backend/src/main/resources/static/images/wonders/rhodosA.png
new file mode 100644
index 00000000..13ea69e1
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/rhodosA.png
Binary files differ
diff --git a/backend/src/main/resources/static/images/wonders/rhodosB.png b/backend/src/main/resources/static/images/wonders/rhodosB.png
new file mode 100644
index 00000000..2cfa4e18
--- /dev/null
+++ b/backend/src/main/resources/static/images/wonders/rhodosB.png
Binary files differ
diff --git a/backend/src/main/resources/static/index.html b/backend/src/main/resources/static/index.html
new file mode 100644
index 00000000..d5ec178d
--- /dev/null
+++ b/backend/src/main/resources/static/index.html
@@ -0,0 +1,64 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Seven Wonders</title>
+ <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+ <link href="/main.css" rel="stylesheet">
+ <script src="/webjars/jquery/jquery.min.js"></script>
+ <script src="/webjars/sockjs-client/sockjs.min.js"></script>
+ <script src="/webjars/stomp-websocket/stomp.min.js"></script>
+ <script src="app.js"></script>
+</head>
+<body>
+<noscript>
+ <h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
+ enabled. Please enable Javascript and reload this page!</h2>
+</noscript>
+
+<h1>Seven Wonders</h1>
+
+<p>This is a stub index page for the project, for the sake of vertical completeness. We will soon get to work on it!</p>
+
+<a href="test.html">Go to WS test page</a>
+
+
+<h2>Connection</h2>
+
+<form class="form-inline">
+ <div class="form-group">
+ <label for="connect">WebSocket connection:</label>
+ <button id="connect" class="btn btn-default" type="submit">Connect</button>
+ <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect</button>
+ </div>
+</form>
+
+<h2>Games</h2>
+
+<form class="form-inline">
+ <div class="form-group">
+ <label for="player-name-field">Player name</label>
+ <input id="player-name-field">
+ </div>
+</form>
+
+<table id="game-list" class="table table-striped">
+ <thead>
+ <tr>
+ <th>Id</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody id="game-list-content">
+ </tbody>
+</table>
+
+<form class="form-inline">
+ <div class="form-group">
+ <label for="game-name-field">Game name</label>
+ <input id="game-name-field">
+ <button id="create-game" class="btn btn-default" type="submit">Create</button>
+ </div>
+</form>
+
+</body>
+</html> \ No newline at end of file
diff --git a/backend/src/main/resources/static/test-ws.js b/backend/src/main/resources/static/test-ws.js
new file mode 100644
index 00000000..1c64349e
--- /dev/null
+++ b/backend/src/main/resources/static/test-ws.js
@@ -0,0 +1,40 @@
+var stompClient = null;
+
+function connect() {
+ console.log('Connecting...');
+ var socket = new SockJS('/seven-wonders-websocket');
+ stompClient = Stomp.over(socket);
+ stompClient.connect({}, function (frame) {
+ console.log('Connected: ' + frame);
+ subscribeTo('/user/queue/errors');
+ });
+}
+
+function send(endpoint, payload) {
+ stompClient.send(endpoint, {}, payload);
+}
+
+function subscribeTo(endpoint) {
+ $("#test-feeds").prepend('<tr><td>' + endpoint + '</td><td>Subscribed</td></tr>');
+ stompClient.subscribe(endpoint, function (data) {
+ $("#test-feeds").prepend('<tr><td>' + endpoint + '</td><td>Received: <pre>' + data.body + '</pre></td></tr>');
+ });
+}
+
+$(function () {
+ $("form").on('submit', function (e) {
+ e.preventDefault();
+ });
+ $("#send-btn").click(function () {
+ var endpoint = $("#path-field").val();
+ var payload = $("#payload-field").val();
+ send(endpoint, payload);
+ });
+ $("#subscribe-btn").click(function () {
+ var endpoint = $("#subscribe-path-field").val();
+ subscribeTo(endpoint);
+ });
+});
+
+// auto-connect
+connect(); \ No newline at end of file
diff --git a/backend/src/main/resources/static/test.html b/backend/src/main/resources/static/test.html
new file mode 100644
index 00000000..e19f9eb3
--- /dev/null
+++ b/backend/src/main/resources/static/test.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Seven Wonders</title>
+ <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+ <link href="/main.css" rel="stylesheet">
+ <script src="/webjars/jquery/jquery.min.js"></script>
+ <script src="/webjars/sockjs-client/sockjs.min.js"></script>
+ <script src="/webjars/stomp-websocket/stomp.min.js"></script>
+ <script src="test-ws.js"></script>
+</head>
+<body>
+<noscript>
+ <h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
+ enabled. Please enable Javascript and reload this page!</h2>
+</noscript>
+
+<h1>Seven Wonders Test Page</h1>
+
+<h2>WS messages tests</h2>
+
+<form class="form-inline">
+ <div class="form-group">
+ <label for="subscribe-path-field">Path:</label>
+ <input id="subscribe-path-field" placeholder="path">
+ <button id="subscribe-btn" class="btn btn-default" type="submit">Subscribe</button>
+ </div>
+</form>
+
+<form class="form-inline">
+ <div class="form-group">
+ <label for="path-field">Path:</label>
+ <input id="path-field" placeholder="path">
+ <label for="payload-field">Payload:</label>
+ <input id="payload-field" placeholder="JSON payload">
+ <button id="send-btn" class="btn btn-default" type="submit">Send</button>
+ </div>
+</form>
+
+<h2>Subscribed feeds</h2>
+
+<table class="table table-striped">
+ <thead>
+ <tr>
+ <th>Endpoint</th>
+ <th>Data received</th>
+ </tr>
+ </thead>
+ <tbody id="test-feeds">
+ </tbody>
+</table>
+
+
+</body>
+</html> \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/LobbyTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/LobbyTest.java
new file mode 100644
index 00000000..4a12592e
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/LobbyTest.java
@@ -0,0 +1,170 @@
+package org.luxons.sevenwonders.game;
+
+import java.util.Arrays;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.Lobby.GameAlreadyStartedException;
+import org.luxons.sevenwonders.game.Lobby.PlayerNameAlreadyUsedException;
+import org.luxons.sevenwonders.game.Lobby.PlayerOverflowException;
+import org.luxons.sevenwonders.game.Lobby.PlayerUnderflowException;
+import org.luxons.sevenwonders.game.Lobby.UnknownPlayerException;
+import org.luxons.sevenwonders.game.data.GameDefinition;
+import org.luxons.sevenwonders.game.data.GameDefinitionLoader;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+@RunWith(Theories.class)
+public class LobbyTest {
+
+ @DataPoints
+ public static int[] nbPlayers() {
+ return new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+ }
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private static GameDefinition gameDefinition;
+
+ private Player gameOwner;
+
+ private Lobby lobby;
+
+ @BeforeClass
+ public static void loadDefinition() {
+ gameDefinition = new GameDefinitionLoader().getGameDefinition();
+ }
+
+ @Before
+ public void setUp() {
+ gameOwner = new Player("gameowner", "Game owner");
+ lobby = new Lobby(0, "Test Game", gameOwner, gameDefinition);
+ }
+
+ @Test
+ public void testId() {
+ Lobby l = new Lobby(5, "Test Game", gameOwner, gameDefinition);
+ assertEquals(5, l.getId());
+ }
+
+ @Test
+ public void testName() {
+ Lobby l = new Lobby(5, "Test Game", gameOwner, gameDefinition);
+ assertEquals("Test Game", l.getName());
+ }
+
+ @Test
+ public void isOwner_falseWhenNull() {
+ assertFalse(lobby.isOwner(null));
+ }
+
+ @Test
+ public void isOwner_falseWhenEmptyString() {
+ assertFalse(lobby.isOwner(""));
+ }
+
+ @Test
+ public void isOwner_falseWhenGarbageString() {
+ assertFalse(lobby.isOwner("this is garbage"));
+ }
+
+ @Test
+ public void isOwner_trueWhenOwnerUsername() {
+ assertTrue(lobby.isOwner(gameOwner.getUsername()));
+ }
+
+ @Test
+ public void isOwner_falseWhenOtherPlayerName() {
+ Player player = new Player("testuser", "Test User");
+ lobby.addPlayer(player);
+ assertFalse(lobby.isOwner(player.getUsername()));
+ }
+
+ @Test
+ public void addPlayer_success() {
+ Player player = new Player("testuser", "Test User");
+ lobby.addPlayer(player);
+ assertTrue(lobby.containsUser("testuser"));
+ }
+
+ @Test(expected = PlayerNameAlreadyUsedException.class)
+ public void addPlayer_failsOnSameName() {
+ Player player = new Player("testuser", "Test User");
+ Player player2 = new Player("testuser2", "Test User");
+ lobby.addPlayer(player);
+ lobby.addPlayer(player2);
+ }
+
+ @Test(expected = PlayerOverflowException.class)
+ public void addPlayer_playerOverflowWhenTooMany() {
+ // the owner + the max number gives an overflow
+ addPlayers(gameDefinition.getMaxPlayers());
+ }
+
+ @Test(expected = GameAlreadyStartedException.class)
+ public void addPlayer_failWhenGameStarted() {
+ // total with owner is the minimum
+ addPlayers(gameDefinition.getMinPlayers() - 1);
+ lobby.startGame();
+ lobby.addPlayer(new Player("soonerNextTime", "The Late Guy"));
+ }
+
+ private void addPlayers(int nbPlayers) {
+ for (int i = 0; i < nbPlayers; i++) {
+ Player player = new Player("testuser" + i, "Test User " + i);
+ lobby.addPlayer(player);
+ }
+ }
+
+ @Test
+ public void reorderPlayers_failsOnSameName() {
+ Player player1 = new Player("testuser1", "Test User 1");
+ Player player2 = new Player("testuser2", "Test User 2");
+ Player player3 = new Player("testuser3", "Test User 3");
+ lobby.addPlayer(player1);
+ lobby.addPlayer(player2);
+ lobby.addPlayer(player3);
+ lobby.reorderPlayers(Arrays.asList("testuser3", "testuser1", "testuser2"));
+ assertEquals("testuser3", lobby.getPlayers().get(0).getUsername());
+ assertEquals("testuser1", lobby.getPlayers().get(1).getUsername());
+ assertEquals("testuser2", lobby.getPlayers().get(2).getUsername());
+ }
+
+ @Test(expected = UnknownPlayerException.class)
+ public void reorderPlayers_failsOnUnknownPlayer() {
+ Player player1 = new Player("testuser1", "Test User 1");
+ Player player2 = new Player("testuser2", "Test User 2");
+ Player player3 = new Player("testuser3", "Test User 3");
+ lobby.addPlayer(player1);
+ lobby.addPlayer(player2);
+ lobby.addPlayer(player3);
+ lobby.reorderPlayers(Arrays.asList("testuser4", "testuser1", "testuser2"));
+ }
+
+ @Theory
+ public void startGame_failsBelowMinPlayers(int nbPlayers) {
+ assumeTrue(nbPlayers < gameDefinition.getMinPlayers());
+ thrown.expect(PlayerUnderflowException.class);
+ // there is already the owner
+ addPlayers(nbPlayers - 1);
+ lobby.startGame();
+ }
+
+ @Theory
+ public void startGame_succeedsAboveMinPlayers(int nbPlayers) {
+ assumeTrue(nbPlayers >= gameDefinition.getMinPlayers());
+ assumeTrue(nbPlayers < gameDefinition.getMaxPlayers());
+ // there is already the owner
+ addPlayers(nbPlayers - 1);
+ lobby.startGame();
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/api/TableTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/api/TableTest.java
new file mode 100644
index 00000000..9ed0af02
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/api/TableTest.java
@@ -0,0 +1,49 @@
+package org.luxons.sevenwonders.game.api;
+
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+@RunWith(Theories.class)
+public class TableTest {
+
+ @DataPoints
+ public static int[] nbPlayers() {
+ return new int[] {2, 3, 4, 5, 6, 7, 8};
+ }
+
+ @Theory
+ public void getBoard_wrapLeft(int nbPlayers) {
+ assumeTrue(nbPlayers >= 2);
+ Table table = TestUtils.createTable(nbPlayers);
+ int last = nbPlayers - 1;
+ assertEquals(table.getBoard(last), table.getBoard(0, RelativeBoardPosition.LEFT));
+ assertEquals(table.getBoard(0), table.getBoard(0, RelativeBoardPosition.SELF));
+ assertEquals(table.getBoard(1), table.getBoard(0, RelativeBoardPosition.RIGHT));
+ }
+
+ @Theory
+ public void getBoard_wrapRight(int nbPlayers) {
+ assumeTrue(nbPlayers >= 2);
+ Table table = TestUtils.createTable(nbPlayers);
+ int last = nbPlayers - 1;
+ assertEquals(table.getBoard(last - 1), table.getBoard(last, RelativeBoardPosition.LEFT));
+ assertEquals(table.getBoard(last), table.getBoard(last, RelativeBoardPosition.SELF));
+ assertEquals(table.getBoard(0), table.getBoard(last, RelativeBoardPosition.RIGHT));
+ }
+
+ @Theory
+ public void getBoard_noWrap(int nbPlayers) {
+ assumeTrue(nbPlayers >= 3);
+ Table table = TestUtils.createTable(nbPlayers);
+ assertEquals(table.getBoard(0), table.getBoard(1, RelativeBoardPosition.LEFT));
+ assertEquals(table.getBoard(1), table.getBoard(1, RelativeBoardPosition.SELF));
+ assertEquals(table.getBoard(2), table.getBoard(1, RelativeBoardPosition.RIGHT));
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/boards/BoardTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/boards/BoardTest.java
new file mode 100644
index 00000000..f9117146
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/boards/BoardTest.java
@@ -0,0 +1,107 @@
+package org.luxons.sevenwonders.game.boards;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.Rule;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.CustomizableSettings;
+import org.luxons.sevenwonders.game.boards.Board.InsufficientFundsException;
+import org.luxons.sevenwonders.game.cards.Color;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.resources.Resources;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.*;
+
+@RunWith(Theories.class)
+public class BoardTest {
+
+ @DataPoints("gold")
+ public static int[] goldAmounts() {
+ return new int[]{-3, -1, 0, 1, 2, 3};
+ }
+
+ @DataPoints("nbCards")
+ public static int[] nbCards() {
+ return new int[] {0, 1, 2};
+ }
+
+ @DataPoints
+ public static ResourceType[] resourceTypes() {
+ return ResourceType.values();
+ }
+
+ @DataPoints
+ public static Color[] colors() {
+ return Color.values();
+ }
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Theory
+ public void initialGold_respectsSettings(@FromDataPoints("gold") int goldAmountInSettings) {
+ CustomizableSettings customSettings = new CustomizableSettings();
+ customSettings.setInitialGold(goldAmountInSettings);
+ Settings settings = new Settings(5, customSettings);
+ Board board = new Board(TestUtils.createWonder(), null, settings);
+ assertEquals(goldAmountInSettings, board.getGold());
+ }
+
+ @Theory
+ public void initialProduction_containsInitialResource(ResourceType type) {
+ Board board = new Board(TestUtils.createWonder(type), null, new Settings(5));
+ Resources resources = TestUtils.createResources(type);
+ assertTrue(board.getProduction().contains(resources));
+ }
+
+ @Theory
+ public void removeGold_successfulWhenNotTooMuch(@FromDataPoints("gold") int initialGold,
+ @FromDataPoints("gold") int goldRemoved) {
+ assumeTrue(goldRemoved >= 0);
+ assumeTrue(initialGold >= goldRemoved);
+ Board board = new Board(TestUtils.createWonder(), null, new Settings(5));
+ board.setGold(initialGold);
+ board.removeGold(goldRemoved);
+ assertEquals(initialGold - goldRemoved, board.getGold());
+ }
+
+ @Theory
+ public void removeGold_failsWhenTooMuch(@FromDataPoints("gold") int initialGold,
+ @FromDataPoints("gold") int goldRemoved) {
+ assumeTrue(goldRemoved >= 0);
+ assumeTrue(initialGold < goldRemoved);
+ thrown.expect(InsufficientFundsException.class);
+ Board board = new Board(TestUtils.createWonder(), null, new Settings(5));
+ board.setGold(initialGold);
+ board.removeGold(goldRemoved);
+ }
+
+ @Theory
+ public void getNbCardsOfColor_properCount_singleColor(ResourceType type, @FromDataPoints("nbCards") int nbCards,
+ @FromDataPoints("nbCards") int nbOtherCards, Color color) {
+ Board board = new Board(TestUtils.createWonder(type), null, new Settings(5));
+ TestUtils.addCards(board, nbCards, nbOtherCards, color);
+ assertEquals(nbCards, board.getNbCardsOfColor(Collections.singletonList(color)));
+ }
+
+ @Theory
+ public void getNbCardsOfColor_properCount_multiColors(ResourceType type, @FromDataPoints("nbCards") int nbCards1,
+ @FromDataPoints("nbCards") int nbCards2, @FromDataPoints("nbCards") int nbOtherCards, Color color1,
+ Color color2) {
+ Board board = new Board(TestUtils.createWonder(type), null, new Settings(5));
+ TestUtils.addCards(board, nbCards1, color1);
+ TestUtils.addCards(board, nbCards2, color2);
+ TestUtils.addCards(board, nbOtherCards, TestUtils.getDifferentColorFrom(color1, color2));
+ assertEquals(nbCards1 + nbCards2, board.getNbCardsOfColor(Arrays.asList(color1, color2)));
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/boards/MilitaryTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/boards/MilitaryTest.java
new file mode 100644
index 00000000..7ef253db
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/boards/MilitaryTest.java
@@ -0,0 +1,72 @@
+package org.luxons.sevenwonders.game.boards;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Rule;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.CustomizableSettings;
+import org.luxons.sevenwonders.game.boards.Military.UnknownAgeException;
+
+import static org.junit.Assert.*;
+
+@RunWith(Theories.class)
+public class MilitaryTest {
+
+ @DataPoints("points")
+ public static int[] points() {
+ return new int[] {0, 1, 3, 5};
+ }
+
+ @DataPoints("ages")
+ public static int[] ages() {
+ return new int[] {1, 2, 3};
+ }
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private static Military createMilitary(int age, int nbPointsPerVictory, int nbPointsPerDefeat) {
+ Map<Integer, Integer> wonPointsPerAge = new HashMap<>();
+ wonPointsPerAge.put(age, nbPointsPerVictory);
+
+ CustomizableSettings customSettings = new CustomizableSettings();
+ customSettings.setWonPointsPerVictoryPerAge(wonPointsPerAge);
+ customSettings.setLostPointsPerDefeat(nbPointsPerDefeat);
+
+ Settings settings = new Settings(5, customSettings);
+ return new Military(settings);
+ }
+
+ @Theory
+ public void victory_addsCorrectPoints(@FromDataPoints("ages") int age, @FromDataPoints("points") int
+ nbPointsPerVictory) {
+ Military military = createMilitary(age, nbPointsPerVictory, 0);
+ int initialPoints = military.getTotalPoints();
+
+ military.victory(age);
+ assertEquals(initialPoints + nbPointsPerVictory, military.getTotalPoints());
+ }
+
+ @Theory
+ public void victory_failsIfUnknownAge(@FromDataPoints("points") int nbPointsPerVictory) {
+ Military military = createMilitary(0, nbPointsPerVictory, 0);
+ thrown.expect(UnknownAgeException.class);
+ military.victory(1);
+ }
+
+ @Theory
+ public void defeat_removesCorrectPoints(@FromDataPoints("points") int nbPointsLostPerDefeat) {
+ Military military = createMilitary(0, 0, nbPointsLostPerDefeat);
+ int initialPoints = military.getTotalPoints();
+
+ military.defeat();
+ assertEquals(initialPoints - nbPointsLostPerDefeat, military.getTotalPoints());
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/boards/RelativeBoardPositionTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/boards/RelativeBoardPositionTest.java
new file mode 100644
index 00000000..e95a1e37
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/boards/RelativeBoardPositionTest.java
@@ -0,0 +1,44 @@
+package org.luxons.sevenwonders.game.boards;
+
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+@RunWith(Theories.class)
+public class RelativeBoardPositionTest {
+
+ @DataPoints
+ public static int[] nbPlayers() {
+ return new int[] {1, 2, 3, 5, 7, 9};
+ }
+
+ @Theory
+ public void getIndexFrom_wrapLeft(int nbPlayers) {
+ assumeTrue(nbPlayers >= 2);
+ int last = nbPlayers - 1;
+ assertEquals(last, RelativeBoardPosition.LEFT.getIndexFrom(0, nbPlayers));
+ assertEquals(0, RelativeBoardPosition.SELF.getIndexFrom(0, nbPlayers));
+ assertEquals(1, RelativeBoardPosition.RIGHT.getIndexFrom(0, nbPlayers));
+ }
+
+ @Theory
+ public void getIndexFrom_wrapRight(int nbPlayers) {
+ assumeTrue(nbPlayers >= 2);
+ int last = nbPlayers - 1;
+ assertEquals(last - 1, RelativeBoardPosition.LEFT.getIndexFrom(last, nbPlayers));
+ assertEquals(last, RelativeBoardPosition.SELF.getIndexFrom(last, nbPlayers));
+ assertEquals(0, RelativeBoardPosition.RIGHT.getIndexFrom(last, nbPlayers));
+ }
+
+ @Theory
+ public void getIndexFrom_noWrap(int nbPlayers) {
+ assumeTrue(nbPlayers >= 3);
+ assertEquals(0, RelativeBoardPosition.LEFT.getIndexFrom(1, nbPlayers));
+ assertEquals(1, RelativeBoardPosition.SELF.getIndexFrom(1, nbPlayers));
+ assertEquals(2, RelativeBoardPosition.RIGHT.getIndexFrom(1, nbPlayers));
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/boards/ScienceTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/boards/ScienceTest.java
new file mode 100644
index 00000000..067a7eff
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/boards/ScienceTest.java
@@ -0,0 +1,113 @@
+package org.luxons.sevenwonders.game.boards;
+
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(Theories.class)
+public class ScienceTest {
+
+ @DataPoints
+ public static int[][] quantitiesWithExpectedPoints() {
+ // compasses, wheels, tablets, jokers, expected points
+ return new int[][]{
+ {0, 0, 0, 1, 1},
+ {0, 0, 1, 0, 1},
+ {0, 0, 0, 2, 4},
+ {0, 0, 1, 1, 4},
+ {0, 0, 2, 0, 4},
+ {0, 0, 0, 3, 10},
+ {0, 0, 1, 2, 10},
+ {0, 1, 1, 1, 10},
+ {1, 1, 1, 0, 10},
+ {0, 0, 0, 4, 16},
+ {0, 0, 1, 3, 16},
+ {0, 0, 2, 2, 16},
+ {0, 0, 3, 1, 16},
+ {0, 0, 4, 0, 16}};
+ }
+
+ @DataPoints
+ public static int[] quantitiesDataPoints() {
+ return new int[] {0, 1, 3, 5, 8};
+ }
+
+ @Test
+ public void addAll_empty() {
+ Science initial = TestUtils.createScience(3, 4, 5, 1);
+ Science empty = new Science();
+ initial.addAll(empty);
+ assertEquals(3, initial.getQuantity(ScienceType.COMPASS));
+ assertEquals(4, initial.getQuantity(ScienceType.WHEEL));
+ assertEquals(5, initial.getQuantity(ScienceType.TABLET));
+ assertEquals(1, initial.getJokers());
+ }
+
+ @Test
+ public void addAll_noJoker() {
+ Science initial = TestUtils.createScience(3, 4, 5, 1);
+ Science other = TestUtils.createScience(1, 2, 3, 0);
+ initial.addAll(other);
+ assertEquals(4, initial.getQuantity(ScienceType.COMPASS));
+ assertEquals(6, initial.getQuantity(ScienceType.WHEEL));
+ assertEquals(8, initial.getQuantity(ScienceType.TABLET));
+ assertEquals(1, initial.getJokers());
+ }
+
+ @Test
+ public void addAll_withJokers() {
+ Science initial = TestUtils.createScience(3, 4, 5, 1);
+ Science other = TestUtils.createScience(0, 0, 0, 3);
+ initial.addAll(other);
+ assertEquals(3, initial.getQuantity(ScienceType.COMPASS));
+ assertEquals(4, initial.getQuantity(ScienceType.WHEEL));
+ assertEquals(5, initial.getQuantity(ScienceType.TABLET));
+ assertEquals(4, initial.getJokers());
+ }
+
+ @Test
+ public void addAll_mixed() {
+ Science initial = TestUtils.createScience(3, 4, 5, 1);
+ Science other = TestUtils.createScience(1, 2, 3, 4);
+ initial.addAll(other);
+ assertEquals(4, initial.getQuantity(ScienceType.COMPASS));
+ assertEquals(6, initial.getQuantity(ScienceType.WHEEL));
+ assertEquals(8, initial.getQuantity(ScienceType.TABLET));
+ assertEquals(5, initial.getJokers());
+ }
+
+ @Theory
+ public void computePoints_compassesOnly_noJoker(int compasses) {
+ Science science = TestUtils.createScience(compasses, 0, 0, 0);
+ assertEquals(compasses * compasses, science.computePoints());
+ }
+
+ @Theory
+ public void computePoints_wheelsOnly_noJoker(int wheels) {
+ Science science = TestUtils.createScience(0, wheels, 0, 0);
+ assertEquals(wheels * wheels, science.computePoints());
+ }
+
+ @Theory
+ public void computePoints_tabletsOnly_noJoker(int tablets) {
+ Science science = TestUtils.createScience(0, 0, tablets, 0);
+ assertEquals(tablets * tablets, science.computePoints());
+ }
+
+ @Theory
+ public void computePoints_allSameNoJoker(int eachSymbol) {
+ Science science = TestUtils.createScience(eachSymbol, eachSymbol, eachSymbol, 0);
+ assertEquals(3 * eachSymbol * eachSymbol + 7 * eachSymbol, science.computePoints());
+ }
+
+ @Theory
+ public void computePoints_expectation(int[] expectation) {
+ Science science = TestUtils.createScience(expectation[0], expectation[1], expectation[2], expectation[3]);
+ assertEquals(expectation[4], science.computePoints());
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/cards/CardBackTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/cards/CardBackTest.java
new file mode 100644
index 00000000..d105c33f
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/cards/CardBackTest.java
@@ -0,0 +1,15 @@
+package org.luxons.sevenwonders.game.cards;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class CardBackTest {
+
+ @Test
+ public void initializedWithImage() throws Exception {
+ String imagePath = "whateverimage.png";
+ CardBack back = new CardBack(imagePath);
+ assertEquals(imagePath, back.getImage());
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/cards/CardTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/cards/CardTest.java
new file mode 100644
index 00000000..4a481442
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/cards/CardTest.java
@@ -0,0 +1,106 @@
+package org.luxons.sevenwonders.game.cards;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.effects.Effect;
+import org.luxons.sevenwonders.game.effects.ProductionIncrease;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.wonders.Wonder;
+
+import static org.junit.Assert.*;
+import static org.luxons.sevenwonders.game.test.TestUtils.*;
+
+public class CardTest {
+
+ private Table table;
+
+ private Card treeFarmCard;
+
+ @Before
+ public void initBoard() {
+ Settings settings = new Settings(5);
+
+ List<Board> boards = new ArrayList<>(3);
+ boards.add(new Board(new Wonder("TestWonder", ResourceType.WOOD), null, settings));
+ boards.add(new Board(new Wonder("TestWonder", ResourceType.STONE), null, settings));
+ boards.add(new Board(new Wonder("TestWonder", ResourceType.PAPYRUS), null, settings));
+ table = new Table(boards);
+
+ Requirements treeFarmRequirements = new Requirements();
+ treeFarmRequirements.setGold(1);
+
+ ProductionIncrease treeFarmEffect = new ProductionIncrease();
+ treeFarmEffect.getProduction().addChoice(ResourceType.WOOD, ResourceType.CLAY);
+
+ List<Effect> effects = Collections.singletonList(treeFarmEffect);
+
+ treeFarmCard = new Card("Tree Farm", Color.BROWN, treeFarmRequirements, effects, "", null, null);
+ }
+
+ @Test
+ public void playCardCostingMoney() {
+ table.getBoard(0).setGold(3);
+ table.getBoard(1).setGold(3);
+ table.getBoard(2).setGold(3);
+ treeFarmCard.applyTo(table, 0, new ArrayList<>());
+ assertEquals(2, table.getBoard(0).getGold());
+ assertEquals(3, table.getBoard(1).getGold());
+ assertEquals(3, table.getBoard(2).getGold());
+ }
+
+ @Test
+ public void equals_falseWhenNull() {
+ Card card = createCard("TestCard");
+ //noinspection ObjectEqualsNull
+ assertFalse(card.equals(null));
+ }
+
+ @Test
+ public void equals_falseWhenDifferentClass() {
+ Card card = createCard("TestCard");
+ Object object = new Object();
+ //noinspection EqualsBetweenInconvertibleTypes
+ assertFalse(card.equals(object));
+ }
+
+ @Test
+ public void equals_trueWhenSame() {
+ Card card = createCard("TestCard");
+ assertEquals(card, card);
+ }
+
+ @Test
+ public void equals_trueWhenSameContent() {
+ Card card1 = createCard("TestCard");
+ Card card2 = createCard("TestCard");
+ assertTrue(card1.equals(card2));
+ }
+
+ @Test
+ public void equals_falseWhenDifferentName() {
+ Card card1 = createCard("TestCard1");
+ Card card2 = createCard("TestCard2");
+ assertFalse(card1.equals(card2));
+ }
+
+ @Test
+ public void hashCode_sameWhenSameContent() {
+ Card card1 = createCard("TestCard");
+ Card card2 = createCard("TestCard");
+ assertEquals(card1.hashCode(), card2.hashCode());
+ }
+
+ @Test
+ public void hashCode_differentWhenDifferentName() {
+ Card card1 = createCard("TestCard1");
+ Card card2 = createCard("TestCard2");
+ assertNotEquals(card1.hashCode(), card2.hashCode());
+ }
+}
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/cards/DecksTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/cards/DecksTest.java
new file mode 100644
index 00000000..06060f16
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/cards/DecksTest.java
@@ -0,0 +1,110 @@
+package org.luxons.sevenwonders.game.cards;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.cards.Decks.CardNotFoundException;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+@RunWith(Theories.class)
+public class DecksTest {
+
+ @DataPoints
+ public static int[] dataPoints() {
+ return new int[] {1, 2, 3, 5, 10};
+ }
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private static Decks createDecks(int nbAges, int nbCardsPerAge) {
+ Map<Integer, List<Card>> cardsPerAge = new HashMap<>();
+ for (int age = 1; age <= nbAges; age++) {
+ int firstCardNumber = (age - 1) * nbCardsPerAge;
+ cardsPerAge.put(age, TestUtils.createSampleCards(firstCardNumber, nbCardsPerAge));
+ }
+ return new Decks(cardsPerAge);
+ }
+
+ @Test(expected = CardNotFoundException.class)
+ public void getCard_failsOnNullNameWhenDeckIsEmpty() {
+ Decks decks = createDecks(0, 0);
+ decks.getCard(null);
+ }
+
+ @Test(expected = CardNotFoundException.class)
+ public void getCard_failsOnEmptyNameWhenDeckIsEmpty() {
+ Decks decks = createDecks(0, 0);
+ decks.getCard("");
+ }
+
+ @Test(expected = CardNotFoundException.class)
+ public void getCard_failsWhenDeckIsEmpty() {
+ Decks decks = createDecks(0, 0);
+ decks.getCard("Any name");
+ }
+
+ @Test(expected = CardNotFoundException.class)
+ public void getCard_failsWhenCardIsNotFound() {
+ Decks decks = createDecks(3, 20);
+ decks.getCard("Unknown name");
+ }
+
+ @Test
+ public void getCard_succeedsWhenCardIsFound() {
+ Decks decks = createDecks(3, 20);
+ Card card = decks.getCard("Test Card 3");
+ assertEquals("Test Card 3", card.getName());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void deal_failsOnZeroPlayers() {
+ Decks decks = createDecks(3, 20);
+ decks.deal(1, 0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void deal_failsOnMissingAge() {
+ Decks decks = createDecks(2, 0);
+ decks.deal(4, 10);
+ }
+
+ @Theory
+ public void deal_failsWhenTooFewPlayers(int nbPlayers, int nbCards) {
+ assumeTrue(nbCards % nbPlayers != 0);
+ thrown.expect(IllegalArgumentException.class);
+ Decks decks = createDecks(1, nbCards);
+ decks.deal(1, nbPlayers);
+ }
+
+ @Theory
+ public void deal_succeedsOnZeroCards(int nbPlayers) {
+ Decks decks = createDecks(1, 0);
+ Hands hands = decks.deal(1, nbPlayers);
+ for (int i = 0; i < nbPlayers; i++) {
+ assertNotNull(hands.get(i));
+ assertTrue(hands.get(i).isEmpty());
+ }
+ }
+
+ @Theory
+ public void deal_evenDistribution(int nbPlayers, int nbCardsPerPlayer) {
+ int nbCardsPerAge = nbPlayers * nbCardsPerPlayer;
+ Decks decks = createDecks(1, nbCardsPerAge);
+ Hands hands = decks.deal(1, nbPlayers);
+ for (int i = 0; i < nbPlayers; i++) {
+ assertEquals(nbCardsPerPlayer, hands.get(i).size());
+ }
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/cards/HandRotationDirectionTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/cards/HandRotationDirectionTest.java
new file mode 100644
index 00000000..6165d158
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/cards/HandRotationDirectionTest.java
@@ -0,0 +1,15 @@
+package org.luxons.sevenwonders.game.cards;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class HandRotationDirectionTest {
+
+ @Test
+ public void testAgesDirections() throws Exception {
+ assertEquals(HandRotationDirection.LEFT, HandRotationDirection.forAge(1));
+ assertEquals(HandRotationDirection.RIGHT, HandRotationDirection.forAge(2));
+ assertEquals(HandRotationDirection.LEFT, HandRotationDirection.forAge(3));
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/cards/HandsTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/cards/HandsTest.java
new file mode 100644
index 00000000..494b9e4c
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/cards/HandsTest.java
@@ -0,0 +1,141 @@
+package org.luxons.sevenwonders.game.cards;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.api.HandCard;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.cards.Hands.PlayerIndexOutOfBoundsException;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+@RunWith(Theories.class)
+public class HandsTest {
+
+ @DataPoints("nbCardsPerPlayer")
+ public static int[] nbCardsPerPlayer() {
+ return new int[] {0, 1, 2, 3, 4, 5, 6, 7};
+ }
+
+ @DataPoints("nbPlayers")
+ public static int[] nbPlayers() {
+ return new int[] {3, 4, 5, 6, 7};
+ }
+
+ private static Hands createHands(int nbPlayers, int nbCardsPerPlayer) {
+ Map<Integer, List<Card>> hands = new HashMap<>();
+ for (int p = 0; p < nbPlayers; p++) {
+ int firstCardNumber = (p - 1) * nbCardsPerPlayer;
+ hands.put(p, TestUtils.createSampleCards(firstCardNumber, nbCardsPerPlayer));
+ }
+ return new Hands(hands, nbPlayers);
+ }
+
+ @Test(expected = PlayerIndexOutOfBoundsException.class)
+ public void get_failsOnMissingPlayer() {
+ Hands hands = createHands(4, 7);
+ hands.get(5);
+ }
+
+ @Test
+ public void get_retrievesCorrectCards() {
+ List<Card> hand0 = TestUtils.createSampleCards(0, 5);
+ List<Card> hand1 = TestUtils.createSampleCards(5, 10);
+ Map<Integer, List<Card>> handsMap = new HashMap<>();
+ handsMap.put(0, hand0);
+ handsMap.put(1, hand1);
+ Hands hands = new Hands(handsMap, 2);
+ assertEquals(hand0, hands.get(0));
+ assertEquals(hand1, hands.get(1));
+ }
+
+ @Theory
+ public void isEmpty_falseWhenAtLeast1_allSame(@FromDataPoints("nbPlayers") int nbPlayers,
+ @FromDataPoints("nbCardsPerPlayer") int nbCardsPerPlayer) {
+ assumeTrue(nbCardsPerPlayer >= 1);
+ Hands hands = createHands(nbPlayers, nbCardsPerPlayer);
+ assertFalse(hands.isEmpty());
+ }
+
+ @Theory
+ public void isEmpty_trueWhenAllEmpty(@FromDataPoints("nbPlayers") int nbPlayers) {
+ Hands hands = createHands(nbPlayers, 0);
+ assertTrue(hands.isEmpty());
+ }
+
+ @Theory
+ public void maxOneCardRemains_falseWhenAtLeast2_allSame(@FromDataPoints("nbPlayers") int nbPlayers,
+ @FromDataPoints("nbCardsPerPlayer") int nbCardsPerPlayer) {
+ assumeTrue(nbCardsPerPlayer >= 2);
+ Hands hands = createHands(nbPlayers, nbCardsPerPlayer);
+ assertFalse(hands.maxOneCardRemains());
+ }
+
+ @Theory
+ public void maxOneCardRemains_trueWhenAtMost1_allSame(@FromDataPoints("nbPlayers") int nbPlayers,
+ @FromDataPoints("nbCardsPerPlayer") int nbCardsPerPlayer) {
+ assumeTrue(nbCardsPerPlayer <= 1);
+ Hands hands = createHands(nbPlayers, nbCardsPerPlayer);
+ assertTrue(hands.maxOneCardRemains());
+ }
+
+ @Theory
+ public void maxOneCardRemains_trueWhenAtMost1_someZero(@FromDataPoints("nbPlayers") int nbPlayers) {
+ Hands hands = createHands(nbPlayers, 1);
+ hands.get(0).remove(0);
+ assertTrue(hands.maxOneCardRemains());
+ }
+
+ @Theory
+ public void gatherAndClear(@FromDataPoints("nbPlayers") int nbPlayers,
+ @FromDataPoints("nbCardsPerPlayer") int nbCardsPerPlayer) {
+ Hands hands = createHands(nbPlayers, nbCardsPerPlayer);
+ List<Card> remainingCards = hands.gatherAndClear();
+ assertEquals(nbPlayers * nbCardsPerPlayer, remainingCards.size());
+ assertTrue(hands.isEmpty());
+ }
+
+ @Test
+ public void rotate_movesOfCorrectOffset_right() {
+ Hands hands = createHands(3, 7);
+ Hands rotated = hands.rotate(HandRotationDirection.RIGHT);
+ assertEquals(rotated.get(1), hands.get(0));
+ assertEquals(rotated.get(2), hands.get(1));
+ assertEquals(rotated.get(0), hands.get(2));
+ }
+
+ @Test
+ public void rotate_movesOfCorrectOffset_left() {
+ Hands hands = createHands(3, 7);
+ Hands rotated = hands.rotate(HandRotationDirection.LEFT);
+ assertEquals(rotated.get(2), hands.get(0));
+ assertEquals(rotated.get(0), hands.get(1));
+ assertEquals(rotated.get(1), hands.get(2));
+ }
+
+ @Test
+ public void createHand_containsAllCards() {
+ List<Card> hand0 = TestUtils.createSampleCards(0, 5);
+ List<Card> hand1 = TestUtils.createSampleCards(5, 10);
+ Map<Integer, List<Card>> handsMap = new HashMap<>();
+ handsMap.put(0, hand0);
+ handsMap.put(1, hand1);
+ Hands hands = new Hands(handsMap, 2);
+
+ Table table = TestUtils.createTable(2);
+ List<HandCard> hand = hands.createHand(table, 0);
+
+ for (HandCard handCard : hand) {
+ assertTrue(hand0.contains(handCard.getCard()));
+ }
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/data/GameDefinitionLoaderTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/data/GameDefinitionLoaderTest.java
new file mode 100644
index 00000000..b38afd49
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/data/GameDefinitionLoaderTest.java
@@ -0,0 +1,16 @@
+package org.luxons.sevenwonders.game.data;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class GameDefinitionLoaderTest {
+
+ @Test
+ public void successfulLoad() throws Exception {
+ GameDefinitionLoader loader = new GameDefinitionLoader();
+ GameDefinition gameDefinition = loader.getGameDefinition();
+ assertNotNull(gameDefinition);
+ }
+
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/data/GameDefinitionTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/data/GameDefinitionTest.java
new file mode 100644
index 00000000..5acc09df
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/data/GameDefinitionTest.java
@@ -0,0 +1,25 @@
+package org.luxons.sevenwonders.game.data;
+
+import java.util.List;
+
+import org.junit.Test;
+import org.luxons.sevenwonders.game.Game;
+import org.luxons.sevenwonders.game.Player;
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.CustomizableSettings;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.*;
+
+public class GameDefinitionTest {
+
+ @Test
+ public void successfulGameInit() throws Exception {
+ GameDefinition gameDefinition = new GameDefinitionLoader().getGameDefinition();
+ assertNotNull(gameDefinition);
+
+ List<Player> players = TestUtils.createPlayers(7);
+ Game game = gameDefinition.initGame(0, new CustomizableSettings(), players);
+ assertNotNull(game);
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethodTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethodTest.java
new file mode 100644
index 00000000..2544ca64
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/data/definitions/WonderSidePickMethodTest.java
@@ -0,0 +1,96 @@
+package org.luxons.sevenwonders.game.data.definitions;
+
+import java.util.Random;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(Theories.class)
+public class WonderSidePickMethodTest {
+
+ @DataPoints
+ public static WonderSide[] sides() {
+ return WonderSide.values();
+ }
+
+ private Random random;
+
+ private Random random2;
+
+ @Before
+ public void setUp() {
+ random = new Random(123); // starts with TRUE
+ random2 = new Random(123456); // starts with FALSE
+ }
+
+ @Test
+ public void pick_allA() {
+ WonderSide side = null;
+ for (int i = 0; i < 10; i++) {
+ side = WonderSidePickMethod.ALL_A.pickSide(random, side);
+ assertEquals(WonderSide.A, side);
+ }
+ }
+
+ @Test
+ public void pick_allB() {
+ WonderSide side = null;
+ for (int i = 0; i < 10; i++) {
+ side = WonderSidePickMethod.ALL_B.pickSide(random, side);
+ assertEquals(WonderSide.B, side);
+ }
+ }
+
+ @Test
+ public void pick_eachRandom() {
+ WonderSide side = WonderSidePickMethod.EACH_RANDOM.pickSide(random, null);
+ assertEquals(WonderSide.A, side);
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random, side);
+ assertEquals(WonderSide.B, side);
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random, side);
+ assertEquals(WonderSide.A, side);
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random, side);
+ assertEquals(WonderSide.B, side);
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random, side);
+ assertEquals(WonderSide.B, side);
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random, side);
+ assertEquals(WonderSide.A, side);
+ }
+
+ @Test
+ public void pick_eachRandom2() {
+ WonderSide side = WonderSidePickMethod.EACH_RANDOM.pickSide(random2, null);
+ assertEquals(WonderSide.B, side);
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random2, side);
+ assertEquals(WonderSide.A, side);
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random2, side);
+ assertEquals(WonderSide.A, side);
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random2, side);
+ assertEquals(WonderSide.B, side);
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random2, side);
+ assertEquals(WonderSide.B, side);
+ side = WonderSidePickMethod.EACH_RANDOM.pickSide(random2, side);
+ assertEquals(WonderSide.B, side);
+ }
+
+ @Theory
+ public void pick_allSameRandom_sameAsFirst(WonderSide firstSide) {
+ WonderSide side = firstSide;
+ for (int i = 0; i < 10; i++) {
+ side = WonderSidePickMethod.SAME_RANDOM_FOR_ALL.pickSide(random, side);
+ assertEquals(firstSide, side);
+ }
+ }
+
+ @Test
+ public void pick_allSameRandom_firstIsRandom() {
+ assertEquals(WonderSide.A, WonderSidePickMethod.SAME_RANDOM_FOR_ALL.pickSide(random, null));
+ assertEquals(WonderSide.B, WonderSidePickMethod.SAME_RANDOM_FOR_ALL.pickSide(random2, null));
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializerTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializerTest.java
new file mode 100644
index 00000000..753a26cf
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/NumericEffectSerializerTest.java
@@ -0,0 +1,128 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.effects.GoldIncrease;
+import org.luxons.sevenwonders.game.effects.MilitaryReinforcements;
+import org.luxons.sevenwonders.game.effects.ProductionIncrease;
+import org.luxons.sevenwonders.game.effects.RawPointsIncrease;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import static org.junit.Assert.*;
+
+@RunWith(Theories.class)
+public class NumericEffectSerializerTest {
+
+ @DataPoints
+ public static int[] dataPoints() {
+ return new int[] {-2, -1, 0, 1, 2, 5};
+ }
+
+ private Gson gson;
+
+ @Before
+ public void setUp() {
+ gson = new GsonBuilder().registerTypeAdapter(MilitaryReinforcements.class, new NumericEffectSerializer())
+ .registerTypeAdapter(RawPointsIncrease.class, new NumericEffectSerializer())
+ .registerTypeAdapter(GoldIncrease.class, new NumericEffectSerializer())
+ // ProductionIncrease is not a numeric effect, it is here for negative testing purpose
+ .registerTypeAdapter(ProductionIncrease.class, new NumericEffectSerializer())
+ .create();
+ }
+
+ @Test
+ public void serialize_militaryReinforcements_null() {
+ assertEquals("null", gson.toJson(null, MilitaryReinforcements.class));
+ }
+
+ @Test
+ public void serialize_rawPointsIncrease_null() {
+ assertEquals("null", gson.toJson(null, RawPointsIncrease.class));
+ }
+
+ @Test
+ public void serialize_goldIncrease_null() {
+ assertEquals("null", gson.toJson(null, GoldIncrease.class));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void serialize_failOnUnknownType() {
+ gson.toJson(new ProductionIncrease());
+ }
+
+ @Theory
+ public void serialize_militaryReinforcements(int count) {
+ MilitaryReinforcements reinforcements = new MilitaryReinforcements(count);
+ assertEquals(String.valueOf(count), gson.toJson(reinforcements));
+ }
+
+ @Theory
+ public void serialize_rawPointsIncrease(int count) {
+ RawPointsIncrease points = new RawPointsIncrease(count);
+ assertEquals(String.valueOf(count), gson.toJson(points));
+ }
+
+ @Theory
+ public void serialize_goldIncrease(int count) {
+ GoldIncrease goldIncrease = new GoldIncrease(count);
+ assertEquals(String.valueOf(count), gson.toJson(goldIncrease));
+ }
+
+ @Theory
+ public void deserialize_militaryReinforcements(int count) {
+ MilitaryReinforcements reinforcements = new MilitaryReinforcements(count);
+ assertEquals(reinforcements, gson.fromJson(String.valueOf(count), MilitaryReinforcements.class));
+ }
+
+ @Theory
+ public void deserialize_rawPointsIncrease(int count) {
+ RawPointsIncrease points = new RawPointsIncrease(count);
+ assertEquals(points, gson.fromJson(String.valueOf(count), RawPointsIncrease.class));
+ }
+
+ @Theory
+ public void deserialize_goldIncrease(int count) {
+ GoldIncrease goldIncrease = new GoldIncrease(count);
+ assertEquals(goldIncrease, gson.fromJson(String.valueOf(count), GoldIncrease.class));
+ }
+
+ @Test(expected = NumberFormatException.class)
+ public void deserialize_militaryReinforcements_failOnEmptyString() {
+ gson.fromJson("\"\"", MilitaryReinforcements.class);
+ }
+
+ @Test(expected = NumberFormatException.class)
+ public void deserialize_rawPointsIncrease_failOnEmptyString() {
+ gson.fromJson("\"\"", RawPointsIncrease.class);
+ }
+
+ @Test(expected = NumberFormatException.class)
+ public void deserialize_goldIncrease_failOnEmptyString() {
+ gson.fromJson("\"\"", GoldIncrease.class);
+ }
+
+ @Test(expected = NumberFormatException.class)
+ public void deserialize_militaryReinforcements_failOnNonNumericString() {
+ gson.fromJson("\"abc\"", MilitaryReinforcements.class);
+ }
+
+ @Test(expected = NumberFormatException.class)
+ public void deserialize_rawPointsIncrease_failOnNonNumericString() {
+ gson.fromJson("\"abc\"", RawPointsIncrease.class);
+ }
+
+ @Test(expected = NumberFormatException.class)
+ public void deserialize_goldIncrease_failOnNonNumericString() {
+ gson.fromJson("\"abc\"", GoldIncrease.class);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void deserialize_failOnUnknownType() {
+ gson.fromJson("\"2\"", ProductionIncrease.class);
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializerTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializerTest.java
new file mode 100644
index 00000000..17940361
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ProductionIncreaseSerializerTest.java
@@ -0,0 +1,203 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.luxons.sevenwonders.game.effects.ProductionIncrease;
+import org.luxons.sevenwonders.game.resources.Production;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.resources.Resources;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import static org.junit.Assert.*;
+
+public class ProductionIncreaseSerializerTest {
+
+ private Gson gson;
+
+ @Before
+ public void setUp() {
+ Type resourceTypeList = new TypeToken<List<ResourceType>>() {
+ }.getType();
+ gson = new GsonBuilder().registerTypeAdapter(Resources.class, new ResourcesSerializer())
+ .registerTypeAdapter(ResourceType.class, new ResourceTypeSerializer())
+ .registerTypeAdapter(resourceTypeList, new ResourceTypesSerializer())
+ .registerTypeAdapter(ProductionIncrease.class, new ProductionIncreaseSerializer())
+ .create();
+ }
+
+ private static ProductionIncrease create(int wood, int stone, int clay) {
+ Production production = new Production();
+ if (wood > 0) {
+ production.addFixedResource(ResourceType.WOOD, wood);
+ }
+ if (stone > 0) {
+ production.addFixedResource(ResourceType.STONE, stone);
+ }
+ if (clay > 0) {
+ production.addFixedResource(ResourceType.CLAY, clay);
+ }
+ ProductionIncrease prodIncrease = new ProductionIncrease();
+ prodIncrease.setProduction(production);
+ return prodIncrease;
+ }
+
+ private static ProductionIncrease createChoice(ResourceType... types) {
+ Production production = new Production();
+ production.addChoice(types);
+ ProductionIncrease prodIncrease = new ProductionIncrease();
+ prodIncrease.setProduction(production);
+ return prodIncrease;
+ }
+
+ @Test
+ public void serialize_nullAsNull() {
+ assertEquals("null", gson.toJson(null, ProductionIncrease.class));
+ }
+
+ @Test
+ public void serialize_emptyProdIncreaseAsNull() {
+ ProductionIncrease prodIncrease = new ProductionIncrease();
+ assertEquals("null", gson.toJson(prodIncrease, ProductionIncrease.class));
+ }
+
+ @Test
+ public void serialize_singleType() {
+ ProductionIncrease prodIncrease = create(1, 0, 0);
+ assertEquals("\"W\"", gson.toJson(prodIncrease, ProductionIncrease.class));
+ }
+
+ @Test
+ public void serialize_multipleTimesSameType() {
+ ProductionIncrease prodIncrease = create(3, 0, 0);
+ assertEquals("\"WWW\"", gson.toJson(prodIncrease, ProductionIncrease.class));
+ }
+
+ @Test
+ public void serialize_mixedTypes() {
+ ProductionIncrease prodIncrease = create(1, 1, 1);
+ assertEquals("\"WSC\"", gson.toJson(prodIncrease, ProductionIncrease.class));
+ }
+
+ @Test
+ public void serialize_mixedTypesMultiple() {
+ ProductionIncrease prodIncrease = create(2, 1, 2);
+ assertEquals("\"WWSCC\"", gson.toJson(prodIncrease, ProductionIncrease.class));
+ }
+
+ @Test
+ public void serialize_choice2() {
+ ProductionIncrease prodIncrease = createChoice(ResourceType.WOOD, ResourceType.CLAY);
+ assertEquals("\"W/C\"", gson.toJson(prodIncrease, ProductionIncrease.class));
+ }
+
+ @Test
+ public void serialize_choice3() {
+ ProductionIncrease prodIncrease = createChoice(ResourceType.WOOD, ResourceType.ORE, ResourceType.CLAY);
+ assertEquals("\"W/O/C\"", gson.toJson(prodIncrease, ProductionIncrease.class));
+ }
+
+ @Test
+ public void serialize_choice2_unordered() {
+ ProductionIncrease prodIncrease = createChoice(ResourceType.CLAY, ResourceType.WOOD);
+ assertEquals("\"W/C\"", gson.toJson(prodIncrease, ProductionIncrease.class));
+ }
+
+ @Test
+ public void serialize_choice3_unordered() {
+ ProductionIncrease prodIncrease = createChoice(ResourceType.WOOD, ResourceType.CLAY, ResourceType.ORE);
+ assertEquals("\"W/O/C\"", gson.toJson(prodIncrease, ProductionIncrease.class));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void serialize_failIfMultipleChoices() {
+ ProductionIncrease prodIncrease = createChoice(ResourceType.WOOD, ResourceType.CLAY);
+ prodIncrease.getProduction().addChoice(ResourceType.ORE, ResourceType.GLASS);
+ gson.toJson(prodIncrease, ProductionIncrease.class);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void serialize_failIfMixedFixedAndChoices() {
+ ProductionIncrease prodIncrease = create(1, 0, 0);
+ prodIncrease.getProduction().addChoice(ResourceType.WOOD, ResourceType.CLAY);
+ gson.toJson(prodIncrease, ProductionIncrease.class);
+ }
+
+ @Test
+ public void deserialize_nullFromNull() {
+ assertNull(gson.fromJson("null", ProductionIncrease.class));
+ }
+
+ @Test
+ public void deserialize_emptyList() {
+ ProductionIncrease prodIncrease = new ProductionIncrease();
+ assertEquals(prodIncrease, gson.fromJson("\"\"", ProductionIncrease.class));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void deserialize_failOnGarbageString() {
+ gson.fromJson("\"this is garbage\"", ProductionIncrease.class);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void deserialize_failOnGarbageStringWithSlashes() {
+ gson.fromJson("\"this/is/garbage\"", ProductionIncrease.class);
+ }
+
+ @Test
+ public void deserialize_singleType() {
+ ProductionIncrease prodIncrease = create(1, 0, 0);
+ assertEquals(prodIncrease, gson.fromJson("\"W\"", ProductionIncrease.class));
+ }
+
+ @Test
+ public void deserialize_multipleTimesSameType() {
+ ProductionIncrease prodIncrease = create(3, 0, 0);
+ assertEquals(prodIncrease, gson.fromJson("\"WWW\"", ProductionIncrease.class));
+ }
+
+ @Test
+ public void deserialize_mixedTypes() {
+ ProductionIncrease prodIncrease = create(1, 1, 1);
+ assertEquals(prodIncrease, gson.fromJson("\"WCS\"", ProductionIncrease.class));
+ }
+
+ @Test
+ public void deserialize_mixedTypes_unordered() {
+ ProductionIncrease prodIncrease = create(1, 3, 2);
+ assertEquals(prodIncrease, gson.fromJson("\"SCWCSS\"", ProductionIncrease.class));
+ }
+
+ @Test
+ public void deserialize_choice2() {
+ ProductionIncrease prodIncrease = createChoice(ResourceType.WOOD, ResourceType.CLAY);
+ assertEquals(prodIncrease, gson.fromJson("\"W/C\"", ProductionIncrease.class));
+ }
+
+ @Test
+ public void deserialize_choice3() {
+ ProductionIncrease prodIncrease = createChoice(ResourceType.WOOD, ResourceType.ORE, ResourceType.CLAY);
+ assertEquals(prodIncrease, gson.fromJson("\"W/O/C\"", ProductionIncrease.class));
+ }
+
+ @Test
+ public void deserialize_choice2_unordered() {
+ ProductionIncrease prodIncrease = createChoice(ResourceType.CLAY, ResourceType.WOOD);
+ assertEquals(prodIncrease, gson.fromJson("\"W/C\"", ProductionIncrease.class));
+ }
+
+ @Test
+ public void deserialize_choice3_unordered() {
+ ProductionIncrease prodIncrease = createChoice(ResourceType.WOOD, ResourceType.CLAY, ResourceType.ORE);
+ assertEquals(prodIncrease, gson.fromJson("\"W/O/C\"", ProductionIncrease.class));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void deserialize_failOnMultipleResourcesInChoice() {
+ gson.fromJson("\"W/SS/C\"", ProductionIncrease.class);
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializerTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializerTest.java
new file mode 100644
index 00000000..86f3f5ab
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypeSerializerTest.java
@@ -0,0 +1,50 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import static org.junit.Assert.*;
+
+public class ResourceTypeSerializerTest {
+
+ private Gson gson;
+
+ @Before
+ public void setUp() {
+ gson = new GsonBuilder().registerTypeAdapter(ResourceType.class, new ResourceTypeSerializer()).create();
+ }
+
+ @Test
+ public void serialize_useSymbolForEachType() {
+ for (ResourceType type : ResourceType.values()) {
+ String expectedJson = "\"" + type.getSymbol() + "\"";
+ assertEquals(expectedJson, gson.toJson(type));
+ }
+ }
+
+ @Test
+ public void deserialize_useSymbolForEachType() {
+ for (ResourceType type : ResourceType.values()) {
+ String typeInJson = "\"" + type.getSymbol() + "\"";
+ assertEquals(type, gson.fromJson(typeInJson, ResourceType.class));
+ }
+ }
+
+ @Test
+ public void deserialize_nullFromNull() {
+ assertNull(gson.fromJson("null", ResourceType.class));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void deserialize_failsOnEmptyString() {
+ gson.fromJson("\"\"", ResourceType.class);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void deserialize_failsOnGarbageString() {
+ gson.fromJson("\"thisisgarbage\"", ResourceType.class);
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializerTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializerTest.java
new file mode 100644
index 00000000..4ebbc33f
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ResourceTypesSerializerTest.java
@@ -0,0 +1,100 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import static org.junit.Assert.*;
+
+public class ResourceTypesSerializerTest {
+
+ private Gson gson;
+
+ @Before
+ public void setUp() {
+ gson = new GsonBuilder().registerTypeAdapter(createListTypeToken(), new ResourceTypesSerializer()).create();
+ }
+
+ private static Type createListTypeToken() {
+ return new TypeToken<List<ResourceType>>() {}.getType();
+ }
+
+ @Test
+ public void serialize_null() {
+ assertEquals("null", gson.toJson(null, createListTypeToken()));
+ }
+
+ @Test
+ public void serialize_emptyList() {
+ List<ResourceType> types = new ArrayList<>();
+ assertEquals("\"\"", gson.toJson(types, createListTypeToken()));
+ }
+
+ @Test
+ public void serialize_singleType() {
+ List<ResourceType> types = new ArrayList<>();
+ types.add(ResourceType.WOOD);
+ assertEquals("\"W\"", gson.toJson(types, createListTypeToken()));
+ }
+
+ @Test
+ public void serialize_multipleTimesSameType() {
+ List<ResourceType> types = new ArrayList<>();
+ types.add(ResourceType.WOOD);
+ types.add(ResourceType.WOOD);
+ types.add(ResourceType.WOOD);
+ assertEquals("\"WWW\"", gson.toJson(types, createListTypeToken()));
+ }
+
+ @Test
+ public void serialize_mixedTypes() {
+ List<ResourceType> types = new ArrayList<>();
+ types.add(ResourceType.WOOD);
+ types.add(ResourceType.CLAY);
+ types.add(ResourceType.STONE);
+ assertEquals("\"WCS\"", gson.toJson(types, createListTypeToken()));
+ }
+
+ @Test
+ public void deserialize_null() {
+ assertNull(gson.fromJson("null", createListTypeToken()));
+ }
+
+ @Test
+ public void deserialize_emptyList() {
+ List<ResourceType> types = new ArrayList<>();
+ assertEquals(types, gson.fromJson("\"\"", createListTypeToken()));
+ }
+
+ @Test
+ public void deserialize_singleType() {
+ List<ResourceType> types = new ArrayList<>();
+ types.add(ResourceType.WOOD);
+ assertEquals(types, gson.fromJson("\"W\"", createListTypeToken()));
+ }
+
+ @Test
+ public void deserialize_multipleTimesSameType() {
+ List<ResourceType> types = new ArrayList<>();
+ types.add(ResourceType.WOOD);
+ types.add(ResourceType.WOOD);
+ types.add(ResourceType.WOOD);
+ assertEquals(types, gson.fromJson("\"WWW\"", createListTypeToken()));
+ }
+
+ @Test
+ public void deserialize_mixedTypes() {
+ List<ResourceType> types = new ArrayList<>();
+ types.add(ResourceType.WOOD);
+ types.add(ResourceType.CLAY);
+ types.add(ResourceType.STONE);
+ assertEquals(types, gson.fromJson("\"WCS\"", createListTypeToken()));
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializerTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializerTest.java
new file mode 100644
index 00000000..1fd01337
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ResourcesSerializerTest.java
@@ -0,0 +1,107 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.resources.Resources;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import static org.junit.Assert.*;
+
+public class ResourcesSerializerTest {
+
+ private Gson gson;
+
+ @Before
+ public void setUp() {
+ gson = new GsonBuilder().registerTypeAdapter(Resources.class, new ResourcesSerializer()).create();
+ }
+
+ @Test
+ public void serialize_null() {
+ assertEquals("null", gson.toJson(null, Resources.class));
+ }
+
+ @Test
+ public void serialize_emptyResourcesToNull() {
+ Resources resources = new Resources();
+ assertEquals("null", gson.toJson(resources));
+ }
+
+ @Test
+ public void serialize_singleType() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.WOOD, 1);
+ assertEquals("\"W\"", gson.toJson(resources));
+ }
+
+ @Test
+ public void serialize_multipleTimesSameType() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.WOOD, 3);
+ assertEquals("\"WWW\"", gson.toJson(resources));
+ }
+
+ @Test
+ public void serialize_mixedTypes() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.WOOD, 1);
+ resources.add(ResourceType.STONE, 1);
+ resources.add(ResourceType.CLAY, 1);
+ assertEquals("\"WSC\"", gson.toJson(resources));
+ }
+
+ @Test
+ public void serialize_mixedTypes_unordered() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.CLAY, 1);
+ resources.add(ResourceType.WOOD, 2);
+ resources.add(ResourceType.CLAY, 1);
+ resources.add(ResourceType.STONE, 1);
+ assertEquals("\"WWSCC\"", gson.toJson(resources));
+ }
+
+ @Test
+ public void deserialize_null() {
+ assertNull(gson.fromJson("null", Resources.class));
+ }
+
+ @Test
+ public void deserialize_emptyList() {
+ Resources resources = new Resources();
+ assertEquals(resources, gson.fromJson("\"\"", Resources.class));
+ }
+
+ @Test
+ public void deserialize_singleType() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.WOOD, 1);
+ assertEquals(resources, gson.fromJson("\"W\"", Resources.class));
+ }
+
+ @Test
+ public void deserialize_multipleTimesSameType() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.WOOD, 3);
+ assertEquals(resources, gson.fromJson("\"WWW\"", Resources.class));
+ }
+
+ @Test
+ public void deserialize_mixedTypes() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.WOOD, 1);
+ resources.add(ResourceType.CLAY, 1);
+ resources.add(ResourceType.STONE, 1);
+ assertEquals(resources, gson.fromJson("\"WCS\"", Resources.class));
+ }
+
+ @Test
+ public void deserialize_mixedTypes_unordered() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.WOOD, 1);
+ resources.add(ResourceType.CLAY, 2);
+ resources.add(ResourceType.STONE, 3);
+ assertEquals(resources, gson.fromJson("\"SCWCSS\"", Resources.class));
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializerTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializerTest.java
new file mode 100644
index 00000000..40088fda
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/data/serializers/ScienceProgressSerializerTest.java
@@ -0,0 +1,145 @@
+package org.luxons.sevenwonders.game.data.serializers;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.luxons.sevenwonders.game.boards.ScienceType;
+import org.luxons.sevenwonders.game.effects.ScienceProgress;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import static org.junit.Assert.*;
+
+public class ScienceProgressSerializerTest {
+
+ private static final String COMPASS_STR = "\"COMPASS\"";
+
+ private static final String WHEEL_STR = "\"WHEEL\"";
+
+ private static final String TABLET_STR = "\"TABLET\"";
+
+ private static final String JOKER_STR = "\"any\"";
+
+ private Gson gson;
+
+ @Before
+ public void setUp() {
+ gson = new GsonBuilder().registerTypeAdapter(ScienceProgress.class, new ScienceProgressSerializer()).create();
+ }
+
+ @Test
+ public void serialize_emptyToNull() {
+ ScienceProgress progress = TestUtils.createScienceProgress(0, 0, 0, 0);
+ String json = gson.toJson(progress);
+ assertEquals("null", json);
+ }
+
+ @Test
+ public void serialize_oneCompass() {
+ ScienceProgress progress = TestUtils.createScienceProgress(1, 0, 0, 0);
+ String json = gson.toJson(progress);
+ assertEquals(COMPASS_STR, json);
+ }
+
+ @Test
+ public void serialize_oneWheel() {
+ ScienceProgress progress = TestUtils.createScienceProgress(0, 1, 0, 0);
+ String json = gson.toJson(progress);
+ assertEquals(WHEEL_STR, json);
+ }
+
+ @Test
+ public void serialize_oneTablet() {
+ ScienceProgress progress = TestUtils.createScienceProgress(0, 0, 1, 0);
+ String json = gson.toJson(progress);
+ assertEquals(TABLET_STR, json);
+ }
+
+ @Test
+ public void serialize_oneJoker() {
+ ScienceProgress progress = TestUtils.createScienceProgress(0, 0, 0, 1);
+ String json = gson.toJson(progress);
+ assertEquals(JOKER_STR, json);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void serialize_failOnMultipleCompasses() {
+ ScienceProgress progress = TestUtils.createScienceProgress(2, 0, 0, 0);
+ gson.toJson(progress);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void serialize_failOnMultipleWheels() {
+ ScienceProgress progress = TestUtils.createScienceProgress(0, 2, 0, 0);
+ gson.toJson(progress);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void serialize_failOnMultipleTablets() {
+ ScienceProgress progress = TestUtils.createScienceProgress(0, 0, 2, 0);
+ gson.toJson(progress);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void serialize_failOnMultipleJokers() {
+ ScienceProgress progress = TestUtils.createScienceProgress(0, 0, 0, 2);
+ gson.toJson(progress);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void serialize_failOnMixedElements() {
+ ScienceProgress progress = TestUtils.createScienceProgress(1, 1, 0, 0);
+ gson.toJson(progress);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void deserialize_failOnEmptyString() {
+ gson.fromJson("\"\"", ScienceProgress.class);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void deserialize_failOnGarbageString() {
+ gson.fromJson("thisisgarbage", ScienceProgress.class);
+ }
+
+ @Test
+ public void deserialize_compass() {
+ ScienceProgress progress = gson.fromJson(COMPASS_STR, ScienceProgress.class);
+ assertNotNull(progress.getScience());
+ assertEquals(1, progress.getScience().getQuantity(ScienceType.COMPASS));
+ assertEquals(0, progress.getScience().getQuantity(ScienceType.WHEEL));
+ assertEquals(0, progress.getScience().getQuantity(ScienceType.TABLET));
+ assertEquals(0, progress.getScience().getJokers());
+ }
+
+ @Test
+ public void deserialize_wheel() {
+ ScienceProgress progress = gson.fromJson(WHEEL_STR, ScienceProgress.class);
+ assertNotNull(progress.getScience());
+ assertEquals(0, progress.getScience().getQuantity(ScienceType.COMPASS));
+ assertEquals(1, progress.getScience().getQuantity(ScienceType.WHEEL));
+ assertEquals(0, progress.getScience().getQuantity(ScienceType.TABLET));
+ assertEquals(0, progress.getScience().getJokers());
+ }
+
+ @Test
+ public void deserialize_tablet() {
+ ScienceProgress progress = gson.fromJson(TABLET_STR, ScienceProgress.class);
+ assertNotNull(progress.getScience());
+ assertEquals(0, progress.getScience().getQuantity(ScienceType.COMPASS));
+ assertEquals(0, progress.getScience().getQuantity(ScienceType.WHEEL));
+ assertEquals(1, progress.getScience().getQuantity(ScienceType.TABLET));
+ assertEquals(0, progress.getScience().getJokers());
+ }
+
+ @Test
+ public void deserialize_joker() {
+ ScienceProgress progress = gson.fromJson(JOKER_STR, ScienceProgress.class);
+ assertNotNull(progress.getScience());
+ assertEquals(0, progress.getScience().getQuantity(ScienceType.COMPASS));
+ assertEquals(0, progress.getScience().getQuantity(ScienceType.WHEEL));
+ assertEquals(0, progress.getScience().getQuantity(ScienceType.TABLET));
+ assertEquals(1, progress.getScience().getJokers());
+ }
+
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/effects/BonusPerBoardElementTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/effects/BonusPerBoardElementTest.java
new file mode 100644
index 00000000..5f42bc53
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/effects/BonusPerBoardElementTest.java
@@ -0,0 +1,139 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.Before;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.boards.BoardElementType;
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition;
+import org.luxons.sevenwonders.game.cards.CardBack;
+import org.luxons.sevenwonders.game.cards.Color;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.*;
+
+@RunWith(Theories.class)
+public class BonusPerBoardElementTest {
+
+ @DataPoints
+ public static int[] values() {
+ return new int[] {0, 1, 2, 3};
+ }
+
+ @DataPoints
+ public static Color[] colors() {
+ return Color.values();
+ }
+
+ @DataPoints
+ public static RelativeBoardPosition[] positions() {
+ return RelativeBoardPosition.values();
+ }
+
+ private Table table;
+
+ @Before
+ public void setUp() throws Exception {
+ table = TestUtils.createTable(4);
+ }
+
+ private static BonusPerBoardElement createBonus(BoardElementType type, int gold, int points, Color... colors) {
+ BonusPerBoardElement bonus = new BonusPerBoardElement();
+ bonus.setType(type);
+ bonus.setGold(gold);
+ bonus.setPoints(points);
+ bonus.setColors(Arrays.asList(colors));
+ return bonus;
+ }
+
+ @Theory
+ public void computePoints_countsCards(RelativeBoardPosition boardPosition, int nbCards, int nbOtherCards,
+ int points, int gold, Color color) {
+ Board board = table.getBoard(0, boardPosition);
+ TestUtils.addCards(board, nbCards, nbOtherCards, color);
+
+ BonusPerBoardElement bonus = createBonus(BoardElementType.CARD, gold, points, color);
+ bonus.setBoards(Collections.singletonList(boardPosition));
+
+ assertEquals(nbCards * points, bonus.computePoints(table, 0));
+ }
+
+ @Theory
+ public void computePoints_countsDefeatTokens(RelativeBoardPosition boardPosition, int nbDefeatTokens, int points, int gold) {
+ Board board = table.getBoard(0, boardPosition);
+ for (int i = 0; i < nbDefeatTokens; i++) {
+ board.getMilitary().defeat();
+ }
+
+ BonusPerBoardElement bonus = createBonus(BoardElementType.DEFEAT_TOKEN, gold, points);
+ bonus.setBoards(Collections.singletonList(boardPosition));
+
+ assertEquals(nbDefeatTokens * points, bonus.computePoints(table, 0));
+ }
+
+ @Theory
+ public void computePoints_countsWonderStages(RelativeBoardPosition boardPosition, int nbStages, int points, int gold) {
+ Board board = table.getBoard(0, boardPosition);
+ for (int i = 0; i < nbStages; i++) {
+ board.getWonder().buildLevel(new CardBack(""));
+ }
+
+ BonusPerBoardElement bonus = createBonus(BoardElementType.BUILT_WONDER_STAGES, gold, points);
+ bonus.setBoards(Collections.singletonList(boardPosition));
+
+ assertEquals(nbStages * points, bonus.computePoints(table, 0));
+ }
+
+ @Theory
+ public void apply_countsCards(RelativeBoardPosition boardPosition, int nbCards, int nbOtherCards,
+ int points, int gold, Color color) {
+ Board board = table.getBoard(0, boardPosition);
+ TestUtils.addCards(board, nbCards, nbOtherCards, color);
+
+ BonusPerBoardElement bonus = createBonus(BoardElementType.CARD, gold, points, color);
+ bonus.setBoards(Collections.singletonList(boardPosition));
+
+ Board selfBoard = table.getBoard(0);
+ int initialGold = selfBoard.getGold();
+ bonus.apply(table, 0);
+ assertEquals(initialGold + nbCards * gold, selfBoard.getGold());
+ }
+
+ @Theory
+ public void apply_countsDefeatTokens(RelativeBoardPosition boardPosition, int nbDefeatTokens, int points, int gold) {
+ Board board = table.getBoard(0, boardPosition);
+ for (int i = 0; i < nbDefeatTokens; i++) {
+ board.getMilitary().defeat();
+ }
+
+ BonusPerBoardElement bonus = createBonus(BoardElementType.DEFEAT_TOKEN, gold, points);
+ bonus.setBoards(Collections.singletonList(boardPosition));
+
+ Board selfBoard = table.getBoard(0);
+ int initialGold = selfBoard.getGold();
+ bonus.apply(table, 0);
+ assertEquals(initialGold + nbDefeatTokens * gold, selfBoard.getGold());
+ }
+
+ @Theory
+ public void apply_countsWonderStages(RelativeBoardPosition boardPosition, int nbStages, int points, int gold) {
+ Board board = table.getBoard(0, boardPosition);
+ for (int i = 0; i < nbStages; i++) {
+ board.getWonder().buildLevel(new CardBack(""));
+ }
+
+ BonusPerBoardElement bonus = createBonus(BoardElementType.BUILT_WONDER_STAGES, gold, points);
+ bonus.setBoards(Collections.singletonList(boardPosition));
+
+ Board selfBoard = table.getBoard(0);
+ int initialGold = selfBoard.getGold();
+ bonus.apply(table, 0);
+ assertEquals(initialGold + nbStages * gold, selfBoard.getGold());
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/effects/DiscountTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/effects/DiscountTest.java
new file mode 100644
index 00000000..cf8ce21d
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/effects/DiscountTest.java
@@ -0,0 +1,72 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.junit.Assume;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.resources.BoughtResources;
+import org.luxons.sevenwonders.game.resources.Provider;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.assertEquals;
+
+@RunWith(Theories.class)
+public class DiscountTest {
+
+ @DataPoints
+ public static int[] discountedPrices() {
+ return new int[]{0, 1, 2};
+ }
+
+ @DataPoints
+ public static ResourceType[] resourceTypes() {
+ return ResourceType.values();
+ }
+
+ @DataPoints
+ public static Provider[] providers() {
+ return Provider.values();
+ }
+
+ @Theory
+ public void apply_givesDiscountedPrice(int discountedPrice, ResourceType discountedType, Provider provider) {
+ Board board = TestUtils.createBoard(ResourceType.CLAY, 3);
+ Discount discount = new Discount();
+ discount.setDiscountedPrice(discountedPrice);
+ discount.getProviders().add(provider);
+ discount.getResourceTypes().add(discountedType);
+ discount.apply(board);
+
+ BoughtResources boughtResources = TestUtils.createBoughtResources(provider, discountedType);
+ assertEquals(discountedPrice, board.getTradingRules().computeCost(boughtResources));
+ }
+
+ @Theory
+ public void apply_doesNotAffectOtherResources(int discountedPrice, ResourceType discountedType, Provider provider,
+ ResourceType otherType, Provider otherProvider) {
+ Assume.assumeTrue(otherProvider != provider);
+ Assume.assumeTrue(otherType != discountedType);
+
+ Board board = TestUtils.createBoard(ResourceType.CLAY, 3);
+ Discount discount = new Discount();
+ discount.setDiscountedPrice(discountedPrice);
+ discount.getProviders().add(provider);
+ discount.getResourceTypes().add(discountedType);
+ discount.apply(board);
+
+ // this is the default in the settings used by TestUtils.createBoard()
+ int normalPrice = 2;
+
+ BoughtResources fromOtherType = TestUtils.createBoughtResources(provider, otherType);
+ assertEquals(normalPrice, board.getTradingRules().computeCost(fromOtherType));
+
+ BoughtResources fromOtherProvider = TestUtils.createBoughtResources(otherProvider, discountedType);
+ assertEquals(normalPrice, board.getTradingRules().computeCost(fromOtherProvider));
+
+ BoughtResources fromOtherProviderAndType = TestUtils.createBoughtResources(otherProvider, otherType);
+ assertEquals(normalPrice, board.getTradingRules().computeCost(fromOtherProviderAndType));
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/effects/GoldIncreaseTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/effects/GoldIncreaseTest.java
new file mode 100644
index 00000000..e4d4c27f
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/effects/GoldIncreaseTest.java
@@ -0,0 +1,78 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.*;
+
+@RunWith(Theories.class)
+public class GoldIncreaseTest {
+
+ @DataPoints
+ public static int[] goldAmounts() {
+ return new int[]{-5, -1, 0, 1, 2, 5, 10};
+ }
+
+ @DataPoints
+ public static ResourceType[] resourceTypes() {
+ return ResourceType.values();
+ }
+
+ @Theory
+ public void apply_increaseGoldWithRightAmount(int initialAmount, int goldIncreaseAmount, ResourceType type) {
+ Board board = TestUtils.createBoard(type, initialAmount);
+ GoldIncrease goldIncrease = new GoldIncrease(goldIncreaseAmount);
+
+ goldIncrease.apply(board);
+
+ assertEquals(initialAmount + goldIncreaseAmount, board.getGold());
+ }
+
+ @Theory
+ public void computePoints_isAlwaysZero(int gold) {
+ GoldIncrease goldIncrease = new GoldIncrease(gold);
+ Table table = TestUtils.createTable(5);
+ assertEquals(0, goldIncrease.computePoints(table, 0));
+ }
+
+ @Theory
+ public void equals_falseWhenNull(int gold) {
+ GoldIncrease goldIncrease = new GoldIncrease(gold);
+ //noinspection ObjectEqualsNull
+ assertFalse(goldIncrease.equals(null));
+ }
+
+ @Theory
+ public void equals_falseWhenDifferentClass(int gold) {
+ GoldIncrease goldIncrease = new GoldIncrease(gold);
+ MilitaryReinforcements reinforcements = new MilitaryReinforcements(gold);
+ //noinspection EqualsBetweenInconvertibleTypes
+ assertFalse(goldIncrease.equals(reinforcements));
+ }
+
+ @Theory
+ public void equals_trueWhenSame(int gold) {
+ GoldIncrease goldIncrease = new GoldIncrease(gold);
+ assertEquals(goldIncrease, goldIncrease);
+ }
+
+ @Theory
+ public void equals_trueWhenSameContent(int gold) {
+ GoldIncrease goldIncrease1 = new GoldIncrease(gold);
+ GoldIncrease goldIncrease2 = new GoldIncrease(gold);
+ assertTrue(goldIncrease1.equals(goldIncrease2));
+ }
+
+ @Theory
+ public void hashCode_sameWhenSameContent(int gold) {
+ GoldIncrease goldIncrease1 = new GoldIncrease(gold);
+ GoldIncrease goldIncrease2 = new GoldIncrease(gold);
+ assertEquals(goldIncrease1.hashCode(), goldIncrease2.hashCode());
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/effects/MilitaryReinforcementsTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/effects/MilitaryReinforcementsTest.java
new file mode 100644
index 00000000..d3c2cc03
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/effects/MilitaryReinforcementsTest.java
@@ -0,0 +1,79 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.*;
+
+@RunWith(Theories.class)
+public class MilitaryReinforcementsTest {
+
+ @DataPoints
+ public static int[] shieldCounts() {
+ return new int[]{0, 1, 2, 3, 5};
+ }
+
+ @DataPoints
+ public static ResourceType[] resourceTypes() {
+ return ResourceType.values();
+ }
+
+ @Theory
+ public void apply_increaseGoldWithRightAmount(int initialShields, int additionalShields, ResourceType type) {
+ Board board = TestUtils.createBoard(type);
+ board.getMilitary().addShields(initialShields);
+
+ MilitaryReinforcements reinforcements = new MilitaryReinforcements(additionalShields);
+ reinforcements.apply(board);
+
+ assertEquals(initialShields + additionalShields, board.getMilitary().getNbShields());
+ }
+
+ @Theory
+ public void computePoints_isAlwaysZero(int shields) {
+ MilitaryReinforcements reinforcements = new MilitaryReinforcements(shields);
+ Table table = TestUtils.createTable(5);
+ assertEquals(0, reinforcements.computePoints(table, 0));
+ }
+
+ @Theory
+ public void equals_falseWhenNull(int shields) {
+ MilitaryReinforcements reinforcements = new MilitaryReinforcements(shields);
+ //noinspection ObjectEqualsNull
+ assertFalse(reinforcements.equals(null));
+ }
+
+ @Theory
+ public void equals_falseWhenDifferentClass(int shields) {
+ MilitaryReinforcements reinforcements = new MilitaryReinforcements(shields);
+ GoldIncrease goldIncrease = new GoldIncrease(shields);
+ //noinspection EqualsBetweenInconvertibleTypes
+ assertFalse(reinforcements.equals(goldIncrease));
+ }
+
+ @Theory
+ public void equals_trueWhenSame(int shields) {
+ MilitaryReinforcements reinforcements = new MilitaryReinforcements(shields);
+ assertEquals(reinforcements, reinforcements);
+ }
+
+ @Theory
+ public void equals_trueWhenSameContent(int shields) {
+ MilitaryReinforcements reinforcements1 = new MilitaryReinforcements(shields);
+ MilitaryReinforcements reinforcements2 = new MilitaryReinforcements(shields);
+ assertTrue(reinforcements1.equals(reinforcements2));
+ }
+
+ @Theory
+ public void hashCode_sameWhenSameContent(int shields) {
+ MilitaryReinforcements reinforcements1 = new MilitaryReinforcements(shields);
+ MilitaryReinforcements reinforcements2 = new MilitaryReinforcements(shields);
+ assertEquals(reinforcements1.hashCode(), reinforcements2.hashCode());
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/effects/ProductionIncreaseTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/effects/ProductionIncreaseTest.java
new file mode 100644
index 00000000..6031e112
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/effects/ProductionIncreaseTest.java
@@ -0,0 +1,85 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.resources.Production;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.resources.Resources;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.*;
+
+@RunWith(Theories.class)
+public class ProductionIncreaseTest {
+
+ @DataPoints
+ public static ResourceType[] resourceTypes() {
+ return ResourceType.values();
+ }
+
+ private static ProductionIncrease createProductionIncrease(ResourceType... types) {
+ ProductionIncrease effect = new ProductionIncrease();
+ effect.getProduction().addAll(TestUtils.createFixedProduction(types));
+ return effect;
+ }
+
+ @Theory
+ public void apply_boardContainsAddedResourceType(ResourceType initialType, ResourceType addedType, ResourceType extraType) {
+ Board board = TestUtils.createBoard(initialType);
+ ProductionIncrease effect = createProductionIncrease(addedType);
+
+ effect.apply(board);
+
+ Resources resources = TestUtils.createResources(initialType, addedType);
+ assertTrue(board.getProduction().contains(resources));
+
+ Resources moreResources = TestUtils.createResources(initialType, addedType, extraType);
+ assertFalse(board.getProduction().contains(moreResources));
+ }
+
+ @Theory
+ public void computePoints_isAlwaysZero(ResourceType addedType) {
+ ProductionIncrease effect = createProductionIncrease(addedType);
+ Table table = TestUtils.createTable(5);
+ assertEquals(0, effect.computePoints(table, 0));
+ }
+
+ @Theory
+ public void equals_falseWhenNull(ResourceType addedType) {
+ ProductionIncrease effect = createProductionIncrease(addedType);
+ //noinspection ObjectEqualsNull
+ assertFalse(effect.equals(null));
+ }
+
+ @Theory
+ public void equals_falseWhenDifferentClass(ResourceType addedType) {
+ ProductionIncrease effect = createProductionIncrease(addedType);
+ Production production = TestUtils.createFixedProduction(addedType);
+ //noinspection EqualsBetweenInconvertibleTypes
+ assertFalse(effect.equals(production));
+ }
+
+ @Theory
+ public void equals_trueWhenSame(ResourceType addedType) {
+ ProductionIncrease effect = createProductionIncrease(addedType);
+ assertEquals(effect, effect);
+ }
+
+ @Theory
+ public void equals_trueWhenSameContent(ResourceType addedType) {
+ ProductionIncrease effect1 = createProductionIncrease(addedType);
+ ProductionIncrease effect2 = createProductionIncrease(addedType);
+ assertTrue(effect1.equals(effect2));
+ }
+
+ @Theory
+ public void hashCode_sameWhenSameContent(ResourceType addedType) {
+ ProductionIncrease effect1 = createProductionIncrease(addedType);
+ ProductionIncrease effect2 = createProductionIncrease(addedType);
+ assertEquals(effect1.hashCode(), effect2.hashCode());
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/effects/RawPointsIncreaseTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/effects/RawPointsIncreaseTest.java
new file mode 100644
index 00000000..a1c8a8de
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/effects/RawPointsIncreaseTest.java
@@ -0,0 +1,61 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.*;
+
+@RunWith(Theories.class)
+public class RawPointsIncreaseTest {
+
+ @DataPoints
+ public static int[] points() {
+ return new int[] {0, 1, 2, 3, 5};
+ }
+
+ @Theory
+ public void computePoints_equalsNbOfPoints(int points) {
+ RawPointsIncrease rawPointsIncrease = new RawPointsIncrease(points);
+ Table table = TestUtils.createTable(5);
+ assertEquals(points, rawPointsIncrease.computePoints(table, 0));
+ }
+
+ @Theory
+ public void equals_falseWhenNull(int points) {
+ RawPointsIncrease rawPointsIncrease = new RawPointsIncrease(points);
+ //noinspection ObjectEqualsNull
+ assertFalse(rawPointsIncrease.equals(null));
+ }
+
+ @Theory
+ public void equals_falseWhenDifferentClass(int points) {
+ RawPointsIncrease rawPointsIncrease = new RawPointsIncrease(points);
+ GoldIncrease goldIncrease = new GoldIncrease(points);
+ //noinspection EqualsBetweenInconvertibleTypes
+ assertFalse(rawPointsIncrease.equals(goldIncrease));
+ }
+
+ @Theory
+ public void equals_trueWhenSame(int points) {
+ RawPointsIncrease rawPointsIncrease = new RawPointsIncrease(points);
+ assertEquals(rawPointsIncrease, rawPointsIncrease);
+ }
+
+ @Theory
+ public void equals_trueWhenSameContent(int points) {
+ RawPointsIncrease rawPointsIncrease1 = new RawPointsIncrease(points);
+ RawPointsIncrease rawPointsIncrease2 = new RawPointsIncrease(points);
+ assertTrue(rawPointsIncrease1.equals(rawPointsIncrease2));
+ }
+
+ @Theory
+ public void hashCode_sameWhenSameContent(int points) {
+ RawPointsIncrease rawPointsIncrease1 = new RawPointsIncrease(points);
+ RawPointsIncrease rawPointsIncrease2 = new RawPointsIncrease(points);
+ assertEquals(rawPointsIncrease1.hashCode(), rawPointsIncrease2.hashCode());
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/effects/ScienceProgressTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/effects/ScienceProgressTest.java
new file mode 100644
index 00000000..56289654
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/effects/ScienceProgressTest.java
@@ -0,0 +1,38 @@
+package org.luxons.sevenwonders.game.effects;
+
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.boards.Science;
+import org.luxons.sevenwonders.game.boards.ScienceType;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.*;
+
+@RunWith(Theories.class)
+public class ScienceProgressTest {
+
+ @DataPoints
+ public static int[] elementsCount() {
+ return new int[] {0, 1, 2};
+ }
+
+ @Theory
+ public void apply_initContainsAddedScience(int initCompasses, int initWheels, int initTablets, int initJokers,
+ int compasses, int wheels, int tablets, int jokers) {
+ Board board = TestUtils.createBoard(ResourceType.ORE);
+ Science initialScience = TestUtils.createScience(initCompasses, initWheels, initTablets, initJokers);
+ board.getScience().addAll(initialScience);
+
+ ScienceProgress effect = TestUtils.createScienceProgress(compasses, wheels, tablets, jokers);
+ effect.apply(board);
+
+ assertEquals(initCompasses + compasses, board.getScience().getQuantity(ScienceType.COMPASS));
+ assertEquals(initWheels + wheels, board.getScience().getQuantity(ScienceType.WHEEL));
+ assertEquals(initTablets + tablets, board.getScience().getQuantity(ScienceType.TABLET));
+ assertEquals(initJokers + jokers, board.getScience().getJokers());
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/effects/SpecialAbilityActivationTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/effects/SpecialAbilityActivationTest.java
new file mode 100644
index 00000000..b04db127
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/effects/SpecialAbilityActivationTest.java
@@ -0,0 +1,94 @@
+package org.luxons.sevenwonders.game.effects;
+
+import java.util.Arrays;
+
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.boards.BoardElementType;
+import org.luxons.sevenwonders.game.boards.RelativeBoardPosition;
+import org.luxons.sevenwonders.game.cards.Card;
+import org.luxons.sevenwonders.game.cards.Color;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(Theories.class)
+public class SpecialAbilityActivationTest {
+
+ @DataPoints
+ public static SpecialAbility[] abilities() {
+ return SpecialAbility.values();
+ }
+
+ @DataPoints
+ public static RelativeBoardPosition[] neighbours() {
+ return new RelativeBoardPosition[]{RelativeBoardPosition.LEFT, RelativeBoardPosition.RIGHT};
+ }
+
+ @DataPoints
+ public static Card[] guilds() {
+ BonusPerBoardElement bonus = new BonusPerBoardElement();
+ bonus.setType(BoardElementType.CARD);
+ bonus.setColors(Arrays.asList(Color.GREY, Color.BROWN));
+ bonus.setBoards(Arrays.asList(RelativeBoardPosition.LEFT, RelativeBoardPosition.RIGHT));
+ bonus.setPoints(1);
+
+ BonusPerBoardElement bonus2 = new BonusPerBoardElement();
+ bonus2.setType(BoardElementType.BUILT_WONDER_STAGES);
+ bonus2.setBoards(
+ Arrays.asList(RelativeBoardPosition.LEFT, RelativeBoardPosition.SELF, RelativeBoardPosition.RIGHT));
+ bonus2.setPoints(1);
+
+ return new Card[]{TestUtils.createGuildCard(1, bonus), TestUtils.createGuildCard(2, bonus2)};
+ }
+
+ @Theory
+ public void apply_addsAbility(SpecialAbility ability) {
+ SpecialAbilityActivation effect = new SpecialAbilityActivation(ability);
+ Table table = TestUtils.createTable(5);
+
+ effect.apply(table, 0);
+
+ Board board = table.getBoard(0);
+ assertTrue(board.hasSpecial(ability));
+ }
+
+ @Theory
+ public void computePoints_zeroExceptForCopyGuild(SpecialAbility ability) {
+ Assume.assumeTrue(ability != SpecialAbility.COPY_GUILD);
+
+ SpecialAbilityActivation effect = new SpecialAbilityActivation(ability);
+ Table table = TestUtils.createTable(5);
+
+ assertEquals(0, effect.computePoints(table, 0));
+ }
+
+ @Theory
+ public void computePoints_copiedGuild(Card guildCard, RelativeBoardPosition neighbour) {
+ SpecialAbilityActivation effect = new SpecialAbilityActivation(SpecialAbility.COPY_GUILD);
+ Table table = TestUtils.createTable(5);
+
+ Board neighbourBoard = table.getBoard(0, neighbour);
+ neighbourBoard.addCard(guildCard);
+
+ Board board = table.getBoard(0);
+ board.setCopiedGuild(guildCard);
+
+ int directPointsFromGuildCard = guildCard.getEffects().stream().mapToInt(e -> e.computePoints(table, 0)).sum();
+ assertEquals(directPointsFromGuildCard, effect.computePoints(table, 0));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void computePoints_copyGuild_failWhenNoChosenGuild() {
+ SpecialAbilityActivation effect = new SpecialAbilityActivation(SpecialAbility.COPY_GUILD);
+ Table table = TestUtils.createTable(5);
+ effect.computePoints(table, 0);
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/resources/ProductionTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/resources/ProductionTest.java
new file mode 100644
index 00000000..76d2345f
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/resources/ProductionTest.java
@@ -0,0 +1,271 @@
+package org.luxons.sevenwonders.game.resources;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class ProductionTest {
+
+ private Resources emptyResources;
+
+ private Resources resources1Wood;
+
+ private Resources resources1Stone;
+
+ private Resources resources1Stone1Wood;
+
+ private Resources resources2Stones;
+
+ private Resources resources2Stones3Clay;
+
+ @Before
+ public void init() {
+ emptyResources = new Resources();
+
+ resources1Wood = new Resources();
+ resources1Wood.add(ResourceType.WOOD, 1);
+
+ resources1Stone = new Resources();
+ resources1Stone.add(ResourceType.STONE, 1);
+
+ resources1Stone1Wood = new Resources();
+ resources1Stone1Wood.add(ResourceType.STONE, 1);
+ resources1Stone1Wood.add(ResourceType.WOOD, 1);
+
+ resources2Stones = new Resources();
+ resources2Stones.add(ResourceType.STONE, 2);
+
+ resources2Stones3Clay = new Resources();
+ resources2Stones3Clay.add(ResourceType.STONE, 2);
+ resources2Stones3Clay.add(ResourceType.CLAY, 3);
+ }
+
+ @Test
+ public void contains_newProductionContainsEmpty() {
+ Production production = new Production();
+ assertTrue(production.contains(emptyResources));
+ }
+
+ @Test
+ public void contains_singleFixedResource_noneAtAll() {
+ Production production = new Production();
+ assertFalse(production.contains(resources2Stones));
+ }
+
+ @Test
+ public void contains_singleFixedResource_notEnough() {
+ Production production = new Production();
+ production.addFixedResource(ResourceType.STONE, 1);
+ assertFalse(production.contains(resources2Stones));
+ }
+
+ @Test
+ public void contains_singleFixedResource_justEnough() {
+ Production production = new Production();
+ production.addFixedResource(ResourceType.STONE, 2);
+ assertTrue(production.contains(resources2Stones));
+ }
+
+ @Test
+ public void contains_singleFixedResource_moreThanEnough() {
+ Production production = new Production();
+ production.addFixedResource(ResourceType.STONE, 3);
+ assertTrue(production.contains(resources2Stones));
+ }
+
+ @Test
+ public void contains_singleFixedResource_moreThanEnough_amongOthers() {
+ Production production = new Production();
+ production.addFixedResource(ResourceType.STONE, 3);
+ production.addFixedResource(ResourceType.CLAY, 2);
+ assertTrue(production.contains(resources2Stones));
+ }
+
+ @Test
+ public void contains_multipleFixedResources_notEnoughOfOne() {
+ Production production = new Production();
+ production.addFixedResource(ResourceType.STONE, 3);
+ production.addFixedResource(ResourceType.CLAY, 1);
+ assertFalse(production.contains(resources2Stones3Clay));
+ }
+
+ @Test
+ public void contains_multipleFixedResources_notEnoughOfBoth() {
+ Production production = new Production();
+ production.addFixedResource(ResourceType.STONE, 1);
+ production.addFixedResource(ResourceType.CLAY, 1);
+ assertFalse(production.contains(resources2Stones3Clay));
+ }
+
+ @Test
+ public void contains_multipleFixedResources_moreThanEnough() {
+ Production production = new Production();
+ production.addFixedResource(ResourceType.STONE, 3);
+ production.addFixedResource(ResourceType.CLAY, 5);
+ assertTrue(production.contains(resources2Stones3Clay));
+ }
+
+ @Test
+ public void contains_singleChoice_containsEmpty() {
+ Production production = new Production();
+ production.addChoice(ResourceType.STONE, ResourceType.CLAY);
+ assertTrue(production.contains(emptyResources));
+ }
+
+ @Test
+ public void contains_singleChoice_enough() {
+ Production production = new Production();
+ production.addChoice(ResourceType.STONE, ResourceType.WOOD);
+ assertTrue(production.contains(resources1Wood));
+ assertTrue(production.contains(resources1Stone));
+ }
+
+ @Test
+ public void contains_multipleChoices_notBoth() {
+ Production production = new Production();
+ production.addChoice(ResourceType.STONE, ResourceType.CLAY);
+ production.addChoice(ResourceType.STONE, ResourceType.CLAY);
+ production.addChoice(ResourceType.STONE, ResourceType.CLAY);
+ assertFalse(production.contains(resources2Stones3Clay));
+ }
+
+ @Test
+ public void contains_multipleChoices_enough() {
+ Production production = new Production();
+ production.addChoice(ResourceType.STONE, ResourceType.ORE);
+ production.addChoice(ResourceType.STONE, ResourceType.WOOD);
+ assertTrue(production.contains(resources1Stone1Wood));
+ }
+
+ @Test
+ public void contains_multipleChoices_enoughReverseOrder() {
+ Production production = new Production();
+ production.addChoice(ResourceType.STONE, ResourceType.WOOD);
+ production.addChoice(ResourceType.STONE, ResourceType.ORE);
+ assertTrue(production.contains(resources1Stone1Wood));
+ }
+
+ @Test
+ public void contains_multipleChoices_moreThanEnough() {
+ Production production = new Production();
+ production.addChoice(ResourceType.LOOM, ResourceType.GLASS, ResourceType.PAPYRUS);
+ production.addChoice(ResourceType.STONE, ResourceType.ORE);
+ production.addChoice(ResourceType.STONE, ResourceType.WOOD);
+ assertTrue(production.contains(resources1Stone1Wood));
+ }
+
+ @Test
+ public void contains_mixedFixedAndChoice_enough() {
+ Production production = new Production();
+ production.addFixedResource(ResourceType.WOOD, 1);
+ production.addChoice(ResourceType.STONE, ResourceType.WOOD);
+ assertTrue(production.contains(resources1Stone1Wood));
+ }
+
+ @Test
+ public void addAll_empty() {
+ Production production = new Production();
+ production.addAll(emptyResources);
+ assertTrue(production.contains(emptyResources));
+ }
+
+ @Test
+ public void addAll_singleResource() {
+ Production production = new Production();
+ production.addAll(resources1Stone);
+ assertTrue(production.contains(resources1Stone));
+ }
+
+ @Test
+ public void addAll_multipleResources() {
+ Production production = new Production();
+ production.addAll(resources2Stones3Clay);
+ assertTrue(production.contains(resources2Stones3Clay));
+ }
+
+ @Test
+ public void addAll_production_multipleFixedResources() {
+ Production production = new Production();
+ production.addAll(resources2Stones3Clay);
+
+ Production production2 = new Production();
+ production2.addAll(production);
+
+ assertTrue(production2.contains(resources2Stones3Clay));
+ }
+
+ @Test
+ public void addAll_production_multipleChoices() {
+ Production production = new Production();
+ production.addChoice(ResourceType.STONE, ResourceType.WOOD);
+ production.addChoice(ResourceType.STONE, ResourceType.ORE);
+
+ Production production2 = new Production();
+ production2.addAll(production);
+ assertTrue(production.contains(resources1Stone1Wood));
+ }
+
+ @Test
+ public void addAll_production_mixedFixedResourcesAndChoices() {
+ Production production = new Production();
+ production.addFixedResource(ResourceType.WOOD, 1);
+ production.addChoice(ResourceType.STONE, ResourceType.WOOD);
+
+ Production production2 = new Production();
+ production2.addAll(production);
+
+ assertTrue(production.contains(resources1Stone1Wood));
+ }
+
+ @Test
+ public void equals_falseWhenNull() {
+ Production production = new Production();
+ production.addFixedResource(ResourceType.GLASS, 1);
+ production.addChoice(ResourceType.ORE, ResourceType.WOOD);
+ //noinspection ObjectEqualsNull
+ assertFalse(production.equals(null));
+ }
+
+ @Test
+ public void equals_falseWhenDifferentClass() {
+ Production production = new Production();
+ production.addFixedResource(ResourceType.GLASS, 1);
+ Resources resources = new Resources();
+ resources.add(ResourceType.GLASS, 1);
+ //noinspection EqualsBetweenInconvertibleTypes
+ assertFalse(production.equals(resources));
+ }
+
+ @Test
+ public void equals_trueWhenSame() {
+ Production production = new Production();
+ assertEquals(production, production);
+ }
+
+ @Test
+ public void equals_trueWhenSameContent() {
+ Production production1 = new Production();
+ Production production2 = new Production();
+ assertTrue(production1.equals(production2));
+ production1.addFixedResource(ResourceType.GLASS, 1);
+ production2.addFixedResource(ResourceType.GLASS, 1);
+ assertTrue(production1.equals(production2));
+ production1.addChoice(ResourceType.ORE, ResourceType.WOOD);
+ production2.addChoice(ResourceType.ORE, ResourceType.WOOD);
+ assertTrue(production1.equals(production2));
+ }
+
+ @Test
+ public void hashCode_sameWhenSameContent() {
+ Production production1 = new Production();
+ Production production2 = new Production();
+ assertEquals(production1.hashCode(), production2.hashCode());
+ production1.addFixedResource(ResourceType.GLASS, 1);
+ production2.addFixedResource(ResourceType.GLASS, 1);
+ assertEquals(production1.hashCode(), production2.hashCode());
+ production1.addChoice(ResourceType.ORE, ResourceType.WOOD);
+ production2.addChoice(ResourceType.ORE, ResourceType.WOOD);
+ assertEquals(production1.hashCode(), production2.hashCode());
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/resources/ResourcesTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/resources/ResourcesTest.java
new file mode 100644
index 00000000..674c90e7
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/resources/ResourcesTest.java
@@ -0,0 +1,431 @@
+package org.luxons.sevenwonders.game.resources;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class ResourcesTest {
+
+ @Test
+ public void init_shouldBeEmpty() {
+ Resources resources = new Resources();
+ for (ResourceType resourceType : ResourceType.values()) {
+ assertEquals(0, resources.getQuantity(resourceType));
+ }
+ }
+
+ @Test
+ public void add_zero() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.CLAY, 0);
+ assertEquals(0, resources.getQuantity(ResourceType.CLAY));
+ }
+
+ @Test
+ public void add_simple() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.WOOD, 3);
+ assertEquals(3, resources.getQuantity(ResourceType.WOOD));
+ }
+
+ @Test
+ public void add_multipleCallsStacked() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.ORE, 3);
+ resources.add(ResourceType.ORE, 2);
+ assertEquals(5, resources.getQuantity(ResourceType.ORE));
+ }
+
+ @Test
+ public void add_interlaced() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.GLASS, 3);
+ resources.add(ResourceType.STONE, 1);
+ resources.add(ResourceType.WOOD, 4);
+ resources.add(ResourceType.GLASS, 2);
+ assertEquals(5, resources.getQuantity(ResourceType.GLASS));
+ }
+
+ @Test
+ public void addAll_empty() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 1);
+ resources.add(ResourceType.CLAY, 3);
+
+ Resources emptyResources = new Resources();
+
+ resources.addAll(emptyResources);
+ assertEquals(1, resources.getQuantity(ResourceType.STONE));
+ assertEquals(3, resources.getQuantity(ResourceType.CLAY));
+ assertEquals(0, resources.getQuantity(ResourceType.ORE));
+ assertEquals(0, resources.getQuantity(ResourceType.GLASS));
+ assertEquals(0, resources.getQuantity(ResourceType.LOOM));
+ }
+
+ @Test
+ public void addAll_zeros() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 1);
+ resources.add(ResourceType.CLAY, 3);
+
+ Resources emptyResources = new Resources();
+ emptyResources.add(ResourceType.STONE, 0);
+ emptyResources.add(ResourceType.CLAY, 0);
+
+ resources.addAll(emptyResources);
+ assertEquals(1, resources.getQuantity(ResourceType.STONE));
+ assertEquals(3, resources.getQuantity(ResourceType.CLAY));
+ assertEquals(0, resources.getQuantity(ResourceType.ORE));
+ assertEquals(0, resources.getQuantity(ResourceType.GLASS));
+ assertEquals(0, resources.getQuantity(ResourceType.LOOM));
+ }
+
+ @Test
+ public void addAll_same() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 1);
+ resources.add(ResourceType.CLAY, 3);
+
+ Resources resources2 = new Resources();
+ resources.add(ResourceType.STONE, 2);
+ resources.add(ResourceType.CLAY, 6);
+
+ resources.addAll(resources2);
+ assertEquals(3, resources.getQuantity(ResourceType.STONE));
+ assertEquals(9, resources.getQuantity(ResourceType.CLAY));
+ assertEquals(0, resources.getQuantity(ResourceType.ORE));
+ assertEquals(0, resources.getQuantity(ResourceType.GLASS));
+ assertEquals(0, resources.getQuantity(ResourceType.LOOM));
+ }
+
+ @Test
+ public void addAll_overlap() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 1);
+ resources.add(ResourceType.CLAY, 3);
+
+ Resources resources2 = new Resources();
+ resources.add(ResourceType.CLAY, 6);
+ resources.add(ResourceType.ORE, 4);
+
+ resources.addAll(resources2);
+ assertEquals(1, resources.getQuantity(ResourceType.STONE));
+ assertEquals(9, resources.getQuantity(ResourceType.CLAY));
+ assertEquals(4, resources.getQuantity(ResourceType.ORE));
+ assertEquals(0, resources.getQuantity(ResourceType.GLASS));
+ assertEquals(0, resources.getQuantity(ResourceType.LOOM));
+ }
+
+ @Test
+ public void contains_emptyContainsEmpty() {
+ Resources emptyResources = new Resources();
+ Resources emptyResources2 = new Resources();
+ assertTrue(emptyResources.contains(emptyResources2));
+ }
+
+ @Test
+ public void contains_singleTypeContainsEmpty() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 1);
+
+ Resources emptyResources = new Resources();
+
+ assertTrue(resources.contains(emptyResources));
+ }
+
+ @Test
+ public void contains_multipleTypesContainsEmpty() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 1);
+ resources.add(ResourceType.CLAY, 3);
+
+ Resources emptyResources = new Resources();
+
+ assertTrue(resources.contains(emptyResources));
+ }
+
+ @Test
+ public void contains_self() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 1);
+ resources.add(ResourceType.CLAY, 3);
+
+ assertTrue(resources.contains(resources));
+ }
+
+ @Test
+ public void contains_allOfEachType() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 1);
+ resources.add(ResourceType.CLAY, 3);
+
+ Resources resources2 = new Resources();
+ resources2.add(ResourceType.STONE, 1);
+ resources2.add(ResourceType.CLAY, 3);
+
+ assertTrue(resources.contains(resources2));
+ }
+
+ @Test
+ public void contains_someOfEachType() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 2);
+ resources.add(ResourceType.CLAY, 4);
+
+ Resources resources2 = new Resources();
+ resources2.add(ResourceType.STONE, 1);
+ resources2.add(ResourceType.CLAY, 3);
+
+ assertTrue(resources.contains(resources2));
+ }
+
+ @Test
+ public void contains_someOfSomeTypes() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 2);
+ resources.add(ResourceType.CLAY, 4);
+
+ Resources resources2 = new Resources();
+ resources2.add(ResourceType.CLAY, 3);
+
+ assertTrue(resources.contains(resources2));
+ }
+
+ @Test
+ public void contains_allOfSomeTypes() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 2);
+ resources.add(ResourceType.CLAY, 4);
+
+ Resources resources2 = new Resources();
+ resources2.add(ResourceType.CLAY, 4);
+
+ assertTrue(resources.contains(resources2));
+ }
+
+ @Test
+ public void minus_empty() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 1);
+ resources.add(ResourceType.CLAY, 3);
+
+ Resources emptyResources = new Resources();
+
+ Resources diff = resources.minus(emptyResources);
+ assertEquals(1, diff.getQuantity(ResourceType.STONE));
+ assertEquals(3, diff.getQuantity(ResourceType.CLAY));
+ assertEquals(0, diff.getQuantity(ResourceType.ORE));
+ assertEquals(0, diff.getQuantity(ResourceType.GLASS));
+ assertEquals(0, diff.getQuantity(ResourceType.LOOM));
+ }
+
+ @Test
+ public void minus_self() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 1);
+ resources.add(ResourceType.CLAY, 3);
+
+ Resources diff = resources.minus(resources);
+ assertEquals(0, diff.getQuantity(ResourceType.STONE));
+ assertEquals(0, diff.getQuantity(ResourceType.CLAY));
+ assertEquals(0, diff.getQuantity(ResourceType.ORE));
+ assertEquals(0, diff.getQuantity(ResourceType.GLASS));
+ assertEquals(0, diff.getQuantity(ResourceType.LOOM));
+ }
+
+ @Test
+ public void minus_allOfEachType() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 1);
+ resources.add(ResourceType.CLAY, 3);
+
+ Resources resources2 = new Resources();
+ resources2.add(ResourceType.STONE, 1);
+ resources2.add(ResourceType.CLAY, 3);
+
+ Resources diff = resources.minus(resources2);
+ assertEquals(0, diff.getQuantity(ResourceType.STONE));
+ assertEquals(0, diff.getQuantity(ResourceType.CLAY));
+ assertEquals(0, diff.getQuantity(ResourceType.ORE));
+ assertEquals(0, diff.getQuantity(ResourceType.GLASS));
+ assertEquals(0, diff.getQuantity(ResourceType.LOOM));
+ }
+
+ @Test
+ public void minus_someOfEachType() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 2);
+ resources.add(ResourceType.CLAY, 4);
+
+ Resources resources2 = new Resources();
+ resources2.add(ResourceType.STONE, 1);
+ resources2.add(ResourceType.CLAY, 3);
+
+ Resources diff = resources.minus(resources2);
+ assertEquals(1, diff.getQuantity(ResourceType.STONE));
+ assertEquals(1, diff.getQuantity(ResourceType.CLAY));
+ assertEquals(0, diff.getQuantity(ResourceType.ORE));
+ assertEquals(0, diff.getQuantity(ResourceType.GLASS));
+ assertEquals(0, diff.getQuantity(ResourceType.LOOM));
+ }
+
+ @Test
+ public void minus_someOfSomeTypes() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 2);
+ resources.add(ResourceType.CLAY, 4);
+
+ Resources resources2 = new Resources();
+ resources2.add(ResourceType.CLAY, 3);
+
+ Resources diff = resources.minus(resources2);
+ assertEquals(2, diff.getQuantity(ResourceType.STONE));
+ assertEquals(1, diff.getQuantity(ResourceType.CLAY));
+ assertEquals(0, diff.getQuantity(ResourceType.ORE));
+ assertEquals(0, diff.getQuantity(ResourceType.GLASS));
+ assertEquals(0, diff.getQuantity(ResourceType.LOOM));
+ }
+
+ @Test
+ public void minus_allOfSomeTypes() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.STONE, 2);
+ resources.add(ResourceType.CLAY, 4);
+
+ Resources resources2 = new Resources();
+ resources2.add(ResourceType.CLAY, 4);
+
+ Resources diff = resources.minus(resources2);
+ assertEquals(2, diff.getQuantity(ResourceType.STONE));
+ assertEquals(0, diff.getQuantity(ResourceType.CLAY));
+ assertEquals(0, diff.getQuantity(ResourceType.ORE));
+ assertEquals(0, diff.getQuantity(ResourceType.GLASS));
+ assertEquals(0, diff.getQuantity(ResourceType.LOOM));
+ }
+
+ @Test
+ public void minus_tooMuchOfExistingType() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.CLAY, 4);
+
+ Resources resources2 = new Resources();
+ resources2.add(ResourceType.CLAY, 5);
+
+ Resources diff = resources.minus(resources2);
+ assertEquals(0, diff.getQuantity(ResourceType.CLAY));
+ }
+
+ @Test
+ public void minus_someOfAnAbsentType() {
+ Resources resources = new Resources();
+
+ Resources resources2 = new Resources();
+ resources2.add(ResourceType.LOOM, 5);
+
+ Resources diff = resources.minus(resources2);
+ assertEquals(0, diff.getQuantity(ResourceType.LOOM));
+ }
+
+ @Test
+ public void minus_someOfATypeWithZero() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.LOOM, 0);
+
+ Resources resources2 = new Resources();
+ resources2.add(ResourceType.LOOM, 5);
+
+ Resources diff = resources.minus(resources2);
+ assertEquals(0, diff.getQuantity(ResourceType.LOOM));
+ }
+
+ @Test
+ public void isEmpty_noElement() {
+ Resources resources = new Resources();
+ assertTrue(resources.isEmpty());
+ }
+
+ @Test
+ public void isEmpty_singleZeroElement() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.LOOM, 0);
+ assertTrue(resources.isEmpty());
+ }
+
+ @Test
+ public void isEmpty_multipleZeroElements() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.WOOD, 0);
+ resources.add(ResourceType.ORE, 0);
+ resources.add(ResourceType.LOOM, 0);
+ assertTrue(resources.isEmpty());
+ }
+
+ @Test
+ public void isEmpty_singleElementMoreThanZero() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.LOOM, 3);
+ assertFalse(resources.isEmpty());
+ }
+
+ @Test
+ public void isEmpty_mixedZeroAndNonZeroElements() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.WOOD, 0);
+ resources.add(ResourceType.LOOM, 3);
+ assertFalse(resources.isEmpty());
+ }
+
+ @Test
+ public void isEmpty_mixedZeroAndNonZeroElements_reverseOrder() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.ORE, 3);
+ resources.add(ResourceType.PAPYRUS, 0);
+ assertFalse(resources.isEmpty());
+ }
+
+ @Test
+ public void equals_falseWhenNull() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.GLASS, 1);
+ //noinspection ObjectEqualsNull
+ assertFalse(resources.equals(null));
+ }
+
+ @Test
+ public void equals_falseWhenDifferentClass() {
+ Resources resources = new Resources();
+ resources.add(ResourceType.GLASS, 1);
+ Production production = new Production();
+ production.addFixedResource(ResourceType.GLASS, 1);
+ //noinspection EqualsBetweenInconvertibleTypes
+ assertFalse(resources.equals(production));
+ }
+
+ @Test
+ public void equals_trueWhenSame() {
+ Resources resources = new Resources();
+ assertEquals(resources, resources);
+ }
+
+ @Test
+ public void equals_trueWhenSameContent() {
+ Resources resources1 = new Resources();
+ Resources resources2 = new Resources();
+ assertTrue(resources1.equals(resources2));
+ resources1.add(ResourceType.GLASS, 1);
+ resources2.add(ResourceType.GLASS, 1);
+ assertTrue(resources1.equals(resources2));
+ }
+
+ @Test
+ public void hashCode_sameWhenSameContent() {
+ Resources resources1 = new Resources();
+ Resources resources2 = new Resources();
+ assertEquals(resources1.hashCode(), resources2.hashCode());
+ resources1.add(ResourceType.GLASS, 1);
+ resources2.add(ResourceType.GLASS, 1);
+ assertEquals(resources1.hashCode(), resources2.hashCode());
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/resources/TradingRulesTest.java b/backend/src/test/java/org/luxons/sevenwonders/game/resources/TradingRulesTest.java
new file mode 100644
index 00000000..cd6661dc
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/resources/TradingRulesTest.java
@@ -0,0 +1,96 @@
+package org.luxons.sevenwonders.game.resources;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.runner.RunWith;
+import org.luxons.sevenwonders.game.test.TestUtils;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+@RunWith(Theories.class)
+public class TradingRulesTest {
+
+ @DataPoints
+ public static int[] costs() {
+ return new int[] {0, 1, 2};
+ }
+
+ @DataPoints
+ public static Provider[] providers() {
+ return Provider.values();
+ }
+
+ @DataPoints
+ public static ResourceType[] resourceTypes() {
+ return ResourceType.values();
+ }
+
+ @Theory
+ public void computeCost_zeroForNoResources(int defaultCost) {
+ TradingRules rules = new TradingRules(defaultCost);
+ assertEquals(0, rules.computeCost(new ArrayList<>()));
+ }
+
+ @Theory
+ public void computeCost_defaultCostWhenNoOverride(int defaultCost, Provider provider, ResourceType type) {
+ TradingRules rules = new TradingRules(defaultCost);
+ BoughtResources resources = TestUtils.createBoughtResources(provider, type);
+ assertEquals(defaultCost, rules.computeCost(Collections.singletonList(resources)));
+ }
+
+ @Theory
+ public void computeCost_twiceDefaultFor2Resources(int defaultCost, Provider provider, ResourceType type) {
+ TradingRules rules = new TradingRules(defaultCost);
+ BoughtResources resources = TestUtils.createBoughtResources(provider, type, type);
+ assertEquals(2 * defaultCost, rules.computeCost(Collections.singletonList(resources)));
+ }
+
+ @Theory
+ public void computeCost_overriddenCost(int defaultCost, int overriddenCost, Provider provider, ResourceType type) {
+ TradingRules rules = new TradingRules(defaultCost);
+ rules.setCost(type, provider, overriddenCost);
+ BoughtResources resources = TestUtils.createBoughtResources(provider, type);
+ assertEquals(overriddenCost, rules.computeCost(Collections.singletonList(resources)));
+ }
+
+ @Theory
+ public void computeCost_defaultCostWhenOverrideOnOtherProviderOrType(int defaultCost, int overriddenCost,
+ Provider overriddenProvider, ResourceType overriddenType, Provider provider, ResourceType type) {
+ assumeTrue(overriddenProvider != provider || overriddenType != type);
+ TradingRules rules = new TradingRules(defaultCost);
+ rules.setCost(overriddenType, overriddenProvider, overriddenCost);
+ BoughtResources resources = TestUtils.createBoughtResources(provider, type);
+ assertEquals(defaultCost, rules.computeCost(Collections.singletonList(resources)));
+ }
+
+ @Theory
+ public void computeCost_oneDefaultAndOneOverriddenType(int defaultCost, int overriddenCost,
+ ResourceType overriddenType, Provider provider, ResourceType type) {
+ assumeTrue(overriddenType != type);
+ TradingRules rules = new TradingRules(defaultCost);
+ rules.setCost(overriddenType, provider, overriddenCost);
+ BoughtResources resources = TestUtils.createBoughtResources(provider, overriddenType, type);
+ assertEquals(defaultCost + overriddenCost, rules.computeCost(Collections.singletonList(resources)));
+ }
+
+ @Theory
+ public void computeCost_oneDefaultAndOneOverriddenProvider(int defaultCost, int overriddenCost,
+ Provider overriddenProvider, Provider provider, ResourceType type) {
+ assumeTrue(overriddenProvider != provider);
+ TradingRules rules = new TradingRules(defaultCost);
+ rules.setCost(type, overriddenProvider, overriddenCost);
+
+ List<BoughtResources> boughtResources = new ArrayList<>(2);
+ boughtResources.add(TestUtils.createBoughtResources(provider, type));
+ boughtResources.add(TestUtils.createBoughtResources(overriddenProvider, type));
+
+ assertEquals(defaultCost + overriddenCost, rules.computeCost(boughtResources));
+ }
+
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/game/test/TestUtils.java b/backend/src/test/java/org/luxons/sevenwonders/game/test/TestUtils.java
new file mode 100644
index 00000000..b5ddb7b1
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/game/test/TestUtils.java
@@ -0,0 +1,191 @@
+package org.luxons.sevenwonders.game.test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.luxons.sevenwonders.game.Game;
+import org.luxons.sevenwonders.game.Player;
+import org.luxons.sevenwonders.game.Settings;
+import org.luxons.sevenwonders.game.api.CustomizableSettings;
+import org.luxons.sevenwonders.game.api.Table;
+import org.luxons.sevenwonders.game.boards.Board;
+import org.luxons.sevenwonders.game.boards.Science;
+import org.luxons.sevenwonders.game.boards.ScienceType;
+import org.luxons.sevenwonders.game.cards.Card;
+import org.luxons.sevenwonders.game.cards.Color;
+import org.luxons.sevenwonders.game.cards.Decks;
+import org.luxons.sevenwonders.game.cards.Requirements;
+import org.luxons.sevenwonders.game.effects.Effect;
+import org.luxons.sevenwonders.game.effects.ScienceProgress;
+import org.luxons.sevenwonders.game.resources.BoughtResources;
+import org.luxons.sevenwonders.game.resources.Production;
+import org.luxons.sevenwonders.game.resources.Provider;
+import org.luxons.sevenwonders.game.resources.ResourceType;
+import org.luxons.sevenwonders.game.resources.Resources;
+import org.luxons.sevenwonders.game.wonders.Wonder;
+import org.luxons.sevenwonders.game.wonders.WonderStage;
+
+public class TestUtils {
+
+ public static Game createGame(int id, int nbPlayers) {
+ Settings settings = new Settings(nbPlayers, new CustomizableSettings());
+ List<Player> players = TestUtils.createPlayers(nbPlayers);
+ List<Board> boards = TestUtils.createBoards(nbPlayers);
+ List<Card> cards = TestUtils.createSampleCards(0, nbPlayers * 7);
+ Map<Integer, List<Card>> cardsPerAge = new HashMap<>();
+ cardsPerAge.put(1, cards);
+ return new Game(id, settings, players, boards, new Decks(cardsPerAge));
+ }
+
+ public static Table createTable(int nbPlayers) {
+ return new Table(createBoards(nbPlayers));
+ }
+
+ public static List<Board> createBoards(int count) {
+ List<Board> boards = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ boards.add(createBoard(ResourceType.WOOD));
+ }
+ return boards;
+ }
+
+ public static List<Player> createPlayers(int count) {
+ List<Player> players = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ String username = "testUser" + i;
+ String displayName = "Test User " + i;
+ Player player = new Player(username, displayName);
+ players.add(player);
+ }
+ return players;
+ }
+
+ public static Board createBoard(ResourceType initialResource) {
+ Settings settings = new Settings(5);
+ Wonder wonder = createWonder(initialResource);
+
+ String username = "testUser" + initialResource.getSymbol();
+ String displayName = "Test User " + initialResource.getSymbol();
+ Player player = new Player(username, displayName);
+
+ return new Board(wonder, player, settings);
+ }
+
+ public static Board createBoard(ResourceType initialResource, ResourceType... production) {
+ Board board = createBoard(initialResource);
+ board.getProduction().addAll(createFixedProduction(production));
+ return board;
+ }
+
+ public static Board createBoard(ResourceType initialResource, int gold, ResourceType... production) {
+ Board board = createBoard(initialResource, production);
+ board.setGold(gold);
+ return board;
+ }
+
+ public static Wonder createWonder() {
+ return createWonder(ResourceType.WOOD);
+ }
+
+ public static Wonder createWonder(ResourceType initialResource) {
+ WonderStage stage1 = new WonderStage();
+ stage1.setRequirements(new Requirements());
+ WonderStage stage2 = new WonderStage();
+ stage1.setRequirements(new Requirements());
+ WonderStage stage3 = new WonderStage();
+ stage1.setRequirements(new Requirements());
+ return new Wonder("Test Wonder " + initialResource.getSymbol(), initialResource, stage1, stage2, stage3);
+ }
+
+ public static Production createFixedProduction(ResourceType... producedTypes) {
+ Production production = new Production();
+ Resources fixedProducedResources = production.getFixedResources();
+ fixedProducedResources.addAll(createResources(producedTypes));
+ return production;
+ }
+
+ public static Resources createResources(ResourceType... types) {
+ Resources resources = new Resources();
+ for (ResourceType producedType : types) {
+ resources.add(producedType, 1);
+ }
+ return resources;
+ }
+
+ public static BoughtResources createBoughtResources(Provider provider, ResourceType... resources) {
+ BoughtResources boughtResources = new BoughtResources();
+ boughtResources.setProvider(provider);
+ boughtResources.setResources(TestUtils.createResources(resources));
+ return boughtResources;
+ }
+
+ public static List<Card> createSampleCards(int fromIndex, int nbCards) {
+ List<Card> sampleCards = new ArrayList<>();
+ for (int i = fromIndex; i < fromIndex + nbCards; i++) {
+ sampleCards.add(createCard(i, Color.BLUE));
+ }
+ return sampleCards;
+ }
+
+ public static Card createCard(String name) {
+ return new Card(name, Color.BLUE, new Requirements(), null, null, null, null);
+ }
+
+ private static Card createCard(int num, Color color) {
+ return new Card("Test Card " + num, color, new Requirements(), null, null, null, null);
+ }
+
+ public static Card createGuildCard(int num, Effect effect) {
+ List<Effect> effects = Collections.singletonList(effect);
+ return new Card("Test Guild " + num, Color.PURPLE, new Requirements(), effects, null, null, null);
+ }
+
+ public static void addCards(Board board, int nbCardsOfColor, int nbOtherCards, Color color) {
+ addCards(board, nbCardsOfColor, color);
+ Color otherColor = getDifferentColorFrom(color);
+ addCards(board, nbOtherCards, otherColor);
+ }
+
+ public static void addCards(Board board, int nbCards, Color color) {
+ for (int i = 0; i < nbCards; i++) {
+ board.addCard(createCard(i, color));
+ }
+ }
+
+ public static Color getDifferentColorFrom(Color... colors) {
+ List<Color> forbiddenColors = Arrays.asList(colors);
+ for (Color color : Color.values()) {
+ if (!forbiddenColors.contains(color)) {
+ return color;
+ }
+ }
+ throw new IllegalArgumentException("All colors are forbidden!");
+ }
+
+ public static ScienceProgress createScienceProgress(int compasses, int wheels, int tablets, int jokers) {
+ ScienceProgress progress = new ScienceProgress();
+ progress.setScience(TestUtils.createScience(compasses, wheels, tablets, jokers));
+ return progress;
+ }
+
+ public static Science createScience(int compasses, int wheels, int tablets, int jokers) {
+ Science science = new Science();
+ if (compasses > 0) {
+ science.add(ScienceType.COMPASS, compasses);
+ }
+ if (wheels > 0) {
+ science.add(ScienceType.WHEEL, wheels);
+ }
+ if (tablets > 0) {
+ science.add(ScienceType.TABLET, tablets);
+ }
+ if (jokers > 0) {
+ science.addJoker(jokers);
+ }
+ return science;
+ }
+}
diff --git a/backend/src/test/java/org/luxons/sevenwonders/repositories/GameRepositoryTest.java b/backend/src/test/java/org/luxons/sevenwonders/repositories/GameRepositoryTest.java
new file mode 100644
index 00000000..5d7d558b
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/repositories/GameRepositoryTest.java
@@ -0,0 +1,61 @@
+package org.luxons.sevenwonders.repositories;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.luxons.sevenwonders.game.Game;
+import org.luxons.sevenwonders.game.test.TestUtils;
+import org.luxons.sevenwonders.repositories.GameRepository.GameAlreadyExistsException;
+import org.luxons.sevenwonders.repositories.GameRepository.GameNotFoundException;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+public class GameRepositoryTest {
+
+ private GameRepository repository;
+
+ @Before
+ public void setUp() {
+ repository = new GameRepository();
+ }
+
+ @Test(expected = GameAlreadyExistsException.class)
+ public void add_failsOnExistingId() {
+ Game game1 = TestUtils.createGame(0, 5);
+ repository.add(game1);
+ Game game2 = TestUtils.createGame(0, 7);
+ repository.add(game2);
+ }
+
+ @Test(expected = GameNotFoundException.class)
+ public void find_failsOnUnknownId() {
+ repository.find(123);
+ }
+
+ @Test
+ public void find_returnsTheSameObject() {
+ Game game = TestUtils.createGame(0, 5);
+ repository.add(game);
+ assertSame(game, repository.find(0));
+ }
+
+ @Test(expected = GameNotFoundException.class)
+ public void remove_failsOnUnknownId() {
+ repository.remove(123);
+ }
+
+ @Test
+ public void remove_succeeds() {
+ Game game = TestUtils.createGame(0, 5);
+ repository.add(game);
+ assertNotNull(repository.find(0));
+ repository.remove(0);
+ try {
+ repository.find(0);
+ fail(); // the call to find() should have failed
+ } catch (GameNotFoundException e) {
+ // the game has been properly removed
+ }
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.java b/backend/src/test/java/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.java
new file mode 100644
index 00000000..f5a8d800
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/repositories/LobbyRepositoryTest.java
@@ -0,0 +1,77 @@
+package org.luxons.sevenwonders.repositories;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.luxons.sevenwonders.game.Lobby;
+import org.luxons.sevenwonders.game.Player;
+import org.luxons.sevenwonders.game.data.GameDefinitionLoader;
+import org.luxons.sevenwonders.repositories.LobbyRepository.LobbyNotFoundException;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class LobbyRepositoryTest {
+
+ private LobbyRepository repository;
+
+ @Before
+ public void setUp() {
+ repository = new LobbyRepository(new GameDefinitionLoader());
+ }
+
+ @Test
+ public void list_initiallyEmpty() {
+ assertTrue(repository.list().isEmpty());
+ }
+
+ @Test
+ public void list_returnsAllLobbies() {
+ Player owner = new Player("owner", "The Owner");
+ Lobby lobby1 = repository.create("Test Name 1", owner);
+ Lobby lobby2 = repository.create("Test Name 2", owner);
+ assertTrue(repository.list().contains(lobby1));
+ assertTrue(repository.list().contains(lobby2));
+ }
+
+ @Test
+ public void create_withCorrectOwner() {
+ Player owner = new Player("owner", "The Owner");
+ Lobby lobby = repository.create("Test Name", owner);
+ assertTrue(lobby.isOwner(owner.getUsername()));
+ }
+
+ @Test(expected = LobbyNotFoundException.class)
+ public void find_failsOnUnknownId() {
+ repository.find(123);
+ }
+
+ @Test
+ public void find_returnsTheSameObject() {
+ Player owner = new Player("owner", "The Owner");
+ Lobby lobby1 = repository.create("Test Name 1", owner);
+ Lobby lobby2 = repository.create("Test Name 2", owner);
+ assertSame(lobby1, repository.find(lobby1.getId()));
+ assertSame(lobby2, repository.find(lobby2.getId()));
+ }
+
+ @Test(expected = LobbyNotFoundException.class)
+ public void remove_failsOnUnknownId() {
+ repository.remove(123);
+ }
+
+ @Test
+ public void remove_succeeds() {
+ Player owner = new Player("owner", "The Owner");
+ Lobby lobby1 = repository.create("Test Name 1", owner);
+ assertNotNull(repository.find(lobby1.getId()));
+ repository.remove(lobby1.getId());
+ try {
+ repository.find(lobby1.getId());
+ fail(); // the call to find() should have failed
+ } catch (LobbyNotFoundException e) {
+ // the lobby has been properly removed
+ }
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.java b/backend/src/test/java/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.java
new file mode 100644
index 00000000..d9e07b3f
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/repositories/PlayerRepositoryTest.java
@@ -0,0 +1,73 @@
+package org.luxons.sevenwonders.repositories;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.luxons.sevenwonders.game.Player;
+import org.luxons.sevenwonders.repositories.PlayerRepository.PlayerNotFoundException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class PlayerRepositoryTest {
+
+ private PlayerRepository repository;
+
+ @Before
+ public void setUp() {
+ repository = new PlayerRepository();
+ }
+
+ @Test
+ public void contains_falseIfNoUserAdded() {
+ assertFalse(repository.contains("anyUsername"));
+ }
+
+ @Test
+ public void contains_trueForCreatedPlayer() {
+ repository.createOrUpdate("player1", "Player 1");
+ assertTrue(repository.contains("player1"));
+ }
+
+ @Test
+ public void createOrUpdate_createsProperly() {
+ Player player1 = repository.createOrUpdate("player1", "Player 1");
+ assertEquals("player1", player1.getUsername());
+ assertEquals("Player 1", player1.getDisplayName());
+ }
+
+ @Test
+ public void createOrUpdate_updatesDisplayName() {
+ Player player1 = repository.createOrUpdate("player1", "Player 1");
+ Player player1Updated = repository.createOrUpdate("player1", "Much Better Name");
+ assertSame(player1, player1Updated);
+ assertEquals("Much Better Name", player1Updated.getDisplayName());
+ }
+
+ @Test(expected = PlayerNotFoundException.class)
+ public void find_failsOnUnknownUsername() {
+ repository.find("anyUsername");
+ }
+
+ @Test
+ public void find_returnsTheSameObject() {
+ Player player1 = repository.createOrUpdate("player1", "Player 1");
+ Player player2 = repository.createOrUpdate("player2", "Player 2");
+ assertSame(player1, repository.find("player1"));
+ assertSame(player2, repository.find("player2"));
+ }
+
+ @Test(expected = PlayerNotFoundException.class)
+ public void remove_failsOnUnknownUsername() {
+ repository.remove("anyUsername");
+ }
+
+ @Test
+ public void remove_succeeds() {
+ repository.createOrUpdate("player1", "Player 1");
+ assertTrue(repository.contains("player1"));
+ repository.remove("player1");
+ assertFalse(repository.contains("player1"));
+ }
+} \ No newline at end of file
diff --git a/backend/src/test/java/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.java b/backend/src/test/java/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.java
new file mode 100644
index 00000000..1a799ff3
--- /dev/null
+++ b/backend/src/test/java/org/luxons/sevenwonders/validation/DestinationAccessValidatorTest.java
@@ -0,0 +1,147 @@
+package org.luxons.sevenwonders.validation;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.luxons.sevenwonders.game.Game;
+import org.luxons.sevenwonders.game.Lobby;
+import org.luxons.sevenwonders.game.Player;
+import org.luxons.sevenwonders.game.data.GameDefinitionLoader;
+import org.luxons.sevenwonders.repositories.GameRepository;
+import org.luxons.sevenwonders.repositories.GameRepository.GameNotFoundException;
+import org.luxons.sevenwonders.repositories.LobbyRepository;
+import org.luxons.sevenwonders.repositories.LobbyRepository.LobbyNotFoundException;
+
+import static org.junit.Assert.*;
+
+public class DestinationAccessValidatorTest {
+
+ private LobbyRepository lobbyRepository;
+
+ private GameRepository gameRepository;
+
+ private DestinationAccessValidator destinationAccessValidator;
+
+ @Before
+ public void setup() {
+ gameRepository = new GameRepository();
+ lobbyRepository = new LobbyRepository(new GameDefinitionLoader());
+ destinationAccessValidator = new DestinationAccessValidator(lobbyRepository, gameRepository);
+ }
+
+ private Lobby createLobby(String gameName, String ownerUsername, String... otherPlayers) {
+ Player owner = new Player(ownerUsername, ownerUsername);
+ Lobby lobby = lobbyRepository.create(gameName, owner);
+ for (String playerName : otherPlayers) {
+ Player player = new Player(playerName, playerName);
+ lobby.addPlayer(player);
+ }
+ return lobby;
+ }
+
+ private void createGame(String gameName, String ownerUsername, String... otherPlayers) {
+ Lobby lobby = createLobby(gameName, ownerUsername, otherPlayers);
+ Game game = lobby.startGame();
+ gameRepository.add(game);
+ }
+
+ @Test
+ public void validate_failsOnNullUser() {
+ assertFalse(destinationAccessValidator.hasAccess(null, "doesNotMatter"));
+ }
+
+ @Test
+ public void validate_successWhenNoReference() {
+ assertTrue(destinationAccessValidator.hasAccess("", ""));
+ assertTrue(destinationAccessValidator.hasAccess("", "test"));
+ assertTrue(destinationAccessValidator.hasAccess("testUser", "test"));
+ }
+
+ @Test
+ public void validate_successWhenNoRefFollows() {
+ assertTrue(destinationAccessValidator.hasAccess("testUser", "/game/"));
+ assertTrue(destinationAccessValidator.hasAccess("testUser", "/lobby/"));
+ assertTrue(destinationAccessValidator.hasAccess("testUser", "prefix/game/"));
+ assertTrue(destinationAccessValidator.hasAccess("testUser", "prefix/lobby/"));
+ assertTrue(destinationAccessValidator.hasAccess("testUser", "/game//suffix"));
+ assertTrue(destinationAccessValidator.hasAccess("testUser", "/lobby//suffix"));
+ }
+
+ @Test
+ public void validate_successWhenRefIsNotANumber() {
+ assertTrue(destinationAccessValidator.hasAccess("testUser", "/game/notANumber"));
+ assertTrue(destinationAccessValidator.hasAccess("testUser", "/lobby/notANumber"));
+ assertTrue(destinationAccessValidator.hasAccess("testUser", "prefix/game/notANumber"));
+ assertTrue(destinationAccessValidator.hasAccess("testUser", "prefix/lobby/notANumber"));
+ assertTrue(destinationAccessValidator.hasAccess("testUser", "/game/notANumber/suffix"));
+ assertTrue(destinationAccessValidator.hasAccess("testUser", "/lobby/notANumber/suffix"));
+ }
+
+ @Test(expected = LobbyNotFoundException.class)
+ public void validate_failWhenNoLobbyExist() {
+ destinationAccessValidator.hasAccess("", "/lobby/0");
+ }
+
+ @Test(expected = GameNotFoundException.class)
+ public void validate_failWhenNoGameExist() {
+ destinationAccessValidator.hasAccess("", "/game/0");
+ }
+
+ @Test(expected = LobbyNotFoundException.class)
+ public void validate_failWhenReferencedLobbyDoesNotExist() {
+ createLobby("Test Game", "ownerUser1");
+ createLobby("Test Game 2", "ownerUser2");
+ destinationAccessValidator.hasAccess("doesNotMatter", "/lobby/3");
+ }
+
+ @Test(expected = GameNotFoundException.class)
+ public void validate_failWhenReferencedGameDoesNotExist() {
+ createGame("Test Game 1", "user1", "user2", "user3");
+ createGame("Test Game 2", "user4", "user5", "user6");
+ destinationAccessValidator.hasAccess("doesNotMatter", "/game/3");
+ }
+
+ @Test
+ public void validate_failWhenUserIsNotPartOfReferencedLobby() {
+ createLobby("Test Game", "ownerUser");
+ destinationAccessValidator.hasAccess("userNotInLobby", "/lobby/0");
+ }
+
+ @Test
+ public void validate_failWhenUserIsNotPartOfReferencedGame() {
+ createGame("Test Game", "ownerUser", "otherUser1", "otherUser2");
+ destinationAccessValidator.hasAccess("userNotInGame", "/game/0");
+ }
+
+ @Test
+ public void validate_successWhenUserIsOwnerOfReferencedLobby() {
+ createLobby("Test Game 1", "user1");
+ assertTrue(destinationAccessValidator.hasAccess("user1", "/lobby/0"));
+ createLobby("Test Game 2", "user2");
+ assertTrue(destinationAccessValidator.hasAccess("user2", "/lobby/1"));
+ }
+
+ @Test
+ public void validate_successWhenUserIsMemberOfReferencedLobby() {
+ createLobby("Test Game 1", "user1", "user2");
+ assertTrue(destinationAccessValidator.hasAccess("user2", "/lobby/0"));
+ createLobby("Test Game 2", "user3", "user4");
+ assertTrue(destinationAccessValidator.hasAccess("user4", "/lobby/1"));
+ }
+
+ @Test
+ public void validate_successWhenUserIsOwnerOfReferencedGame() {
+ createGame("Test Game 1", "owner1", "user2", "user3");
+ assertTrue(destinationAccessValidator.hasAccess("owner1", "/game/0"));
+ createGame("Test Game 2", "owner4", "user5", "user6");
+ assertTrue(destinationAccessValidator.hasAccess("owner4", "/game/1"));
+ }
+
+ @Test
+ public void validate_successWhenUserIsMemberOfReferencedGame() {
+ createGame("Test Game 1", "owner1", "user2", "user3");
+ assertTrue(destinationAccessValidator.hasAccess("user2", "/game/0"));
+ createGame("Test Game 2", "owner4", "user5", "user6");
+ assertTrue(destinationAccessValidator.hasAccess("user6", "/game/1"));
+ }
+
+} \ No newline at end of file
bgstack15