package uulm.teamname.marvelous.gamelibrary.gamelogic; import org.tinylog.Logger; import uulm.teamname.marvelous.gamelibrary.ArrayTools; import uulm.teamname.marvelous.gamelibrary.IntVector2; import uulm.teamname.marvelous.gamelibrary.config.CharacterProperties; import uulm.teamname.marvelous.gamelibrary.config.FieldType; import uulm.teamname.marvelous.gamelibrary.entities.Character; import uulm.teamname.marvelous.gamelibrary.entities.*; import uulm.teamname.marvelous.gamelibrary.events.Event; import uulm.teamname.marvelous.gamelibrary.events.*; 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.awt.geom.Point2D; import java.util.List; import java.util.*; /** Contains game logic handling. */ public class GameLogic { private static final Random rand = new Random(); /** * 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 the request is valid */ protected 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); Entity target = getAttackable(state, data.targetField, data.targetEntity); requireTurn(state, origin); requireAlive(origin); requireAP(origin, 1); if(target instanceof Character) { Character targetCharacter = (Character)target; requireOppositeTeam(origin, targetCharacter); requireAlive(targetCharacter); } if(request.type == RequestType.MeleeAttackRequest) { if(origin.meleeDamage != data.value) { throw new InvalidRequestException("Invalid melee damage"); } if(data.originField.distanceChebyshev(data.targetField) != 1) { throw new InvalidRequestException("Invalid melee target distance"); } }else if(request.type == RequestType.RangedAttackRequest) { if(origin.rangedDamage != data.value) { throw new InvalidRequestException("Invalid ranged damage"); } if(data.originField.distanceChebyshev(data.targetField) > origin.attackRange) { throw new InvalidRequestException("Invalid ranged distance (too far)"); } if(data.originField.distanceChebyshev(data.targetField) <= 1) { throw new InvalidRequestException("Invalid ranged distance (too short)"); } requireLineOfSight(state, data.originField, data.targetField); } return true; } case MoveRequest -> { CharacterRequest data = (CharacterRequest)request; Character origin = getCharacter(state, data.originField, data.originEntity); requireTurn(state, origin); requireAlive(origin); requireMP(origin, 1); verifyCoordinates(state, data.targetField); if(data.originField.distanceChebyshev(data.targetField) != 1) { throw new InvalidRequestException("Invalid move distance"); } if(state.entities.blocksMovement(data.targetField)) { throw new InvalidRequestException("Moving into blocked field"); } return true; } case ExchangeInfinityStoneRequest -> { CharacterRequest data = (CharacterRequest)request; Character origin = getCharacter(state, data.originField, data.originEntity); Character target = getCharacter(state, data.targetField, data.targetEntity); requireTurn(state, origin); requireAlive(origin); requireAlive(target); requireAP(origin, 1); requireInfinityStone(origin, data.stoneType); if(data.originField.distanceChebyshev(data.targetField) != 1) { throw new InvalidRequestException("Invalid infinity stone exchange distance"); } return true; } case UseInfinityStoneRequest -> { CharacterRequest data = (CharacterRequest)request; Character origin = getCharacter(state, data.originField, data.originEntity); requireTurn(state, origin); requireAlive(origin); requireAP(origin, 1); requireInfinityStone(origin, data.stoneType); if(state.stoneCooldown.onCooldown(data.stoneType)) { throw new InvalidRequestException("Using infinity stone on cooldown"); } switch(((CharacterRequest) request).stoneType) { case SpaceStone -> { verifyCoordinates(state, data.targetField); if(state.entities.findByPosition(data.targetField).size() != 0) { throw new InvalidRequestException("Using space stone onto non-free field"); } } case MindStone -> { verifyCoordinates(state, data.targetField); requireLineOfSight(state, data.originField, data.targetField); getAttackableWithoutID(state, data.targetField); } case RealityStone -> { verifyCoordinates(state, data.targetField); boolean rock = false; boolean empty = true; for(Entity entity: state.entities.findByPosition(data.targetField)) { if(entity.id.type == EntityType.Rocks) { rock = true; break; }else { empty = false; } } if(!empty && !rock) { throw new InvalidRequestException("Using reality stone on non-free field without a rock"); } } case PowerStone -> { verifyCoordinates(state, data.targetField); Entity target = getAttackableWithoutID(state, data.targetField); if(target instanceof Character) { Character targetCharacter = (Character)target; requireOppositeTeam(origin, targetCharacter); requireAlive(targetCharacter); } if(data.originField.distanceChebyshev(data.targetField) != 1) { throw new InvalidRequestException("Invalid melee target distance"); } } case TimeStone -> { // "👍 i approve" - the server } case SoulStone -> { verifyCoordinates(state, data.targetField); Character target = getCharacterWithoutID(state, data.targetField); if(data.originEntity.equals(data.targetEntity)) { throw new InvalidRequestException("Invalid soul stone target (same as origin)"); } if(data.originField.distanceChebyshev(data.targetField) != 1) { throw new InvalidRequestException("Invalid soul stone target distance"); } if(target.isAlive()) { throw new InvalidRequestException("Invalid soul stone target (already alive)"); } } } return true; } case EndRoundRequest, Req -> { return true; } } }catch(InvalidRequestException exception) { Logger.debug("Request denied: " + exception.getMessage()); return false; }catch(Exception ignored) { return false; } return false; } /** * Retrieves an attack-able Entity ({@link Character} or {@link Rock}) 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 entity * @throws InvalidRequestException if the entity is invalid or not found */ private static Entity getAttackable(GameState state, IntVector2 position, EntityID entityID) throws InvalidRequestException { Entity entity = state.entities.findEntity(entityID); if(entity == null || !entity.getPosition().equals(position) || (!(entity instanceof Character) && !(entity instanceof Rock)) || entity.id.type == EntityType.NPC) { throw new InvalidRequestException("Invalid target character or rock"); } return entity; } /** * Retrieves an attack-able Entity ({@link Character} or {@link Rock}) for a {@link Request}. * @param state The game state to use * @param position The requested position * @return The found entity * @throws InvalidRequestException if the entity is invalid or not found */ private static Entity getAttackableWithoutID(GameState state, IntVector2 position) throws InvalidRequestException { ArrayList entities = state.entities.findByPosition(position); if(entities.isEmpty() || entities.get(0).id.type == EntityType.NPC) { throw new InvalidRequestException("Invalid target character or rock"); } return entities.get(0); } /** * 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().equals(position) || !(entity instanceof Character) || entity.id.type == EntityType.NPC) { throw new InvalidRequestException("Invalid origin or target character"); } try { return (Character)entity; }catch(Exception ignored) { throw new InvalidRequestException("Invalid origin or target character (cast failed)"); } } /** * Retrieves a {@link Character} for a {@link Request}. * @param state The game state to use * @param position The requested position * @return The found character * @throws InvalidRequestException if the character is invalid or not found */ private static Character getCharacterWithoutID(GameState state, IntVector2 position) throws InvalidRequestException { ArrayList entities = state.entities.findByPosition(position); if(entities.isEmpty() || !(entities.get(0) instanceof Character) || entities.get(0).id.type == EntityType.NPC) { throw new InvalidRequestException("Invalid origin or target character"); } try { return (Character)entities.get(0); }catch(Exception ignored) { throw new InvalidRequestException("Invalid origin or target character (cast failed)"); } } /** * Verifies that a {@link Character} has a turn. */ private static void requireTurn(GameState state, Character entity) throws InvalidRequestException { if(!entity.id.equals(state.activeCharacter)) { throw new InvalidRequestException("Target does not have the turn"); } } /** * Verifies that a {@link Character} is of the opposite team of another Character. */ private static void requireOppositeTeam(Character a, Character b) throws InvalidRequestException { if(a.id.type == b.id.type) { throw new InvalidRequestException("Origin and target are not on opposite team"); } } /** * Verifies that a {@link Character} is of the same team of another Character, but not the same. */ private static void requireSameTeam(Character a, Character b) throws InvalidRequestException { if(a.id.type != b.id.type || a.id.id == b.id.id) { throw new InvalidRequestException("Origin and target are not on same team"); } } /** * Verifies that a {@link Character} is alive. */ private static void requireAlive(Character entity) throws InvalidRequestException { if(!entity.isAlive()) { throw new InvalidRequestException("Character is not alive"); } } /** * 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("Character does not have enough ap"); } } /** * 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("Character does not have enough mp"); } } /** * 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("Character does not have the infinity stone"); } } /** * 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("Out of bounds coordinates"); } } /** * 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("Character does not have line of sight to target"); } } /** * Produces resulting {@link Event Events} 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()); Entity targetEntity = state.entities.findEntity(data.targetEntity); result.addAll(checkDeath(state, targetEntity, data.value)); } 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 entity per field anyways }else if(entity instanceof InfinityStone) { result.add(new EventBuilder(EventType.DestroyedEntityEvent) .withTargetField(data.targetField) .withTargetEntity(entity.id) .buildEntityEvent()); break; //we should only have one entity per field anyways }else if(entity instanceof Portal) { List targets = new ArrayList<>(); for(Entity e: state.entities) { if(e.id.type == EntityType.Portals && !e.id.equals(entity.id)) { targets.add(e); } } if(targets.isEmpty()) { break; } Entity target = targets.get(rand.nextInt(targets.size())); List fields = getFreeNeighbour(state, target.getPosition()); if(fields.isEmpty()) { break; } IntVector2 field = fields.get(rand.nextInt(fields.size())); result.add(new EventBuilder(EventType.TeleportedEvent) .withTeleportedEntity(data.originEntity) .withOriginField(data.targetField) .withTargetField(field) .withOriginPortal(entity.id) .withTargetPortal(target.id) .buildTeleportedEvent()); break; //we should only have one entity 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 -> { ArrayList found = state.entities.findByPosition(data.targetField); Entity targetEntity = found.get(0); result.add(new EventBuilder(EventType.TakenDamageEvent) .withTargetEntity(targetEntity.id) .withTargetField(data.targetField) .withAmount(state.partyConfig.mindStoneDMG) .buildEntityEvent()); result.addAll(checkDeath(state, targetEntity, state.partyConfig.mindStoneDMG)); } case RealityStone -> { EntityID target = null; for(Entity entity: state.entities.findByPosition(data.targetField)) { if(entity.id.type == EntityType.Rocks) { target = entity.id; break; } } if(target == null) { result.add(new EventBuilder(EventType.SpawnEntityEvent) .withTargetField(data.targetField) .withEntity(new Rock(new EntityID(EntityType.Rocks, state.entities.findFreeRockSlot()), data.targetField, 100)) .buildEntityEvent()); }else { result.add(new EventBuilder(EventType.DestroyedEntityEvent) .withTargetField(data.targetField) .withTargetEntity(target) .buildEntityEvent()); } } case PowerStone -> { ArrayList found = state.entities.findByPosition(data.targetField); Entity targetEntity = found.get(0); Character origin = (Character)state.entities.findEntity(data.originEntity); int dmg = (int)Math.round(origin.hp.getMax() * 0.1); //this is ugly ... but also easy to understand int hp1 = origin.hp.getValue(); int hp2 = Math.max(1, origin.hp.getValue() - dmg); int actualDmg = hp1 - hp2; if(actualDmg > 0) { result.add(new EventBuilder(EventType.TakenDamageEvent) .withTargetEntity(data.originEntity) .withTargetField(data.originField) .withAmount(actualDmg) .buildEntityEvent()); } result.add(new EventBuilder(EventType.TakenDamageEvent) .withTargetEntity(targetEntity.id) .withTargetField(data.targetField) .withAmount(origin.meleeDamage * 2) .buildEntityEvent()); result.addAll(checkDeath(state, targetEntity, origin.meleeDamage * 2)); } case TimeStone -> { Character origin = (Character)state.entities.findEntity(data.originEntity); int ap = origin.ap.getValue() - origin.ap.getMax(); if(ap < 0) { result.add(new EventBuilder(EventType.ConsumedAPEvent) .withTargetEntity(data.originEntity) .withTargetField(data.originField) .withAmount(ap) .buildEntityEvent()); } int mp = origin.mp.getValue() - origin.mp.getMax(); if(mp < 0) { result.add(new EventBuilder(EventType.ConsumedMPEvent) .withTargetEntity(data.originEntity) .withTargetField(data.originField) .withAmount(mp) .buildEntityEvent()); } } case SoulStone -> { ArrayList found = state.entities.findByPosition(data.targetField); Character target = (Character)found.get(0); result.add(new EventBuilder(EventType.HealedEvent) .withTargetEntity(target.id) .withTargetField(data.targetField) .withAmount(target.hp.getMax()) .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; } /** * Checks for death of a Character or Rock and returns the resulting {@link Event Events}. * @param state The game state to apply to * @param targetEntity The entity to check * @param damage The damage taken * @return The resulting events */ private static ArrayList checkDeath(GameState state, Entity targetEntity, int damage) { ArrayList result = new ArrayList<>(); if(targetEntity instanceof Character) { Character target = (Character)targetEntity; if(target.hp.getValue() <= damage) { List stones = Arrays.asList(target.inventory.getStonesAsArray()); Collections.shuffle(stones); // required by documents ArrayList used = new ArrayList<>(); for(StoneType stone: stones) { ArrayList options = getFreeNeighbour(state, target.getPosition(), used); IntVector2 picked = options.get(rand.nextInt(options.size())); used.add(picked); result.add(new EventBuilder(EventType.SpawnEntityEvent) .withEntity(new InfinityStone( new EntityID(EntityType.InfinityStones, stone.getID()), picked, stone )) .buildEntityEvent()); } } }else if(targetEntity instanceof Rock) { Rock target = (Rock)targetEntity; if(target.getHp() <= damage) { result.add(new EventBuilder(EventType.DestroyedEntityEvent) .withTargetField(target.getPosition()) .withTargetEntity(target.id) .buildEntityEvent()); } } return result; } /** * 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 -> { EntityEvent data = (EntityEvent)event; Entity targetEntity = state.entities.findEntity(data.targetEntity); if(targetEntity instanceof Character) { Character target = (Character)targetEntity; target.hp.decreaseValue(data.amount); EntityType opposing = target.id.type == EntityType.P1 ? EntityType.P2 : EntityType.P1; state.winConditions.increaseValue(opposing, WinCondition.TotalDamage, data.amount); if(!target.isAlive()) { target.inventory.clear(); state.winConditions.increaseValue(opposing, WinCondition.TotalKnockouts, 1); } if(state.activeCharacter != null && state.activeCharacter.type == EntityType.NPC && state.activeCharacter.id == NPCType.Thanos.getID()) { NPC thanos = (NPC)state.entities.findEntity(state.activeCharacter); target.inventory.transfer(thanos.inventory); } }else if(targetEntity instanceof Rock) { Rock target = (Rock)targetEntity; target.decreaseHp(data.amount); } } case ConsumedAPEvent -> { EntityEvent data = (EntityEvent)event; ((Character)state.entities.findEntity(data.targetEntity)).ap.decreaseValue(data.amount); } case ConsumedMPEvent -> { EntityEvent data = (EntityEvent)event; if(data.targetEntity.type != EntityType.NPC) { ((Character)state.entities.findEntity(data.targetEntity)).mp.decreaseValue(data.amount); }else { ((NPC)state.entities.findEntity(data.targetEntity)).mp.decreaseValue(data.amount); } } case RoundSetupEvent -> { GameEvent data = (GameEvent)event; state.roundNumber++; state.turnNumber = 0; state.turnOrder = new ArrayList<>(); for(EntityID turn: data.characterOrder) { if(turn.type != EntityType.NPC || turn.id == NPCType.Thanos.getID()) { state.turnOrder.add(turn); } } state.stoneCooldown.update(); } case TurnEvent -> { GameEvent data = (GameEvent)event; if(data.nextCharacter.type != EntityType.NPC) { Character target = (Character)state.entities.findEntity(data.nextCharacter); target.ap.setValue(target.ap.getMax()); target.mp.setValue(target.mp.getMax()); }else if(data.nextCharacter.id == NPCType.Thanos.getID()) { NPC target = (NPC)state.entities.findEntity(data.nextCharacter); if(state.roundNumber > state.partyConfig.maxRounds + 1) { target.mp.setMax(target.mp.getMax() + 1); } target.mp.setValue(target.mp.getMax()); } state.turnNumber++; state.activeCharacter = data.nextCharacter.clone(); } case SpawnEntityEvent -> { EntityEvent data = (EntityEvent)event; state.entities.addEntity(data.entity); if(state.activeCharacter != null && state.activeCharacter.type == EntityType.NPC && state.activeCharacter.id == NPCType.Goose.getID()) { state.unvomitedStones.remove(((InfinityStone)data.entity).type); } } case HealedEvent -> { EntityEvent data = (EntityEvent)event; ((Character)state.entities.findEntity(data.targetEntity)).hp.increaseValue(data.amount); } case MoveEvent -> { CharacterEvent data = (CharacterEvent)event; if(data.originEntity.type != EntityType.NPC) { Character target = (Character)state.entities.findEntity(data.originEntity); for(Entity entity: state.entities.findByPosition(data.targetField)) { if(entity instanceof InfinityStone) { target.inventory.addStone(((InfinityStone)entity).type); state.winConditions.updateValue(target.id.type, WinCondition.MaxStones, target.inventory.getSize()); } } target.setPosition(data.targetField); }else { NPC target = (NPC)state.entities.findEntity(data.originEntity); for(Entity entity: state.entities.findByPosition(data.targetField)) { if(entity instanceof InfinityStone) { target.inventory.addStone(((InfinityStone)entity).type); state.winConditions.updateValue(target.id.type, WinCondition.MaxStones, target.inventory.getSize()); } } target.setPosition(data.targetField); } } case TeleportedEvent -> { TeleportedEvent data = (TeleportedEvent)event; Character target = (Character)state.entities.findEntity(data.teleportedEntity); target.setPosition(data.targetField); } case UseInfinityStoneEvent -> { state.stoneCooldown.setCooldown(((CharacterEvent)event).stoneType); } case ExchangeInfinityStoneEvent -> { CharacterEvent data = (CharacterEvent)event; ((Character)state.entities.findEntity(data.originEntity)).inventory.removeStone(data.stoneType); Character target = (Character)state.entities.findEntity(data.targetEntity); target.inventory.addStone(data.stoneType); state.winConditions.updateValue(target.id.type, WinCondition.MaxStones, target.inventory.getSize()); } case WinEvent -> { state.won = true; } case GamestateEvent -> { GamestateEvent data = (GamestateEvent)event; state.entities.clear(); state.entities.addEntities(data.entities); state.mapSize.set(data.mapSize); state.turnOrder = ArrayTools.toArrayList(data.turnOrder); state.activeCharacter = data.activeCharacter == null ? null : data.activeCharacter.clone(); state.stoneCooldown.import_(data.stoneCooldowns); state.won = data.winCondition; } } } /** * 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(state.turnOrder.toArray(new EntityID[0])) .withMapSize(state.mapSize) .withActiveCharacter(state.activeCharacter) .withStoneCooldowns(state.stoneCooldown.export()) .withWinCondition(state.won) .buildGameStateEvent(); } /** * 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 */ public static boolean checkLineOfSight(GameState state, IntVector2 start, IntVector2 end) { for(IntVector2 pos: rasterize(start, end, false, false)) { if(state.entities.blocksVision(pos)) { 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 */ public static ArrayList getFreeNeighbour(GameState state, IntVector2 start) { return getFreeNeighbour(state, start, new ArrayList<>(0)); } /** * Finds free neighbour options from a starting field. * @param state The game state to work on * @param start The starting position * @param blocked A list of positions to treat as not-free * @return A list of free neighbour field options */ public static ArrayList getFreeNeighbour(GameState state, IntVector2 start, List blocked) { ArrayList options = new ArrayList<>(); if(start.getX() < 0 || start.getX() >= state.mapSize.getX() || start.getY() < 0 || start.getY() >= state.mapSize.getY()) { return options; } ArrayList allOptions = new ArrayList<>(); for(IntVector2 dir: IntVector2.CardinalDirections) { IntVector2 pos = start.copy().add(dir); if(pos.getX() < 0 || pos.getX() >= state.mapSize.getX() || pos.getY() < 0 || pos.getY() >= state.mapSize.getY()) { continue; } allOptions.add(pos); if(!blocked.contains(pos) && state.entities.findByPosition(pos).isEmpty()) { options.add(pos); } } if(options.isEmpty()) { if(allOptions.isEmpty()) { return allOptions; } return getFreeNeighbour(state, allOptions.get(rand.nextInt(allOptions.size())), blocked); }else { return options; } } /** * Finds free field options. * @param state The game state to work on * @return A list of free field options */ public static ArrayList getFreeFields(GameState state) { ArrayList options = 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) { options.add(pos); } } } return options; } /** * Starts the game and initializes all entities. * @param state The game state to work on * @param selectedCharacters1 The characters selected by player 1 * @param selectedCharacters2 The characters selected by player 2 */ protected static void startGame(GameState state, List selectedCharacters1, List selectedCharacters2) { Logger.trace("Starting game"); ArrayList free = new ArrayList<>(); int rockIndex = 0; int portalIndex = 0; for(int x = 0; x < state.mapSize.getX(); x++) { for(int y = 0; y < state.mapSize.getY(); y++) { if(state.scenarioConfig.scenario[y][x] == FieldType.ROCK) { state.entities.addEntity(new Rock(new EntityID(EntityType.Rocks, rockIndex++), new IntVector2(x, y), 100)); }else if(state.scenarioConfig.scenario[y][x] == FieldType.PORTAL) { state.entities.addEntity(new Portal(new EntityID(EntityType.Portals, portalIndex++), new IntVector2(x, y))); }else { free.add(new IntVector2(x, y)); } } } int p1 = selectedCharacters1.size(); int all = selectedCharacters1.size() + selectedCharacters2.size(); ArrayList characters = new ArrayList<>(selectedCharacters1); characters.addAll(selectedCharacters2); for(int i = 0; i < all; i++) { int choice = rand.nextInt(free.size()); IntVector2 position = free.get(choice); free.remove(choice); CharacterProperties selected = state.characterConfig.getIDMap().get(characters.get(i)); EntityID id = new EntityID(i < p1 ? EntityType.P1 : EntityType.P2, i % p1); state.entities.addEntity(new Character( id, position, selected.name, selected.HP, selected.MP, selected.AP, selected.attackRange, selected.rangedDamage, selected.meleeDamage )); state.turnOrder.add(id); } } /** * Forcefully starts the next round. * @param state The game state to work on * @return The list of resulting {@link Event Events} */ protected static ArrayList startRound(GameState state) { ArrayList result = handleRoundStart(state); result.addAll(handleTurnStart(state)); return result; } /** * Starts end of round handling if necessary. * @param state The game state to work on * @return The list of resulting {@link Event Events} */ protected static ArrayList checkTurnEnd(GameState state) { if( (state.activeCharacter.type == EntityType.NPC && state.activeCharacter.id == NPCType.Thanos.getID()) || ((Character) state.entities.findEntity(state.activeCharacter)).ap.getValue() <= 0 && ((Character) state.entities.findEntity(state.activeCharacter)).mp.getValue() <= 0 ) { return handleTurnEnd(state); } return new ArrayList<>(); } /** * Forcefully ends the current turn. * @param state The game state to work on * @return The list of resulting {@link Event Events} */ protected static ArrayList endTurn(GameState state) { return handleTurnEnd(state); } /** * 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 Events} */ private static ArrayList handleTurnEnd(GameState state) { ArrayList result = new ArrayList<>(); boolean anyAlive = false; ArrayList turns = new ArrayList<>(); for (EntityID id: state.turnOrder) { if(id.type == EntityType.NPC) { if(id.id == NPCType.Thanos.getID()) { turns.add(id); } continue; } Entity found = state.entities.findEntity(id); if(found == null) { continue; } Character character = ((Character)found); if(character.isAlive()){ anyAlive = true; turns.add(id); }else { // send empty turn for knocked out characters result.add(new EventBuilder(EventType.TurnEvent) .withTurnCount(state.turnOrder.indexOf(id) + 1) .withNextCharacter(id) .buildGameEvent()); } if(character.inventory.getFreeSlots() == 0) { // no slots => has all infinity stones result.addAll(handlePlayerWin(state, character.id.type, "collected all infinity stones")); return result; } } if(!anyAlive) { String[] reason = new String[1]; EntityType winner = state.winConditions.getWinner(reason); if(winner == EntityType.None) { winner = rand.nextBoolean() ? EntityType.P1 : EntityType.P2; reason[0] = "determined by lot as tiebreaker"; } result.addAll(handlePlayerWin(state, winner, reason[0])); return result; } int index = turns.indexOf(state.activeCharacter); if(index == turns.size() - 1) { result.addAll(handleRoundStart(state)); }else { state.activeCharacter = turns.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 Events} */ private static ArrayList handleRoundStart(GameState state) { ArrayList result = new ArrayList<>(); state.roundNumber++; result.add(new EventBuilder(EventType.RoundSetupEvent) .withRoundCount(state.roundNumber) .buildGameEvent()); ArrayList turns = new ArrayList<>(); if(state.roundNumber >= 1 && state.roundNumber <= 6) { turns.add(new EntityID(EntityType.NPC, NPCType.Goose.getID())); result.addAll(handleGoose(state)); } HashSet revived = new HashSet<>(); if(state.roundNumber == 7) { turns.add(new EntityID(EntityType.NPC, NPCType.Stan.getID())); result.addAll(handleStan(state, revived)); } if(state.roundNumber == state.partyConfig.maxRounds + 1) { result.addAll(spawnThanos(state)); } Collections.shuffle(state.turnOrder); for (EntityID id: state.turnOrder) { Entity found = state.entities.findEntity(id); if(found == null) { continue; } if(id.type == EntityType.NPC || revived.contains(id) || ((Character)found).isAlive()){ state.activeCharacter = id; break; }else { // again send empty turns for knocked out characters result.add(new EventBuilder(EventType.TurnEvent) .withTurnCount(state.turnOrder.indexOf(id) + 1) .withNextCharacter(id) .buildGameEvent()); } } turns.addAll(state.turnOrder); // RoundSetupEvent has to be sent first, but the contents of it are determined later... ((GameEvent)result.get(0)).characterOrder = turns.toArray(new EntityID[0]); 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 Events} */ private static ArrayList handleGoose(GameState state) { ArrayList result = new ArrayList<>(); ArrayList inventory = new ArrayList<>(state.unvomitedStones); int picked = rand.nextInt(inventory.size()); StoneType stone = inventory.get(picked); inventory.remove(picked); 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, NPCType.Goose.getID()); result.add(new EventBuilder(EventType.SpawnEntityEvent) .withEntity(new NPC(goose, position, inventory)) .buildEntityEvent()); result.add(new EventBuilder(EventType.TurnEvent) .withTurnCount(1) .withNextCharacter(goose) .buildGameEvent()); 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 Events} */ private 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) { if(id.type == EntityType.NPC) { continue; } 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, NPCType.Stan.getID()); result.add(new EventBuilder(EventType.SpawnEntityEvent) .withEntity(new NPC(stan, spawnPosition)) .buildEntityEvent()); result.add(new EventBuilder(EventType.TurnEvent) .withTurnCount(1) .withNextCharacter(stan) .buildGameEvent()); for(Character character: characters) { if(checkLineOfSight(state, spawnPosition, character.getPosition())) { if(!character.isAlive()) { revived.add(character.id); } if(character.hp.getValue() != character.hp.getMax()) { result.add(new EventBuilder(EventType.HealedEvent) .withTargetEntity(character.id) .withTargetField(character.getPosition()) .withAmount(character.hp.getMax() - character.hp.getValue()) .buildEntityEvent()); } } } result.add(new EventBuilder(EventType.DestroyedEntityEvent) .withTargetEntity(stan) .withTargetField(spawnPosition) .buildEntityEvent()); return result; } /** * Spawns Thanos at the beginning of the first overtime round. * @param state The game state to work on * @return The list of resulting {@link Event Events} */ private static ArrayList spawnThanos(GameState state) { ArrayList result = new ArrayList<>(); ArrayList free = getFreeFields(state); IntVector2 position = free.get(rand.nextInt(free.size())); int maxMP = -1; for(EntityID id: state.turnOrder) { if(id.type == EntityType.NPC) { continue; } Character character = (Character)state.entities.findEntity(id); if(character.mp.getMax() > maxMP) { maxMP = character.mp.getMax(); } } EntityID thanos = new EntityID(EntityType.NPC, NPCType.Thanos.getID()); NPC thanosNPC = new NPC(thanos, position, maxMP); result.add(new EventBuilder(EventType.SpawnEntityEvent) .withEntity(thanosNPC) .buildEntityEvent()); for(Entity e: state.entities) { if(e.id.type == EntityType.Portals) { result.add(new EventBuilder(EventType.DestroyedEntityEvent) .withTargetEntity(e.id) .withTargetField(e.getPosition()) .buildEntityEvent()); } } state.turnOrder.add(thanos); state.entities.addEntity(thanosNPC); return result; } /** * Handles Thanos' AI. * @param state The game state to work on * @return The list of resulting {@link Event Events} */ private static ArrayList handleThanos(GameState state, NPC thanos) { ArrayList result = new ArrayList<>(); result.add(new EventBuilder(EventType.TurnEvent) .withTurnCount(state.turnOrder.indexOf(thanos.id) + 1) .withNextCharacter(thanos.id) .buildGameEvent()); if(thanos.inventory.getFreeSlots() > 0) { IntVector2 picked = null; float lowestDistance = Integer.MAX_VALUE; for(int x = 0; x < state.mapSize.getX(); x++) { for(int y = 0; y < state.mapSize.getY(); y++) { IntVector2 pos = new IntVector2(x, y); for(Entity e: state.entities.findByPosition(pos)) { if(e instanceof InfinityStone || (e instanceof Character && ((Character)e).inventory.getSize() > 0)) { float distance = thanos.getPosition().distanceChebyshev(pos); if(distance < lowestDistance) { picked = pos; lowestDistance = distance; break; } } } } } if(picked == null) { return result; } ArrayList path = GameLogic.Bresenham8Connected(thanos.getPosition(), picked); int mp = thanos.mp.getMax(); if(mp <= 0) { return result; } IntVector2 current = thanos.getPosition(); for(IntVector2 pos: path) { if(pos.equals(thanos.getPosition())) { continue; } if(!pos.equals(picked)) { for(Entity entity: state.entities.findByPosition(pos)) { if(entity instanceof Rock) { result.add(new EventBuilder(EventType.DestroyedEntityEvent) .withTargetEntity(entity.id) .withTargetField(entity.getPosition()) .buildEntityEvent()); } } } result.add(new EventBuilder(EventType.MoveEvent) .withOriginEntity(thanos.id) .withOriginField(current) .withTargetField(pos) .buildCharacterEvent()); result.add(new EventBuilder(EventType.ConsumedMPEvent) .withTargetEntity(thanos.id) .withTargetField(pos) .withAmount(1) .buildEntityEvent()); for(Entity entity: state.entities.findByPosition(pos)) { if(entity instanceof Character) { result.add(new EventBuilder(EventType.MoveEvent) .withOriginEntity(entity.id) .withOriginField(pos) .withTargetField(current) .buildCharacterEvent()); result.add(new EventBuilder(EventType.TakenDamageEvent) .withTargetEntity(entity.id) .withTargetField(current) .withAmount(((Character)entity).hp.getValue()) .buildEntityEvent()); break; } if(entity instanceof InfinityStone) { result.add(new EventBuilder(EventType.DestroyedEntityEvent) .withTargetEntity(entity.id) .withTargetField(pos) .buildEntityEvent()); break; } } current = pos; if(--mp == 0) { break; } } }else { for(EntityID id: state.turnOrder) { if(id.type == EntityType.NPC) { continue; } Entity found = state.entities.findEntity(id); if(found == null) { continue; } if(rand.nextBoolean()) { result.add(new EventBuilder(EventType.DestroyedEntityEvent) .withTargetEntity(id) .withTargetField(found.getPosition()) .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 Events} */ private static ArrayList handleTurnStart(GameState state) { ArrayList result = new ArrayList<>(); if(state.activeCharacter.type == EntityType.NPC && state.activeCharacter.id == NPCType.Thanos.getID()) { NPC thanos = (NPC)state.entities.findEntity(state.activeCharacter); result.addAll(handleThanos(state, thanos)); return result; } result.add(new EventBuilder(EventType.TurnEvent) .withTurnCount(state.turnOrder.indexOf(state.activeCharacter) + 1) .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 Events} */ private static ArrayList handlePlayerWin(GameState state, EntityType winner, String reason) { Logger.trace("Player " + winner + " won: " + reason); ArrayList result = new ArrayList<>(); result.add(new EventBuilder(EventType.WinEvent) .withPlayerWon(winner == EntityType.P1 ? 1 : 2) .buildGameEvent()); return result; } /** * Computes all fields which intersect the line between the center points of the given two fields including the two defining points. * @return The list of intersecting positions */ public static ArrayList rasterize(IntVector2 a, IntVector2 b) { return rasterize(a, b, true, true); } /** * Computes all fields which intersect the line between the center points of the given two fields. * @param includeA Whether to include point a in the result * @param includeB Whether to include point b in the result * @return The list of intersecting positions */ public static ArrayList rasterize(IntVector2 a, IntVector2 b, boolean includeA, boolean includeB) { ArrayList result = new ArrayList<>(); int x1 = Math.min(a.getX(), b.getX()); int x2 = Math.max(a.getX(), b.getX()); int y1 = Math.min(a.getY(), b.getY()); int y2 = Math.max(a.getY(), b.getY()); Line2D line = new Line2D.Float(a.getX() + 0.5f, a.getY() + 0.5f, b.getX() + 0.5f, b.getY() + 0.5f); for(int i = x1; i <= x2; i++) { for(int j = y1; j <= y2; j++) { HashSet intersections = new HashSet<>(); for(Line2D.Float part: getWireframe(new Rectangle.Float(i, j, 1, 1))) { if(part.intersectsLine(line)) { Point2D.Float intersection = calculateInterceptionPoint(line.getP1(), line.getP2(), part.getP1(), part.getP2()); intersections.add(intersection); if(intersections.size() > 1) { break; } } } if(intersections.size() > 1) { result.add(new IntVector2(i, j)); } } } if(includeA) { result.add(a); } if(includeB) { result.add(b); } return result; } /** * Computes all fields which intersect the ray between the center points of the given two fields extending past the second one. * @param size The size of the map * @param includeA Whether to include point a in the result * @return The list of intersecting positions */ public static ArrayList rasterizeInfinity(IntVector2 a, IntVector2 b, IntVector2 size, boolean includeA) { ArrayList result = new ArrayList<>(); if(includeA) { result.add(a); } int x1 = a.getX(); int y1 = a.getY(); int x2 = b.getX(); int y2 = b.getY(); Line2D line = new Line2D.Float(x1 + 0.5f, y1 + 0.5f, x2 + 0.5f, y2 + 0.5f); double l = line.getP1().distance(line.getP2()); for(int i = 0; i < size.getX(); i++) { for(int j = 0; j < size.getY(); j++) { HashSet intersections = new HashSet<>(); for(Line2D.Float part: getWireframe(new Rectangle.Float(i, j, 1, 1))) { Point2D.Float intersection = calculateInterceptionPoint(line.getP1(), line.getP2(), part.getP1(), part.getP2()); if(intersection != null) { double dPa = intersection.distance(part.getP1()); double dPb = intersection.distance(part.getP2()); // sum of distances to points of the rectangle part == 1 => point lies on line segment // -----a---x----b----- // --x--a--------b----- if(dPa + dPb == 1) { double da = intersection.distance(line.getP1()); double db = intersection.distance(line.getP2()); // distance to a greater than to b => point lies on the ray from a over b // sum of distances analog to rectangle part => point lies between a and b // 0.000000005 because computers suck (missing precision) if(da > db || da + db <= l + 0.000000005) { intersections.add(intersection); if(intersections.size() > 1) { break; } } } } } if(intersections.size() > 1) { result.add(new IntVector2(i, j)); } } } return result; } //https://rosettacode.org/wiki/Find_the_intersection_of_two_lines#Java public static Point2D.Float calculateInterceptionPoint(Point2D s1, Point2D s2, Point2D d1, Point2D d2) { double a1 = s2.getY() - s1.getY(); double b1 = s1.getX() - s2.getX(); double c1 = a1 * s1.getX() + b1 * s1.getY(); double a2 = d2.getY() - d1.getY(); double b2 = d1.getX() - d2.getX(); double c2 = a2 * d1.getX() + b2 * d1.getY(); double delta = a1 * b2 - a2 * b1; if(delta == 0) { return null; } return new Point2D.Float((float) ((b2 * c1 - b1 * c2) / delta), (float) ((a1 * c2 - a2 * c1) / delta)); } /** Computes all wireframe lines for the given rectangle. */ public static ArrayList getWireframe(Rectangle.Float rect) { ArrayList result = new ArrayList<>(); Point2D.Float lt = new Point2D.Float(rect.x, rect.y); Point2D.Float rt = new Point2D.Float(rect.x + rect.width, rect.y); Point2D.Float rb = new Point2D.Float(rect.x + rect.width, rect.y + rect.height); Point2D.Float lb = new Point2D.Float(rect.x, rect.y + rect.height); result.add(new Line2D.Float(lt, rt)); result.add(new Line2D.Float(rt, rb)); result.add(new Line2D.Float(lb, rb)); result.add(new Line2D.Float(lt, lb)); return result; } //https://stackoverflow.com/a/5187612 public static ArrayList Bresenham4Connected(IntVector2 a, IntVector2 b) { ArrayList result = new ArrayList<>(); int x0 = a.getX(); int y0 = a.getY(); int x1 = b.getX(); int y1 = b.getY(); int dx = Math.abs(x1 - x0); int dy = Math.abs(y1 - y0); int ix = x0 < x1 ? 1 : -1; int iy = y0 < y1 ? 1 : -1; int e = 0; for(int i = 0; i < dx + dy; i++) { result.add(new IntVector2(x0, y0)); int e1 = e + dy; int e2 = e - dx; if(Math.abs(e1) < Math.abs(e2)) { x0 += ix; e = e1; }else { y0 += iy; e = e2; } } result.add(b); return result; } //https://www.programmersought.com/article/43476640523/ public static ArrayList Bresenham8Connected(IntVector2 a, IntVector2 b) { ArrayList result = new ArrayList<>(); int x0 = a.getX(); int y0 = a.getY(); int x1 = b.getX(); int y1 = b.getY(); int dx = Math.abs(x1 - x0); int dy = Math.abs(y1 - y0); int ix = x0 < x1 ? 1 : -1; int iy = y0 < y1 ? 1 : -1; int e = dx - dy; while(x0 != x1 || y0 != y1) { result.add(new IntVector2(x0, y0)); int e2 = e << 1; if(e2 > -dy) { e -= dy; x0 += ix; } if(e2 < dx) { e += dx; y0 += iy; } } result.add(b); return result; } }