diff options
Diffstat (limited to 'backend')
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 Binary files differnew file mode 100644 index 00000000..57bdffcf --- /dev/null +++ b/backend/src/main/resources/static/images/background.jpg diff --git a/backend/src/main/resources/static/images/cards/academy.png b/backend/src/main/resources/static/images/cards/academy.png Binary files differnew file mode 100644 index 00000000..d2a75075 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/academy.png diff --git a/backend/src/main/resources/static/images/cards/age1.png b/backend/src/main/resources/static/images/cards/age1.png Binary files differnew file mode 100644 index 00000000..a06332d7 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/age1.png diff --git a/backend/src/main/resources/static/images/cards/age2.png b/backend/src/main/resources/static/images/cards/age2.png Binary files differnew file mode 100644 index 00000000..9b52aa4e --- /dev/null +++ b/backend/src/main/resources/static/images/cards/age2.png diff --git a/backend/src/main/resources/static/images/cards/age3.png b/backend/src/main/resources/static/images/cards/age3.png Binary files differnew file mode 100644 index 00000000..86c983ee --- /dev/null +++ b/backend/src/main/resources/static/images/cards/age3.png diff --git a/backend/src/main/resources/static/images/cards/altar.png b/backend/src/main/resources/static/images/cards/altar.png Binary files differnew file mode 100644 index 00000000..bbde8f2f --- /dev/null +++ b/backend/src/main/resources/static/images/cards/altar.png diff --git a/backend/src/main/resources/static/images/cards/apothecary.png b/backend/src/main/resources/static/images/cards/apothecary.png Binary files differnew file mode 100644 index 00000000..01804c0a --- /dev/null +++ b/backend/src/main/resources/static/images/cards/apothecary.png diff --git a/backend/src/main/resources/static/images/cards/aqueduct.png b/backend/src/main/resources/static/images/cards/aqueduct.png Binary files differnew file mode 100644 index 00000000..c29d9566 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/aqueduct.png diff --git a/backend/src/main/resources/static/images/cards/archeryrange.png b/backend/src/main/resources/static/images/cards/archeryrange.png Binary files differnew file mode 100644 index 00000000..15c6edda --- /dev/null +++ b/backend/src/main/resources/static/images/cards/archeryrange.png diff --git a/backend/src/main/resources/static/images/cards/arena.png b/backend/src/main/resources/static/images/cards/arena.png Binary files differnew file mode 100644 index 00000000..7dc76961 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/arena.png diff --git a/backend/src/main/resources/static/images/cards/arsenal.png b/backend/src/main/resources/static/images/cards/arsenal.png Binary files differnew file mode 100644 index 00000000..fc3f4a27 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/arsenal.png diff --git a/backend/src/main/resources/static/images/cards/barracks.png b/backend/src/main/resources/static/images/cards/barracks.png Binary files differnew file mode 100644 index 00000000..f5a68c17 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/barracks.png diff --git a/backend/src/main/resources/static/images/cards/baths.png b/backend/src/main/resources/static/images/cards/baths.png Binary files differnew file mode 100644 index 00000000..3d99d59d --- /dev/null +++ b/backend/src/main/resources/static/images/cards/baths.png diff --git a/backend/src/main/resources/static/images/cards/bazar.png b/backend/src/main/resources/static/images/cards/bazar.png Binary files differnew file mode 100644 index 00000000..f36e25c2 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/bazar.png diff --git a/backend/src/main/resources/static/images/cards/brickyard.png b/backend/src/main/resources/static/images/cards/brickyard.png Binary files differnew file mode 100644 index 00000000..ae0b7e9b --- /dev/null +++ b/backend/src/main/resources/static/images/cards/brickyard.png diff --git a/backend/src/main/resources/static/images/cards/buildersguild.png b/backend/src/main/resources/static/images/cards/buildersguild.png Binary files differnew file mode 100644 index 00000000..f5402611 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/buildersguild.png diff --git a/backend/src/main/resources/static/images/cards/caravansery.png b/backend/src/main/resources/static/images/cards/caravansery.png Binary files differnew file mode 100644 index 00000000..997bb102 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/caravansery.png diff --git a/backend/src/main/resources/static/images/cards/chamberofcommerce.png b/backend/src/main/resources/static/images/cards/chamberofcommerce.png Binary files differnew file mode 100644 index 00000000..44b5af28 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/chamberofcommerce.png diff --git a/backend/src/main/resources/static/images/cards/circus.png b/backend/src/main/resources/static/images/cards/circus.png Binary files differnew file mode 100644 index 00000000..b1ec4d8b --- /dev/null +++ b/backend/src/main/resources/static/images/cards/circus.png diff --git a/backend/src/main/resources/static/images/cards/claypit.png b/backend/src/main/resources/static/images/cards/claypit.png Binary files differnew file mode 100644 index 00000000..5442248e --- /dev/null +++ b/backend/src/main/resources/static/images/cards/claypit.png diff --git a/backend/src/main/resources/static/images/cards/claypool.png b/backend/src/main/resources/static/images/cards/claypool.png Binary files differnew file mode 100644 index 00000000..873cad47 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/claypool.png diff --git a/backend/src/main/resources/static/images/cards/courthouse.png b/backend/src/main/resources/static/images/cards/courthouse.png Binary files differnew file mode 100644 index 00000000..394901f2 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/courthouse.png diff --git a/backend/src/main/resources/static/images/cards/craftsmensguild.png b/backend/src/main/resources/static/images/cards/craftsmensguild.png Binary files differnew file mode 100644 index 00000000..09bff60e --- /dev/null +++ b/backend/src/main/resources/static/images/cards/craftsmensguild.png diff --git a/backend/src/main/resources/static/images/cards/dispensary.png b/backend/src/main/resources/static/images/cards/dispensary.png Binary files differnew file mode 100644 index 00000000..4917166b --- /dev/null +++ b/backend/src/main/resources/static/images/cards/dispensary.png diff --git a/backend/src/main/resources/static/images/cards/easttradingpost.png b/backend/src/main/resources/static/images/cards/easttradingpost.png Binary files differnew file mode 100644 index 00000000..0c67cc78 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/easttradingpost.png diff --git a/backend/src/main/resources/static/images/cards/excavation.png b/backend/src/main/resources/static/images/cards/excavation.png Binary files differnew file mode 100644 index 00000000..0fe1b01f --- /dev/null +++ b/backend/src/main/resources/static/images/cards/excavation.png diff --git a/backend/src/main/resources/static/images/cards/forestcave.png b/backend/src/main/resources/static/images/cards/forestcave.png Binary files differnew file mode 100644 index 00000000..262fffc6 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/forestcave.png diff --git a/backend/src/main/resources/static/images/cards/fortifications.png b/backend/src/main/resources/static/images/cards/fortifications.png Binary files differnew file mode 100644 index 00000000..3e113473 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/fortifications.png diff --git a/backend/src/main/resources/static/images/cards/forum.png b/backend/src/main/resources/static/images/cards/forum.png Binary files differnew file mode 100644 index 00000000..d6262158 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/forum.png diff --git a/backend/src/main/resources/static/images/cards/foundry.png b/backend/src/main/resources/static/images/cards/foundry.png Binary files differnew file mode 100644 index 00000000..da95a48e --- /dev/null +++ b/backend/src/main/resources/static/images/cards/foundry.png diff --git a/backend/src/main/resources/static/images/cards/gardens.png b/backend/src/main/resources/static/images/cards/gardens.png Binary files differnew file mode 100644 index 00000000..9a49a0ad --- /dev/null +++ b/backend/src/main/resources/static/images/cards/gardens.png diff --git a/backend/src/main/resources/static/images/cards/glassworks.png b/backend/src/main/resources/static/images/cards/glassworks.png Binary files differnew file mode 100644 index 00000000..285d7d54 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/glassworks.png diff --git a/backend/src/main/resources/static/images/cards/guardtower.png b/backend/src/main/resources/static/images/cards/guardtower.png Binary files differnew file mode 100644 index 00000000..524b06f3 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/guardtower.png diff --git a/backend/src/main/resources/static/images/cards/haven.png b/backend/src/main/resources/static/images/cards/haven.png Binary files differnew file mode 100644 index 00000000..e0b345b2 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/haven.png diff --git a/backend/src/main/resources/static/images/cards/laboratory.png b/backend/src/main/resources/static/images/cards/laboratory.png Binary files differnew file mode 100644 index 00000000..4c29e81f --- /dev/null +++ b/backend/src/main/resources/static/images/cards/laboratory.png diff --git a/backend/src/main/resources/static/images/cards/library.png b/backend/src/main/resources/static/images/cards/library.png Binary files differnew file mode 100644 index 00000000..7495a2ca --- /dev/null +++ b/backend/src/main/resources/static/images/cards/library.png diff --git a/backend/src/main/resources/static/images/cards/lighthouse.png b/backend/src/main/resources/static/images/cards/lighthouse.png Binary files differnew file mode 100644 index 00000000..2124811b --- /dev/null +++ b/backend/src/main/resources/static/images/cards/lighthouse.png diff --git a/backend/src/main/resources/static/images/cards/lodge.png b/backend/src/main/resources/static/images/cards/lodge.png Binary files differnew file mode 100644 index 00000000..22758688 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/lodge.png diff --git a/backend/src/main/resources/static/images/cards/loom.png b/backend/src/main/resources/static/images/cards/loom.png Binary files differnew file mode 100644 index 00000000..70bdf375 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/loom.png diff --git a/backend/src/main/resources/static/images/cards/lumberyard.png b/backend/src/main/resources/static/images/cards/lumberyard.png Binary files differnew file mode 100644 index 00000000..8558af1a --- /dev/null +++ b/backend/src/main/resources/static/images/cards/lumberyard.png diff --git a/backend/src/main/resources/static/images/cards/magistratesguild.png b/backend/src/main/resources/static/images/cards/magistratesguild.png Binary files differnew file mode 100644 index 00000000..d7deabb3 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/magistratesguild.png diff --git a/backend/src/main/resources/static/images/cards/marketplace.png b/backend/src/main/resources/static/images/cards/marketplace.png Binary files differnew file mode 100644 index 00000000..cd3676d4 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/marketplace.png diff --git a/backend/src/main/resources/static/images/cards/mine.png b/backend/src/main/resources/static/images/cards/mine.png Binary files differnew file mode 100644 index 00000000..4062775c --- /dev/null +++ b/backend/src/main/resources/static/images/cards/mine.png diff --git a/backend/src/main/resources/static/images/cards/observatory.png b/backend/src/main/resources/static/images/cards/observatory.png Binary files differnew file mode 100644 index 00000000..1da3d7b4 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/observatory.png diff --git a/backend/src/main/resources/static/images/cards/orevein.png b/backend/src/main/resources/static/images/cards/orevein.png Binary files differnew file mode 100644 index 00000000..fabea674 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/orevein.png diff --git a/backend/src/main/resources/static/images/cards/palace.png b/backend/src/main/resources/static/images/cards/palace.png Binary files differnew file mode 100644 index 00000000..1a24890e --- /dev/null +++ b/backend/src/main/resources/static/images/cards/palace.png diff --git a/backend/src/main/resources/static/images/cards/pantheon.png b/backend/src/main/resources/static/images/cards/pantheon.png Binary files differnew file mode 100644 index 00000000..264bae02 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/pantheon.png diff --git a/backend/src/main/resources/static/images/cards/pawnshop.png b/backend/src/main/resources/static/images/cards/pawnshop.png Binary files differnew file mode 100644 index 00000000..30bb3807 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/pawnshop.png diff --git a/backend/src/main/resources/static/images/cards/philosophersguild.png b/backend/src/main/resources/static/images/cards/philosophersguild.png Binary files differnew file mode 100644 index 00000000..f72590f6 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/philosophersguild.png diff --git a/backend/src/main/resources/static/images/cards/press.png b/backend/src/main/resources/static/images/cards/press.png Binary files differnew file mode 100644 index 00000000..c932df06 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/press.png diff --git a/backend/src/main/resources/static/images/cards/quarry.png b/backend/src/main/resources/static/images/cards/quarry.png Binary files differnew file mode 100644 index 00000000..8cdbdb22 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/quarry.png diff --git a/backend/src/main/resources/static/images/cards/sawmill.png b/backend/src/main/resources/static/images/cards/sawmill.png Binary files differnew file mode 100644 index 00000000..5abff473 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/sawmill.png diff --git a/backend/src/main/resources/static/images/cards/school.png b/backend/src/main/resources/static/images/cards/school.png Binary files differnew file mode 100644 index 00000000..ab2218d0 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/school.png diff --git a/backend/src/main/resources/static/images/cards/scientistsguild.png b/backend/src/main/resources/static/images/cards/scientistsguild.png Binary files differnew file mode 100644 index 00000000..7ee639e3 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/scientistsguild.png diff --git a/backend/src/main/resources/static/images/cards/scriptorium.png b/backend/src/main/resources/static/images/cards/scriptorium.png Binary files differnew file mode 100644 index 00000000..36dca27a --- /dev/null +++ b/backend/src/main/resources/static/images/cards/scriptorium.png diff --git a/backend/src/main/resources/static/images/cards/senate.png b/backend/src/main/resources/static/images/cards/senate.png Binary files differnew file mode 100644 index 00000000..ee878ea6 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/senate.png diff --git a/backend/src/main/resources/static/images/cards/shipownersguild.png b/backend/src/main/resources/static/images/cards/shipownersguild.png Binary files differnew file mode 100644 index 00000000..3eecd2da --- /dev/null +++ b/backend/src/main/resources/static/images/cards/shipownersguild.png diff --git a/backend/src/main/resources/static/images/cards/siegeworkshop.png b/backend/src/main/resources/static/images/cards/siegeworkshop.png Binary files differnew file mode 100644 index 00000000..bacf8309 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/siegeworkshop.png diff --git a/backend/src/main/resources/static/images/cards/spiesguild.png b/backend/src/main/resources/static/images/cards/spiesguild.png Binary files differnew file mode 100644 index 00000000..85e28d9e --- /dev/null +++ b/backend/src/main/resources/static/images/cards/spiesguild.png diff --git a/backend/src/main/resources/static/images/cards/stables.png b/backend/src/main/resources/static/images/cards/stables.png Binary files differnew file mode 100644 index 00000000..48c963f0 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/stables.png diff --git a/backend/src/main/resources/static/images/cards/statue.png b/backend/src/main/resources/static/images/cards/statue.png Binary files differnew file mode 100644 index 00000000..55aaa5cb --- /dev/null +++ b/backend/src/main/resources/static/images/cards/statue.png diff --git a/backend/src/main/resources/static/images/cards/stockade.png b/backend/src/main/resources/static/images/cards/stockade.png Binary files differnew file mode 100644 index 00000000..37741429 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/stockade.png diff --git a/backend/src/main/resources/static/images/cards/stonepit.png b/backend/src/main/resources/static/images/cards/stonepit.png Binary files differnew file mode 100644 index 00000000..724900c7 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/stonepit.png diff --git a/backend/src/main/resources/static/images/cards/strategistsguild.png b/backend/src/main/resources/static/images/cards/strategistsguild.png Binary files differnew file mode 100644 index 00000000..ae186a4b --- /dev/null +++ b/backend/src/main/resources/static/images/cards/strategistsguild.png diff --git a/backend/src/main/resources/static/images/cards/study.png b/backend/src/main/resources/static/images/cards/study.png Binary files differnew file mode 100644 index 00000000..d8b9ebf9 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/study.png diff --git a/backend/src/main/resources/static/images/cards/tavern.png b/backend/src/main/resources/static/images/cards/tavern.png Binary files differnew file mode 100644 index 00000000..418b0fb2 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/tavern.png diff --git a/backend/src/main/resources/static/images/cards/temple.png b/backend/src/main/resources/static/images/cards/temple.png Binary files differnew file mode 100644 index 00000000..9a8d89dc --- /dev/null +++ b/backend/src/main/resources/static/images/cards/temple.png diff --git a/backend/src/main/resources/static/images/cards/theater.png b/backend/src/main/resources/static/images/cards/theater.png Binary files differnew file mode 100644 index 00000000..0d5b2b01 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/theater.png diff --git a/backend/src/main/resources/static/images/cards/timberyard.png b/backend/src/main/resources/static/images/cards/timberyard.png Binary files differnew file mode 100644 index 00000000..0f20547f --- /dev/null +++ b/backend/src/main/resources/static/images/cards/timberyard.png diff --git a/backend/src/main/resources/static/images/cards/townhall.png b/backend/src/main/resources/static/images/cards/townhall.png Binary files differnew file mode 100644 index 00000000..d0638739 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/townhall.png diff --git a/backend/src/main/resources/static/images/cards/tradersguild.png b/backend/src/main/resources/static/images/cards/tradersguild.png Binary files differnew file mode 100644 index 00000000..15777e77 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/tradersguild.png diff --git a/backend/src/main/resources/static/images/cards/trainingground.png b/backend/src/main/resources/static/images/cards/trainingground.png Binary files differnew file mode 100644 index 00000000..d59ef4f8 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/trainingground.png diff --git a/backend/src/main/resources/static/images/cards/treefarm.png b/backend/src/main/resources/static/images/cards/treefarm.png Binary files differnew file mode 100644 index 00000000..18cf228f --- /dev/null +++ b/backend/src/main/resources/static/images/cards/treefarm.png diff --git a/backend/src/main/resources/static/images/cards/university.png b/backend/src/main/resources/static/images/cards/university.png Binary files differnew file mode 100644 index 00000000..c9ca8a80 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/university.png diff --git a/backend/src/main/resources/static/images/cards/vineyard.png b/backend/src/main/resources/static/images/cards/vineyard.png Binary files differnew file mode 100644 index 00000000..58fa8ee1 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/vineyard.png diff --git a/backend/src/main/resources/static/images/cards/walls.png b/backend/src/main/resources/static/images/cards/walls.png Binary files differnew file mode 100644 index 00000000..3823c62f --- /dev/null +++ b/backend/src/main/resources/static/images/cards/walls.png diff --git a/backend/src/main/resources/static/images/cards/westtradingpost.png b/backend/src/main/resources/static/images/cards/westtradingpost.png Binary files differnew file mode 100644 index 00000000..b536269f --- /dev/null +++ b/backend/src/main/resources/static/images/cards/westtradingpost.png diff --git a/backend/src/main/resources/static/images/cards/workersguild.png b/backend/src/main/resources/static/images/cards/workersguild.png Binary files differnew file mode 100644 index 00000000..de4f452f --- /dev/null +++ b/backend/src/main/resources/static/images/cards/workersguild.png diff --git a/backend/src/main/resources/static/images/cards/workshop.png b/backend/src/main/resources/static/images/cards/workshop.png Binary files differnew file mode 100644 index 00000000..8f585d61 --- /dev/null +++ b/backend/src/main/resources/static/images/cards/workshop.png diff --git a/backend/src/main/resources/static/images/tokens/buy.png b/backend/src/main/resources/static/images/tokens/buy.png Binary files differnew file mode 100644 index 00000000..07af65a3 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/buy.png diff --git a/backend/src/main/resources/static/images/tokens/card.png b/backend/src/main/resources/static/images/tokens/card.png Binary files differnew file mode 100644 index 00000000..fcdbc068 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/card.png diff --git a/backend/src/main/resources/static/images/tokens/check.png b/backend/src/main/resources/static/images/tokens/check.png Binary files differnew file mode 100644 index 00000000..98db5be0 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/check.png diff --git a/backend/src/main/resources/static/images/tokens/clay.png b/backend/src/main/resources/static/images/tokens/clay.png Binary files differnew file mode 100644 index 00000000..72fc0b0e --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/clay.png diff --git a/backend/src/main/resources/static/images/tokens/coin.png b/backend/src/main/resources/static/images/tokens/coin.png Binary files differnew file mode 100644 index 00000000..f4813042 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/coin.png diff --git a/backend/src/main/resources/static/images/tokens/coin1.png b/backend/src/main/resources/static/images/tokens/coin1.png Binary files differnew file mode 100644 index 00000000..dd57e5f0 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/coin1.png diff --git a/backend/src/main/resources/static/images/tokens/coin3.png b/backend/src/main/resources/static/images/tokens/coin3.png Binary files differnew file mode 100644 index 00000000..546d41b6 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/coin3.png diff --git a/backend/src/main/resources/static/images/tokens/free.png b/backend/src/main/resources/static/images/tokens/free.png Binary files differnew file mode 100644 index 00000000..1c8d0782 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/free.png diff --git a/backend/src/main/resources/static/images/tokens/glass.png b/backend/src/main/resources/static/images/tokens/glass.png Binary files differnew file mode 100644 index 00000000..61fd2be5 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/glass.png diff --git a/backend/src/main/resources/static/images/tokens/linen.png b/backend/src/main/resources/static/images/tokens/linen.png Binary files differnew file mode 100644 index 00000000..294adcb2 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/linen.png diff --git a/backend/src/main/resources/static/images/tokens/no.png b/backend/src/main/resources/static/images/tokens/no.png Binary files differnew file mode 100644 index 00000000..78d09fea --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/no.png diff --git a/backend/src/main/resources/static/images/tokens/ore.png b/backend/src/main/resources/static/images/tokens/ore.png Binary files differnew file mode 100644 index 00000000..c2149daa --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/ore.png diff --git a/backend/src/main/resources/static/images/tokens/paper.png b/backend/src/main/resources/static/images/tokens/paper.png Binary files differnew file mode 100644 index 00000000..91a59221 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/paper.png diff --git a/backend/src/main/resources/static/images/tokens/pyramid-stage0.png b/backend/src/main/resources/static/images/tokens/pyramid-stage0.png Binary files differnew file mode 100644 index 00000000..b6a3977f --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/pyramid-stage0.png diff --git a/backend/src/main/resources/static/images/tokens/pyramid-stage1.png b/backend/src/main/resources/static/images/tokens/pyramid-stage1.png Binary files differnew file mode 100644 index 00000000..ead4a34e --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/pyramid-stage1.png diff --git a/backend/src/main/resources/static/images/tokens/pyramid-stage2.png b/backend/src/main/resources/static/images/tokens/pyramid-stage2.png Binary files differnew file mode 100644 index 00000000..7239a3a4 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/pyramid-stage2.png diff --git a/backend/src/main/resources/static/images/tokens/pyramid-stage3.png b/backend/src/main/resources/static/images/tokens/pyramid-stage3.png Binary files differnew file mode 100644 index 00000000..cab9912b --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/pyramid-stage3.png diff --git a/backend/src/main/resources/static/images/tokens/pyramid.png b/backend/src/main/resources/static/images/tokens/pyramid.png Binary files differnew file mode 100644 index 00000000..074247da --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/pyramid.png diff --git a/backend/src/main/resources/static/images/tokens/stone.png b/backend/src/main/resources/static/images/tokens/stone.png Binary files differnew file mode 100644 index 00000000..674c40db --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/stone.png diff --git a/backend/src/main/resources/static/images/tokens/trash.png b/backend/src/main/resources/static/images/tokens/trash.png Binary files differnew file mode 100644 index 00000000..086df817 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/trash.png diff --git a/backend/src/main/resources/static/images/tokens/victory1.png b/backend/src/main/resources/static/images/tokens/victory1.png Binary files differnew file mode 100644 index 00000000..6b9aff29 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/victory1.png diff --git a/backend/src/main/resources/static/images/tokens/victory3.png b/backend/src/main/resources/static/images/tokens/victory3.png Binary files differnew file mode 100644 index 00000000..474cb30c --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/victory3.png diff --git a/backend/src/main/resources/static/images/tokens/victory5.png b/backend/src/main/resources/static/images/tokens/victory5.png Binary files differnew file mode 100644 index 00000000..ad042119 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/victory5.png diff --git a/backend/src/main/resources/static/images/tokens/victoryminus1.png b/backend/src/main/resources/static/images/tokens/victoryminus1.png Binary files differnew file mode 100644 index 00000000..00a615c7 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/victoryminus1.png diff --git a/backend/src/main/resources/static/images/tokens/wood.png b/backend/src/main/resources/static/images/tokens/wood.png Binary files differnew file mode 100644 index 00000000..09a4ede8 --- /dev/null +++ b/backend/src/main/resources/static/images/tokens/wood.png diff --git a/backend/src/main/resources/static/images/wonders/alexandriaA.png b/backend/src/main/resources/static/images/wonders/alexandriaA.png Binary files differnew file mode 100644 index 00000000..416d534e --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/alexandriaA.png diff --git a/backend/src/main/resources/static/images/wonders/alexandriaB.png b/backend/src/main/resources/static/images/wonders/alexandriaB.png Binary files differnew file mode 100644 index 00000000..205a5256 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/alexandriaB.png diff --git a/backend/src/main/resources/static/images/wonders/babylonA.png b/backend/src/main/resources/static/images/wonders/babylonA.png Binary files differnew file mode 100644 index 00000000..f8e3725e --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/babylonA.png diff --git a/backend/src/main/resources/static/images/wonders/babylonB.png b/backend/src/main/resources/static/images/wonders/babylonB.png Binary files differnew file mode 100644 index 00000000..53f6f045 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/babylonB.png diff --git a/backend/src/main/resources/static/images/wonders/ephesosA.png b/backend/src/main/resources/static/images/wonders/ephesosA.png Binary files differnew file mode 100644 index 00000000..285c8edf --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/ephesosA.png diff --git a/backend/src/main/resources/static/images/wonders/ephesosB.png b/backend/src/main/resources/static/images/wonders/ephesosB.png Binary files differnew file mode 100644 index 00000000..1e0e2541 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/ephesosB.png diff --git a/backend/src/main/resources/static/images/wonders/extra/agrigentoA.jpg b/backend/src/main/resources/static/images/wonders/extra/agrigentoA.jpg Binary files differnew file mode 100644 index 00000000..76ba8195 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/extra/agrigentoA.jpg diff --git a/backend/src/main/resources/static/images/wonders/extra/angkorwatA.jpg b/backend/src/main/resources/static/images/wonders/extra/angkorwatA.jpg Binary files differnew file mode 100644 index 00000000..32f52514 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/extra/angkorwatA.jpg diff --git a/backend/src/main/resources/static/images/wonders/extra/angkorwatB.jpg b/backend/src/main/resources/static/images/wonders/extra/angkorwatB.jpg Binary files differnew file mode 100644 index 00000000..c3f4304e --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/extra/angkorwatB.jpg diff --git a/backend/src/main/resources/static/images/wonders/extra/avalonA.jpg b/backend/src/main/resources/static/images/wonders/extra/avalonA.jpg Binary files differnew file mode 100644 index 00000000..7f7f0678 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/extra/avalonA.jpg diff --git a/backend/src/main/resources/static/images/wonders/extra/ctesiphonB.jpg b/backend/src/main/resources/static/images/wonders/extra/ctesiphonB.jpg Binary files differnew file mode 100644 index 00000000..c00b40ac --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/extra/ctesiphonB.jpg diff --git a/backend/src/main/resources/static/images/wonders/extra/iramA.jpg b/backend/src/main/resources/static/images/wonders/extra/iramA.jpg Binary files differnew file mode 100644 index 00000000..d2c24e95 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/extra/iramA.jpg diff --git a/backend/src/main/resources/static/images/wonders/extra/persepolisA.jpg b/backend/src/main/resources/static/images/wonders/extra/persepolisA.jpg Binary files differnew file mode 100644 index 00000000..2caa4f89 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/extra/persepolisA.jpg diff --git a/backend/src/main/resources/static/images/wonders/extra/romaA.jpg b/backend/src/main/resources/static/images/wonders/extra/romaA.jpg Binary files differnew file mode 100644 index 00000000..c54bc820 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/extra/romaA.jpg 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 Binary files differnew file mode 100644 index 00000000..1c5dad97 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/extra/sangri-laA.jpg diff --git a/backend/src/main/resources/static/images/wonders/extra/spahanA.jpg b/backend/src/main/resources/static/images/wonders/extra/spahanA.jpg Binary files differnew file mode 100644 index 00000000..ab2cfc84 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/extra/spahanA.jpg 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 Binary files differnew file mode 100644 index 00000000..4aacd39b --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/extra/the-great-wallA.jpg diff --git a/backend/src/main/resources/static/images/wonders/extra/veniseA.jpg b/backend/src/main/resources/static/images/wonders/extra/veniseA.jpg Binary files differnew file mode 100644 index 00000000..55ec00b5 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/extra/veniseA.jpg diff --git a/backend/src/main/resources/static/images/wonders/extra/veniseB.jpg b/backend/src/main/resources/static/images/wonders/extra/veniseB.jpg Binary files differnew file mode 100644 index 00000000..e18f3a12 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/extra/veniseB.jpg diff --git a/backend/src/main/resources/static/images/wonders/gizahA.png b/backend/src/main/resources/static/images/wonders/gizahA.png Binary files differnew file mode 100644 index 00000000..5e755594 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/gizahA.png diff --git a/backend/src/main/resources/static/images/wonders/gizahB.png b/backend/src/main/resources/static/images/wonders/gizahB.png Binary files differnew file mode 100644 index 00000000..60b90fed --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/gizahB.png diff --git a/backend/src/main/resources/static/images/wonders/halikarnassusA.png b/backend/src/main/resources/static/images/wonders/halikarnassusA.png Binary files differnew file mode 100644 index 00000000..5e6acc36 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/halikarnassusA.png diff --git a/backend/src/main/resources/static/images/wonders/halikarnassusB.png b/backend/src/main/resources/static/images/wonders/halikarnassusB.png Binary files differnew file mode 100644 index 00000000..42d67786 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/halikarnassusB.png diff --git a/backend/src/main/resources/static/images/wonders/olympiaA.png b/backend/src/main/resources/static/images/wonders/olympiaA.png Binary files differnew file mode 100644 index 00000000..315c090b --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/olympiaA.png diff --git a/backend/src/main/resources/static/images/wonders/olympiaB.png b/backend/src/main/resources/static/images/wonders/olympiaB.png Binary files differnew file mode 100644 index 00000000..a6c81af6 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/olympiaB.png diff --git a/backend/src/main/resources/static/images/wonders/rhodosA.png b/backend/src/main/resources/static/images/wonders/rhodosA.png Binary files differnew file mode 100644 index 00000000..13ea69e1 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/rhodosA.png diff --git a/backend/src/main/resources/static/images/wonders/rhodosB.png b/backend/src/main/resources/static/images/wonders/rhodosB.png Binary files differnew file mode 100644 index 00000000..2cfa4e18 --- /dev/null +++ b/backend/src/main/resources/static/images/wonders/rhodosB.png 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 |