package uulm.teamname.marvelous.gamelibrary.gamelogic; import uulm.teamname.marvelous.gamelibrary.IntVector2; import uulm.teamname.marvelous.gamelibrary.entities.*; import uulm.teamname.marvelous.gamelibrary.entities.Character; import uulm.teamname.marvelous.gamelibrary.events.*; import uulm.teamname.marvelous.gamelibrary.events.Event; import uulm.teamname.marvelous.gamelibrary.requests.CharacterRequest; import uulm.teamname.marvelous.gamelibrary.requests.Request; import uulm.teamname.marvelous.gamelibrary.requests.RequestType; import java.awt.*; import java.awt.geom.Line2D; import java.util.*; /** Contains game logic handling. */ class GameLogic { private static final Random rand = new Random(); /** * Produces resulting {@link Event}s from a given {@link Request}. * @param state The game state to execute on * @param request The request to execute * @return The list of resulting events */ public static ArrayList executeRequest(GameState state, Request request) { ArrayList result = new ArrayList<>(); switch(request.type) { case MeleeAttackRequest, RangedAttackRequest -> { CharacterRequest data = (CharacterRequest)request; result.add(new EventBuilder(request.type == RequestType.MeleeAttackRequest ? EventType.MeleeAttackEvent : EventType.RangedAttackEvent) .withOriginEntity(data.originEntity) .withTargetEntity(data.targetEntity) .withOriginField(data.originField) .withTargetField(data.targetField) .withAmount(data.value) .buildCharacterEvent()); result.add(new EventBuilder(EventType.ConsumedAPEvent) .withTargetEntity(data.originEntity) .withTargetField(data.originField) .withAmount(1) .buildEntityEvent()); result.add(new EventBuilder(EventType.TakenDamageEvent) .withTargetEntity(data.targetEntity) .withTargetField(data.targetField) .withAmount(data.value) .buildEntityEvent()); } case MoveRequest -> { CharacterRequest data = (CharacterRequest)request; result.add(new EventBuilder(EventType.MoveEvent) .withOriginEntity(data.originEntity) .withOriginField(data.originField) .withTargetField(data.targetField) .buildCharacterEvent()); result.add(new EventBuilder(EventType.ConsumedMPEvent) .withTargetEntity(data.originEntity) .withTargetField(data.targetField) //when this event gets handled, the character already moved to the target field .withAmount(1) .buildEntityEvent()); for(Entity entity: state.entities.findByPosition(data.targetField)) { if(entity instanceof Character) { result.add(new EventBuilder(EventType.MoveEvent) .withOriginEntity(entity.id) .withOriginField(data.targetField) .withTargetField(data.originField) .buildCharacterEvent()); break; //we should only have one character per field anyways } } } case ExchangeInfinityStoneRequest, UseInfinityStoneRequest -> { CharacterRequest data = (CharacterRequest)request; result.add(new EventBuilder(request.type == RequestType.ExchangeInfinityStoneRequest ? EventType.ExchangeInfinityStoneEvent : EventType.UseInfinityStoneEvent) .withOriginEntity(data.originEntity) .withOriginField(data.originField) .withTargetEntity(data.targetEntity) .withTargetField(data.targetField) .withStoneType(data.stoneType) .buildCharacterEvent()); result.add(new EventBuilder(EventType.ConsumedAPEvent) .withTargetEntity(data.originEntity) .withTargetField(data.originField) .withAmount(1) .buildEntityEvent()); if(request.type == RequestType.UseInfinityStoneRequest) { switch(((CharacterRequest) request).stoneType) { case SpaceStone -> { result.add(new EventBuilder(EventType.MoveEvent) .withOriginEntity(data.originEntity) .withOriginField(data.originField) .withTargetField(data.targetField) .buildCharacterEvent()); } case MindStone -> { EntityType target = data.originEntity.type == EntityType.P1 ? EntityType.P2 : EntityType.P1; Line2D line = new Line2D.Float(data.originField.getX(), data.originField.getY(), data.targetField.getX(), data.targetField.getY()); for(int i = data.originField.getX(); i <= data.targetField.getX(); i++) { for(int j = data.originField.getY(); j <= data.targetField.getY(); j++) { var cell = new Rectangle.Float(i - 0.5f, j - 0.5f, 1, 1); if(line.intersects(cell)) { for(Entity entity: state.entities.findByPosition(data.targetField)) { if(entity.id.isSameType(target)) { result.add(new EventBuilder(EventType.TakenDamageEvent) .withTargetEntity(entity.id) .withTargetField(new IntVector2(i, j)) .withAmount(data.value) .buildEntityEvent()); } } } } } } case RealityStone -> { if(data.originEntity == data.targetEntity) { // => place stone //TODO: use config values result.add(new EventBuilder(EventType.SpawnEntityEvent) .withTargetField(data.targetField) .withEntity(new Rock(new EntityID(EntityType.Rocks, state.entities.findFreeRockSlot()), data.targetField, 100)) .buildEntityEvent()); }else { // => destroy stone result.add(new EventBuilder(EventType.DestroyedEntityEvent) .withTargetField(data.targetField) .withTargetEntity(data.targetEntity) .buildEntityEvent()); } } case PowerStone -> { Character origin = (Character)state.entities.findEntity(data.originEntity); int dmg = (int)Math.round(origin.hp.getValue() * 0.1); if(origin.hp.getValue() != 1 && dmg > 0) { result.add(new EventBuilder(EventType.TakenDamageEvent) .withTargetEntity(data.originEntity) .withTargetField(data.originField) .withAmount(dmg) .buildEntityEvent()); } result.add(new EventBuilder(EventType.TakenDamageEvent) .withTargetEntity(data.targetEntity) .withTargetField(data.targetField) .withAmount(data.value) .buildEntityEvent()); } case TimeStone -> { Character origin = (Character)state.entities.findEntity(data.originEntity); int ap = origin.ap.max - origin.ap.getValue(); if(ap < 0) { result.add(new EventBuilder(EventType.ConsumedAPEvent) .withTargetEntity(data.originEntity) .withTargetField(data.originField) .withAmount(ap) .buildEntityEvent()); } int mp = origin.mp.max - origin.mp.getValue(); if(mp < 0) { result.add(new EventBuilder(EventType.ConsumedMPEvent) .withTargetEntity(data.originEntity) .withTargetField(data.originField) .withAmount(mp) .buildEntityEvent()); } } case SoulStone -> { Character target = (Character)state.entities.findEntity(data.targetEntity); result.add(new EventBuilder(EventType.HealedEvent) .withTargetEntity(data.targetEntity) .withTargetField(data.targetField) .withAmount(target.hp.max) .buildEntityEvent()); } } } } case EndRoundRequest -> { result.addAll(handleTurnEnd(state)); //why is it called end round request when it ends a turn... } case Req -> { result.add(buildGameStateEvent(state)); } } return result; } /** * Builds a {@link EventType#GamestateEvent} for the given {@link GameState}. * @param state The game state to use * @return The resulting event */ public static Event buildGameStateEvent(GameState state) { return new EventBuilder(EventType.GamestateEvent) .withEntities(state.entities.export()) .withTurnOrder((EntityID[])state.turnOrder.toArray()) .withMapSize(state.mapSize) .withActiveCharacter(state.activeCharacter) .withStoneCooldowns(state.stoneCooldown.export()) .withWinCondition(state.won) .buildGameStateEvent(); } /** * Checks a {@link Request} for validity for a {@link GameState}. * @param state The game state to check on * @param request The request to validate * @return Whether or not the request is valid */ public static boolean checkRequest(GameState state, Request request) { try { switch(request.type) { case MeleeAttackRequest, RangedAttackRequest -> { CharacterRequest data = (CharacterRequest)request; Character origin = getCharacter(state, data.originField, data.originEntity); Character target = getCharacter(state, data.targetField, data.targetEntity); requireAlive(origin); requireAlive(target); requireAP(origin, 1); if(request.type == RequestType.MeleeAttackRequest) { if(origin.meleeDamage != data.value) { throw new InvalidRequestException(); } if(data.originField.distanceChebyshev(data.targetField) > 1) { throw new InvalidRequestException(); } }else if(request.type == RequestType.RangedAttackRequest) { if(origin.rangedDamage != data.value) { throw new InvalidRequestException(); } if(data.originField.distanceChebyshev(data.targetField) > origin.attackRange) { throw new InvalidRequestException(); } if(data.originField.distanceChebyshev(data.targetField) <= 1) { throw new InvalidRequestException(); } requireLineOfSight(state, data.originField, data.targetField); } return true; } case MoveRequest -> { CharacterRequest data = (CharacterRequest)request; Character origin = getCharacter(state, data.originField, data.originEntity); requireAlive(origin); requireMP(origin, 1); verifyCoordinates(state, data.targetField); if(data.originField.distanceChebyshev(data.targetField) != 1) { throw new InvalidRequestException(); } if(state.entities.blocksMovement(data.targetField)) { throw new InvalidRequestException(); } return true; } case ExchangeInfinityStoneRequest -> { CharacterRequest data = (CharacterRequest)request; Character origin = getCharacter(state, data.originField, data.originEntity); Character target = getCharacter(state, data.targetField, data.targetEntity); requireAlive(origin); requireAlive(target); requireAP(origin, 1); requireInfinityStone(origin, data.stoneType); if(data.originField.distanceChebyshev(data.targetField) != 1) { throw new InvalidRequestException(); } return true; } case UseInfinityStoneRequest -> { CharacterRequest data = (CharacterRequest)request; Character origin = getCharacter(state, data.originField, data.originEntity); requireAlive(origin); requireAP(origin, 1); requireInfinityStone(origin, data.stoneType); if(state.stoneCooldown.onCooldown(data.stoneType)) { throw new InvalidRequestException(); } switch(((CharacterRequest) request).stoneType) { case SpaceStone -> { verifyCoordinates(state, data.targetField); if(state.entities.blocksMovement(data.targetField)) { throw new InvalidRequestException(); } } case MindStone -> { if(data.originField == data.targetField) { throw new InvalidRequestException(); } //TODO: mind stone damage check (config) ??????? } case RealityStone -> { if(data.originEntity == data.targetEntity) { // => place stone if(state.entities.findByPosition(data.targetField).size() != 0) { throw new InvalidRequestException(); } }else { // => destroy stone boolean hasRock = false; for(Entity entity: state.entities.findByPosition(data.targetField)) { if(entity.id.isSameType(EntityType.Rocks)) { hasRock = true; break; } } if(!hasRock) { throw new InvalidRequestException(); } } } case PowerStone -> { Character target = getCharacter(state, data.targetField, data.targetEntity); requireAlive(target); if(origin.rangedDamage * 2 != data.value) { throw new InvalidRequestException(); } } case TimeStone -> { // "👍 i approve" - the server } case SoulStone -> { Character target = getCharacter(state, data.targetField, data.targetEntity); if(target.hp.getValue() != 0) { throw new InvalidRequestException(); } } } return true; } case Req -> { return true; } } }catch(Exception ignored) { return false; } return false; } /** * Retrieves a {@link Character} for a {@link Request}. * @param state The game state to use * @param position The requested position * @param entityID The requested {@link EntityID} * @return The found character * @throws InvalidRequestException if the character is invalid or not found */ private static Character getCharacter(GameState state, IntVector2 position, EntityID entityID) throws InvalidRequestException { Entity entity = state.entities.findEntity(entityID); if(entity == null || entity.getPosition() != position || !(entity instanceof Character)) { throw new InvalidRequestException(); } try { return (Character)entity; }catch(Exception ignored) { throw new InvalidRequestException(); } } /** * Verifies that a {@link Character} is alive. */ private static void requireAlive(Character entity) throws InvalidRequestException { if(entity.hp.getValue() <= 0 || !entity.isActive()) { throw new InvalidRequestException(); } } /** * Verifies that a {@link Character} has enough {@link StatType#AP}. */ private static void requireAP(Character entity, int ap) throws InvalidRequestException { if(entity.ap.getValue() < ap) { throw new InvalidRequestException(); } } /** * Verifies that a {@link Character} has enough {@link StatType#MP}. */ private static void requireMP(Character entity, int mp) throws InvalidRequestException { if(entity.mp.getValue() < mp) { throw new InvalidRequestException(); } } /** * Verifies that a {@link Character} has the required {@link StoneType}. */ private static void requireInfinityStone(Character entity, StoneType stone) throws InvalidRequestException { if(stone == null || !entity.inventory.hasStone(stone)) { throw new InvalidRequestException(); } } /** * Verifies that coordinates are within the playing area. */ private static void verifyCoordinates(GameState state, IntVector2 position) throws InvalidRequestException { if(position.getX() < 0 || position.getX() >= state.mapSize.getX() || position.getY() < 0 || position.getY() >= state.mapSize.getY()) { throw new InvalidRequestException(); } } /** * Verifies that there is a line of sight between two positions. */ private static void requireLineOfSight(GameState state, IntVector2 start, IntVector2 end) throws InvalidRequestException { if(!checkLineOfSight(state, start, end)) { throw new InvalidRequestException(); } } /** * Checks if a line of sight exists between the two positions * @param state The game state to work on * @param start The first position * @param end The second position * @return Whether or not the light of sight exists */ private static boolean checkLineOfSight(GameState state, IntVector2 start, IntVector2 end) { //naive code for the win!!! \o/ //at least its early exit and probably only O(ln(n*m)) //TODO: implement proper line rasterization algorithm in GameLogic.checkLineOfSight Line2D line = new Line2D.Float(start.getX(), start.getY(), end.getX(), end.getY()); for(int i = start.getX(); i <= end.getX(); i++) { for(int j = start.getY(); j <= end.getY(); j++) { var cell = new Rectangle.Float(i - 0.5f, j - 0.5f, 1, 1); if(line.intersects(cell)) { if(state.entities.blocksVision(new IntVector2(i, j))) { return false; } } } } return true; } /** * Finds free neighbour options from a starting field. * @param state The game state to work on * @param start The starting position * @return A list of free neighbour field options */ private static ArrayList getFreeNeighbour(GameState state, IntVector2 start) { ArrayList options = new ArrayList<>(); if(start.getX() < 0 || start.getX() >= state.mapSize.getX() || start.getY() < 0 || start.getY() >= state.mapSize.getY()) { return options; } for(IntVector2 dir: IntVector2.CardinalDirections) { if(state.entities.findByPosition(start.add(dir)).size() == 0) { options.add(start.add(dir)); } } if(options.size() == 0) { return getFreeNeighbour(state, start.add(IntVector2.CardinalDirections[rand.nextInt(IntVector2.CardinalDirections.length)])); }else { return options; } } /** * Applies an {@link Event} to a {@link GameState}. * @param state The game state to apply to * @param event The event to apply */ public static void applyEvent(GameState state, Event event) { switch(event.type) { case DestroyedEntityEvent -> { state.entities.removeEntity(((EntityEvent)event).targetEntity); } case TakenDamageEvent -> { Character target = (Character)state.entities.findEntity(((CharacterEvent)event).targetEntity); target.hp.decreaseValue(((CharacterEvent)event).amount); EntityType opposing = target.id.type == EntityType.P1 ? EntityType.P2 : EntityType.P1; state.winConditions.increaseValue(opposing, WinCondition.TotalDamage, ((CharacterEvent)event).amount); if(target.hp.getValue() == 0) { state.winConditions.increaseValue(opposing, WinCondition.TotalKnockouts, 1); } } case ConsumedAPEvent -> { ((Character)state.entities.findEntity(((CharacterEvent)event).targetEntity)).ap.decreaseValue(((CharacterEvent)event).amount); } case ConsumedMPEvent -> { ((Character)state.entities.findEntity(((CharacterEvent)event).targetEntity)).mp.decreaseValue(((CharacterEvent)event).amount); } case SpawnEntityEvent -> { state.entities.addEntity(((EntityEvent)event).entity); } case HealedEvent -> { ((Character)state.entities.findEntity(((CharacterEvent)event).targetEntity)).hp.increaseValue(((CharacterEvent)event).amount); } case MoveEvent -> { Character target = (Character)state.entities.findEntity(((CharacterEvent)event).originEntity); for(Entity entity: state.entities.findByPosition(((CharacterEvent)event).targetField)) { if(entity instanceof InfinityStone) { target.inventory.addStone(((InfinityStone)entity).type); state.winConditions.updateValue(target.id.type, WinCondition.MaxStones, target.inventory.getSize()); } } target.setPosition(((CharacterEvent)event).targetField); } case UseInfinityStoneEvent -> { state.stoneCooldown.setCooldown(((CharacterEvent)event).stoneType, 10); //TODO: use stone cooldown from config } case ExchangeInfinityStoneEvent -> { ((Character)state.entities.findEntity(((CharacterEvent)event).originEntity)).inventory.removeStone(((CharacterEvent)event).stoneType); Character target = (Character)state.entities.findEntity(((CharacterEvent)event).targetEntity); target.inventory.addStone(((CharacterEvent)event).stoneType); state.winConditions.updateValue(target.id.type, WinCondition.MaxStones, target.inventory.getSize()); } } } /** * Starts end of round handling if necessary. * @param state The game state to work on * @return The list of resulting {@link Event}s */ public static ArrayList checkTurnEnd(GameState state) { if( ((Character) state.entities.findEntity(state.activeCharacter)).ap.getValue() <= 0 && ((Character) state.entities.findEntity(state.activeCharacter)).mp.getValue() <= 0 ) { return handleTurnEnd(state); } return new ArrayList<>(); } /** * Handles everything that happens at the end of a turn, including new rounds. * @param state The game state to work on * @return The list of resulting {@link Event}s */ public static ArrayList handleTurnEnd(GameState state) { ArrayList result = new ArrayList<>(); ArrayList alive = new ArrayList<>(); for (EntityID id: state.turnOrder) { Character character = ((Character)state.entities.findEntity(id)); if(character.hp.getValue() > 0){ alive.add(id); } if(character.inventory.getFreeSlots() == 0) { // no slots => has all infinity stones result.addAll(handlePlayerWin(state, character.id.type)); return result; } } if(alive.isEmpty()) { EntityType winner = state.winConditions.getWinner(); if(winner == EntityType.None) { winner = rand.nextBoolean() ? EntityType.P1 : EntityType.P2; } result.addAll(handlePlayerWin(state, winner)); return result; } int index = alive.indexOf(state.activeCharacter); if(index == alive.size() - 1) { result.addAll(handleRoundStart(state)); }else { state.activeCharacter = alive.get(index + 1); } result.addAll(handleTurnStart(state)); return result; } /** * Handles everything that happens at the beginning of new rounds. * @param state The game state to work on * @return The list of resulting {@link Event}s */ public static ArrayList handleRoundStart(GameState state) { ArrayList result = new ArrayList<>(); state.roundNumber++; if(state.roundNumber >= 1 && state.roundNumber <= 6) { result.addAll(handleGoose(state)); } HashSet revived = new HashSet<>(); if(state.roundNumber == 7) { result.addAll(handleStan(state, revived)); } //TODO: add handling for thanos Collections.shuffle(state.turnOrder); for (EntityID id: state.turnOrder) { if(revived.contains(id) || ((Character)state.entities.findEntity(id)).hp.getValue() > 0){ state.activeCharacter = id; break; } } state.stoneCooldown.update(); result.add(new EventBuilder(EventType.RoundSetupEvent) .withRoundCount(state.roundNumber) .withCharacterOrder(state.turnOrder.toArray(new EntityID[0])) .buildGameEvent()); return result; } /** * Handles the actions of Goose at rounds 1-6. * @param state The game state to work on * @return The list of resulting {@link Event}s */ public static ArrayList handleGoose(GameState state) { ArrayList result = new ArrayList<>(); StoneType[] available = state.unvomitedStones.toArray(new StoneType[0]); StoneType stone = available[rand.nextInt(available.length)]; state.unvomitedStones.remove(stone); ArrayList free = new ArrayList<>(); for(int x = 0; x < state.mapSize.getX(); x++) { for(int y = 0; y < state.mapSize.getY(); y++) { IntVector2 pos = new IntVector2(x, y); if(state.entities.findByPosition(pos).size() == 0) { free.add(pos); } } } IntVector2 position = free.get(rand.nextInt(free.size())); EntityID goose = new EntityID(EntityType.NPC, 0); result.add(new EventBuilder(EventType.SpawnEntityEvent) .withEntity(new NPC(goose, position)) .buildEntityEvent()); result.add(new EventBuilder(EventType.SpawnEntityEvent) .withEntity(new InfinityStone(new EntityID(EntityType.InfinityStones, stone.getID()), position, stone)) .buildEntityEvent()); result.add(new EventBuilder(EventType.DestroyedEntityEvent) .withTargetEntity(goose) .withTargetField(position) .buildEntityEvent()); return result; } /** * Handles the actions of Stan at round 7. * @param state The game state to work on * @return The list of resulting {@link Event}s */ public static ArrayList handleStan(GameState state, HashSet revived) { ArrayList result = new ArrayList<>(); ArrayList characters = new ArrayList<>(); ArrayList targetOptions = new ArrayList<>(); int lowest = -1; for(EntityID id: state.turnOrder) { Character character = (Character)state.entities.findEntity(id); characters.add(character); if(lowest == -1 || character.hp.getValue() < lowest) { lowest = character.hp.getValue(); targetOptions.clear(); } if(lowest == character.hp.getValue()) { targetOptions.add(character.getPosition()); } } IntVector2 targetPosition = targetOptions.get(rand.nextInt(targetOptions.size())); ArrayList spawnOptions = getFreeNeighbour(state, targetPosition); if(spawnOptions.size() == 0) { return result; } IntVector2 spawnPosition = spawnOptions.get(rand.nextInt(spawnOptions.size())); EntityID stan = new EntityID(EntityType.NPC, 1); result.add(new EventBuilder(EventType.SpawnEntityEvent) .withEntity(new NPC(stan, spawnPosition)) .buildEntityEvent()); for(Character character: characters) { if(checkLineOfSight(state, spawnPosition, character.getPosition())) { if(character.hp.getValue() == 0) { revived.add(character.id); } if(character.hp.getValue() != character.hp.max) { result.add(new EventBuilder(EventType.HealedEvent) .withTargetEntity(character.id) .withTargetField(character.getPosition()) .withAmount(character.hp.max - character.hp.getValue()) .buildEntityEvent()); } } } result.add(new EventBuilder(EventType.DestroyedEntityEvent) .withTargetEntity(stan) .withTargetField(spawnPosition) .buildEntityEvent()); return result; } /** * Handles everything that happens at the beginning of a turn. * @param state The game state to work on * @return The list of resulting {@link Event}s */ public static ArrayList handleTurnStart(GameState state) { ArrayList result = new ArrayList<>(); state.turnNumber++; Character activeCharacter = (Character)state.entities.findEntity(state.activeCharacter); if(activeCharacter.ap.getValue() != activeCharacter.ap.max) { result.add(new EventBuilder(EventType.ConsumedAPEvent) .withTargetEntity(state.activeCharacter) .withTargetField(activeCharacter.getPosition()) .withAmount(activeCharacter.ap.getValue() - activeCharacter.ap.max) .buildGameEvent()); } if(activeCharacter.mp.getValue() != activeCharacter.mp.max) { result.add(new EventBuilder(EventType.ConsumedMPEvent) .withTargetEntity(state.activeCharacter) .withTargetField(activeCharacter.getPosition()) .withAmount(activeCharacter.mp.getValue() - activeCharacter.mp.max) .buildGameEvent()); } result.add(new EventBuilder(EventType.TurnEvent) .withTurnCount(state.turnOrder.size()) .withNextCharacter(state.activeCharacter) .buildGameEvent()); return result; } /** * Handles the victory of a player through one character. * @param state The game state to work on * @param winner The winning character * @return The list of resulting {@link Event}s */ public static ArrayList handlePlayerWin(GameState state, EntityType winner) { ArrayList result = new ArrayList<>(); state.won = true; result.add(new EventBuilder(EventType.WinEvent) .withPlayerWon(winner == EntityType.P1 ? 1 : 2) .buildGameEvent()); return result; } }