diff --git a/pom.xml b/pom.xml index fcc0726..7c59dcc 100644 --- a/pom.xml +++ b/pom.xml @@ -79,7 +79,6 @@ dotenv-java 3.0.0 - diff --git a/src/main/java/cz/trailsthroughshadows/algorithm/Dungeon.java b/src/main/java/cz/trailsthroughshadows/algorithm/Dungeon.java index c542bb8..a96e0c0 100644 --- a/src/main/java/cz/trailsthroughshadows/algorithm/Dungeon.java +++ b/src/main/java/cz/trailsthroughshadows/algorithm/Dungeon.java @@ -1,232 +1,231 @@ -package cz.trailsthroughshadows.algorithm; - -import cz.trailsthroughshadows.algorithm.entity.Entity; -import cz.trailsthroughshadows.algorithm.location.LocationImpl; -import cz.trailsthroughshadows.algorithm.util.ListUtil; -import cz.trailsthroughshadows.api.table.action.Action; -import cz.trailsthroughshadows.api.table.action.attack.Attack; -import cz.trailsthroughshadows.api.table.action.movement.Movement; -import cz.trailsthroughshadows.api.table.action.restorecards.RestoreCards; -import cz.trailsthroughshadows.api.table.action.skill.Skill; -import cz.trailsthroughshadows.api.table.action.summon.Summon; -import cz.trailsthroughshadows.api.table.action.summon.SummonAction; -import cz.trailsthroughshadows.api.table.effect.Effect; -import cz.trailsthroughshadows.api.table.enemy.Enemy; -import cz.trailsthroughshadows.api.table.playerdata.character.Character; -import cz.trailsthroughshadows.api.table.schematic.hex.Hex; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -@Getter -@AllArgsConstructor -@Slf4j -public class Dungeon { - - private final ArrayList enemies = new ArrayList<>(); - private final ArrayList characters = new ArrayList<>(); - private final ArrayList summons = new ArrayList<>(); - private final LocationImpl location; - - public String entityToString(Entity entity) { - StringBuilder res = new StringBuilder(); - res.append("%s %s".formatted(entity.getClass().getSimpleName(), entity.getName())); - - if (entity instanceof Character character) - res.append(" (%s)".formatted(character.getPlayerName())); - - Hex hex = entity.getHex(); - if (hex != null) - res.append(" [%d]".formatted(entity.getHex().getKey().getId())); - else res.append(" [no hex]"); - - return res.toString(); - } - - public boolean isAlly(Entity e1, Entity e2) { - if ((e1 instanceof Character || e1 instanceof Summon) && (e2 instanceof Character || e2 instanceof Summon)) { - return true; - } - return e1 instanceof Enemy && e2 instanceof Enemy; - } - - public void applyEffect(Entity entity, Effect effect) { - log.info("\t\tApplying effect {} to {}", effect.toString(), entityToString(entity)); - - entity.getActiveEffects().add(effect); - // todo logic - } - - public void damageEntity(Entity entity, int damage) { - log.info("\t\tDamaging {} for {}", entityToString(entity), damage); - - switch (entity) { - case Character character -> log.info("\t\t\tCharacter can't be damaged yet"); // TODO - case Enemy enemy -> enemy.setBaseHealth(enemy.getBaseHealth() - damage); - case Summon summon -> summon.setHealth(summon.getHealth() - damage); - case null, default -> throw new IllegalStateException("Unexpected value: " + entity); - } - - if (entity instanceof Enemy enemy) { - if (enemy.getBaseHealth() <= 0) { - log.info("\t\t{} died", entityToString(entity)); - enemies.remove(enemy); - } - } - if (entity instanceof Summon summon) { - if (summon.getHealth() <= 0) { - log.info("\t\t{} died", entityToString(entity)); - summons.remove(summon); - } - } - } - - public void moveCharacter(Character character, Hex hex) { - character.setHex(hex); - } - - public void moveEnemy(Enemy enemy, Hex hex) { - enemy.setHex(hex); - } - - public List calculateTarget(Entity entity, Effect.EffectTarget target) { - return calculateTarget(entity, target, 0); - } - - public List calculateTarget(Entity entity, Effect.EffectTarget target, int range) { - List targets = new ArrayList<>(); - Hex hex = entity.getHex(); - - log.info("Calculating target for " + entity + " at " + hex + " with range " + range + " and target " + target); - - switch (target) { - case SELF: - targets.add(entity); - break; - case ALL_ALLIES: - targets.addAll(characters); - targets.addAll(summons); - break; - case ALL_ENEMIES: - targets.addAll(enemies); - break; - case ALL: - targets.addAll(ListUtil.union(characters, enemies, summons).stream() - .filter(ent -> location.getDistance(hex, ent.getHex()) <= range) - .toList()); - break; - case ONE: - ListUtil.union(characters, enemies, summons).stream() - .filter(ent -> location.getDistance(hex, ent.getHex()) <= range) - .min((ent1, ent2) -> location.getDistance(hex, ent1.getHex()) - location.getDistance(hex, ent2.getHex())) - .ifPresent(targets::add); - break; - default: - throw new IllegalStateException("Unexpected value: " + target); - } - - return targets.stream().sorted((ent1, ent2) -> location.getDistance(hex, ent1.getHex()) - location.getDistance(hex, ent2.getHex())).toList(); - } - - public void evaluateMovement(Entity entity, Movement movement) { - log.info("Evaluating movement {} ({}) for {}", movement.getType(), movement.getRange(), entityToString(entity)); - - // todo i dont want to do movement - } - - public void evaluateSummon(Entity entity, Summon summon, int range) { - log.info("\tEvaluating summon {} for {} with range {}", entityToString(summon), entityToString(entity), range); - - // todo - } - - public void evaluateSummons(Entity entity, Collection summons) { - log.info("Evaluating {} summons for {}", summons.size(), entityToString(entity)); - - for (SummonAction summonAction : summons) { - evaluateSummon(entity, summonAction.getSummon(), summonAction.getRange()); - } - } - - public void evaluateSkill(Entity entity, Skill skill) { - log.info("Evaluating skill {} for {}", skill.toString(), entityToString(entity)); - - for (Effect effect : skill.getEffects()) { - List targets = calculateTarget(entity, effect.getTarget(), skill.getRange()); - log.info("\tSkill: {} targets with range {}", targets.size(), skill.getRange()); - for (Entity target : targets) { - applyEffect(target, effect); - } - } - } - - public void evaluateAttack(Entity entity, Attack attack) { - log.info("Evaluating attack {} for {}", attack.toString(), entityToString(entity)); - - for (int i = 0; i < attack.getNumAttacks(); i++) { - List targets = calculateTarget(entity, attack.getTarget(), attack.getRange()); - log.info("\tAttack {}: {} targets with range {}", i + 1, targets.size(), attack.getRange()); - for (Entity target : targets) { - if (isAlly(entity, target)) { - log.info("\t\t{} and {} are friends", entityToString(entity), entityToString(target)); - continue; - } - - damageEntity(target, attack.getDamage()); - for (Effect effect : attack.getEffects()) { - applyEffect(target, effect); - } - } - } - } - - public void evaluateRestoreCards(Entity entity, RestoreCards restoreCards) { - log.info("Evaluating restoreCards {} ({}) for {}", restoreCards.getNumCards(), restoreCards.getTarget(), entityToString(entity)); - int remaining = restoreCards.getNumCards(); - - for (Entity target : calculateTarget(entity, restoreCards.getTarget())) { - if (!isAlly(entity, target)) continue; - - log.info("\tRestoring cards for {}", entityToString(target)); -// for (Action action : target.getActions()) { -// if (remaining == 0) { -// return; +//package cz.trailsthroughshadows.algorithm; +// +//import cz.trailsthroughshadows.algorithm.entity.Entity; +//import cz.trailsthroughshadows.algorithm.location.LocationImpl; +//import cz.trailsthroughshadows.algorithm.util.List; +//import cz.trailsthroughshadows.api.table.action.Action; +//import cz.trailsthroughshadows.api.table.action.attack.Attack; +//import cz.trailsthroughshadows.api.table.action.movement.Movement; +//import cz.trailsthroughshadows.api.table.action.restorecards.RestoreCards; +//import cz.trailsthroughshadows.api.table.action.skill.Skill; +//import cz.trailsthroughshadows.api.table.action.summon.Summon; +//import cz.trailsthroughshadows.api.table.action.summon.SummonAction; +//import cz.trailsthroughshadows.api.table.effect.Effect; +//import cz.trailsthroughshadows.api.table.enemy.Enemy; +//import cz.trailsthroughshadows.api.table.playerdata.character.Character; +//import cz.trailsthroughshadows.api.table.schematic.hex.Hex; +//import lombok.AllArgsConstructor; +//import lombok.Getter; +//import lombok.extern.slf4j.Slf4j; +// +//import java.util.ArrayList; +//import java.util.Collection; +// +//@Getter +//@AllArgsConstructor +//@Slf4j +//public class Dungeon { +// +// private final ArrayList enemies = new ArrayList<>(); +// private final ArrayList characters = new ArrayList<>(); +// private final ArrayList summons = new ArrayList<>(); +// private final LocationImpl location; +// +// public String entityToString(Entity entity) { +// StringBuilder res = new StringBuilder(); +// res.append("%s %s".formatted(entity.getClass().getSimpleName(), entity.getName())); +// +// if (entity instanceof Character character) +// res.append(" (%s)".formatted(character.getPlayerName())); +// +// Hex hex = entity.getHex(); +// if (hex != null) +// res.append(" [%d]".formatted(entity.getHex().getKey().getId())); +// else res.append(" [no hex]"); +// +// return res.toString(); +// } +// +// public boolean isAlly(Entity e1, Entity e2) { +// if ((e1 instanceof Character || e1 instanceof Summon) && (e2 instanceof Character || e2 instanceof Summon)) { +// return true; +// } +// return e1 instanceof Enemy && e2 instanceof Enemy; +// } +// +// public void applyEffect(Entity entity, Effect effect) { +// log.info("\t\tApplying effect {} to {}", effect.toString(), entityToString(entity)); +// +// entity.getActiveEffects().add(effect); +// // todo logic +// } +// +// public void damageEntity(Entity entity, int damage) { +// log.info("\t\tDamaging {} for {}", entityToString(entity), damage); +// +// switch (entity) { +// case Character character -> log.info("\t\t\tCharacter can't be damaged yet"); // TODO +// case Enemy enemy -> enemy.setBaseHealth(enemy.getBaseHealth() - damage); +// case Summon summon -> summon.setHealth(summon.getHealth() - damage); +// case null, default -> throw new IllegalStateException("Unexpected value: " + entity); +// } +// +// if (entity instanceof Enemy enemy) { +// if (enemy.getBaseHealth() <= 0) { +// log.info("\t\t{} died", entityToString(entity)); +// enemies.remove(enemy); +// } +// } +// if (entity instanceof Summon summon) { +// if (summon.getHealth() <= 0) { +// log.info("\t\t{} died", entityToString(entity)); +// summons.remove(summon); +// } +// } +// } +// +// public void moveCharacter(Character character, Hex hex) { +// character.setHex(hex); +// } +// +// public void moveEnemy(Enemy enemy, Hex hex) { +// enemy.setHex(hex); +// } +// +// public java.util.List calculateTarget(Entity entity, Effect.EffectTarget target) { +// return calculateTarget(entity, target, 0); +// } +// +// public java.util.List calculateTarget(Entity entity, Effect.EffectTarget target, int range) { +// java.util.List targets = new ArrayList<>(); +// Hex hex = entity.getHex(); +// +// log.info("Calculating target for " + entity + " at " + hex + " with range " + range + " and target " + target); +// +// switch (target) { +// case SELF: +// targets.add(entity); +// break; +// case ALL_ALLIES: +// targets.addAll(characters); +// targets.addAll(summons); +// break; +// case ALL_ENEMIES: +// targets.addAll(enemies); +// break; +// case ALL: +// targets.addAll(List.union(characters, enemies, summons).stream() +// .filter(ent -> location.getDistance(hex, ent.getHex()) <= range) +// .toList()); +// break; +// case ONE: +// List.union(characters, enemies, summons).stream() +// .filter(ent -> location.getDistance(hex, ent.getHex()) <= range) +// .min((ent1, ent2) -> location.getDistance(hex, ent1.getHex()) - location.getDistance(hex, ent2.getHex())) +// .ifPresent(targets::add); +// break; +// default: +// throw new IllegalStateException("Unexpected value: " + target); +// } +// +// return targets.stream().sorted((ent1, ent2) -> location.getDistance(hex, ent1.getHex()) - location.getDistance(hex, ent2.getHex())).toList(); +// } +// +// public void evaluateMovement(Entity entity, Movement movement) { +// log.info("Evaluating movement {} ({}) for {}", movement.getType(), movement.getRange(), entityToString(entity)); +// +// // todo i dont want to do movement +// } +// +// public void evaluateSummon(Entity entity, Summon summon, int range) { +// log.info("\tEvaluating summon {} for {} with range {}", entityToString(summon), entityToString(entity), range); +// +// // todo +// } +// +// public void evaluateSummons(Entity entity, Collection summons) { +// log.info("Evaluating {} summons for {}", summons.size(), entityToString(entity)); +// +// for (SummonAction summonAction : summons) { +// evaluateSummon(entity, summonAction.getSummon(), summonAction.getRange()); +// } +// } +// +// public void evaluateSkill(Entity entity, Skill skill) { +// log.info("Evaluating skill {} for {}", skill.toString(), entityToString(entity)); +// +// for (Effect effect : skill.getEffects()) { +// java.util.List targets = calculateTarget(entity, effect.getTarget(), skill.getRange()); +// log.info("\tSkill: {} targets with range {}", targets.size(), skill.getRange()); +// for (Entity target : targets) { +// applyEffect(target, effect); +// } +// } +// } +// +// public void evaluateAttack(Entity entity, Attack attack) { +// log.info("Evaluating attack {} for {}", attack.toString(), entityToString(entity)); +// +// for (int i = 0; i < attack.getNumAttacks(); i++) { +// java.util.List targets = calculateTarget(entity, attack.getTarget(), attack.getRange()); +// log.info("\tAttack {}: {} targets with range {}", i + 1, targets.size(), attack.getRange()); +// for (Entity target : targets) { +// if (isAlly(entity, target)) { +// log.info("\t\t{} and {} are friends", entityToString(entity), entityToString(target)); +// continue; // } // -// if (action.getDiscarded() && action.getDiscard() != Action.Discard.PERMANENT) { -// action.setDiscarded(false); -// log.info("\t\tRestored {} for {}", action.getTitle(), entityToString(target)); -// remaining -= 1; +// damageEntity(target, attack.getDamage()); +// for (Effect effect : attack.getEffects()) { +// applyEffect(target, effect); // } // } - } - } - - public void evaluateAction(Entity entity, Action action) { - log.info("Evaluating action {} for {}", action.getTitle(), entityToString(entity)); - log.info("\tAction: {}", action); - if (action.getMovement() != null) { - evaluateMovement(entity, action.getMovement()); - } - if (action.getSummonActions() != null) { - evaluateSummons(entity, action.getSummonActions()); - } - if (action.getSkill() != null) { - evaluateSkill(entity, action.getSkill()); - } - if (action.getAttack() != null) { - evaluateAttack(entity, action.getAttack()); - } - if (action.getRestoreCards() != null) { - evaluateRestoreCards(entity, action.getRestoreCards()); - } - - if (action.getDiscard() != Action.Discard.NEVER) { - action.setDiscarded(true); - } - } -} +// } +// } +// +// public void evaluateRestoreCards(Entity entity, RestoreCards restoreCards) { +// log.info("Evaluating restoreCards {} ({}) for {}", restoreCards.getNumCards(), restoreCards.getTarget(), entityToString(entity)); +// int remaining = restoreCards.getNumCards(); +// +// for (Entity target : calculateTarget(entity, restoreCards.getTarget())) { +// if (!isAlly(entity, target)) continue; +// +// log.info("\tRestoring cards for {}", entityToString(target)); +//// for (Action action : target.getActions()) { +//// if (remaining == 0) { +//// return; +//// } +//// +//// if (action.getDiscarded() && action.getDiscard() != Action.Discard.PERMANENT) { +//// action.setDiscarded(false); +//// log.info("\t\tRestored {} for {}", action.getTitle(), entityToString(target)); +//// remaining -= 1; +//// } +//// } +// } +// } +// +// public void evaluateAction(Entity entity, Action action) { +// log.info("Evaluating action {} for {}", action.getTitle(), entityToString(entity)); +// log.info("\tAction: {}", action); +// if (action.getMovement() != null) { +// evaluateMovement(entity, action.getMovement()); +// } +// if (action.getSummonActions() != null) { +// evaluateSummons(entity, action.getSummonActions()); +// } +// if (action.getSkill() != null) { +// evaluateSkill(entity, action.getSkill()); +// } +// if (action.getAttack() != null) { +// evaluateAttack(entity, action.getAttack()); +// } +// if (action.getRestoreCards() != null) { +// evaluateRestoreCards(entity, action.getRestoreCards()); +// } +// +// if (action.getDiscard() != Action.Discard.NEVER) { +// action.setDiscarded(true); +// } +// } +//} diff --git a/src/main/java/cz/trailsthroughshadows/algorithm/entity/Entity.java b/src/main/java/cz/trailsthroughshadows/algorithm/entity/Entity.java index 35fb48b..76b8d1e 100644 --- a/src/main/java/cz/trailsthroughshadows/algorithm/entity/Entity.java +++ b/src/main/java/cz/trailsthroughshadows/algorithm/entity/Entity.java @@ -17,15 +17,8 @@ public abstract class Entity { public Hex hex; @Transient public List activeEffects = new ArrayList<>(); - @Column(nullable = false) - public CombatStyle combatStyle = CombatStyle.MELEE; public abstract String getName(); // public abstract List getActions(); - - enum CombatStyle { - MELEE, - RANGED, - } } diff --git a/src/main/java/cz/trailsthroughshadows/algorithm/location/LocationImpl.java b/src/main/java/cz/trailsthroughshadows/algorithm/location/LocationImpl.java index 2576ffb..a291902 100644 --- a/src/main/java/cz/trailsthroughshadows/algorithm/location/LocationImpl.java +++ b/src/main/java/cz/trailsthroughshadows/algorithm/location/LocationImpl.java @@ -1,12 +1,9 @@ package cz.trailsthroughshadows.algorithm.location; -import cz.trailsthroughshadows.algorithm.utils.Vec3; import cz.trailsthroughshadows.api.table.schematic.hex.Hex; import cz.trailsthroughshadows.api.table.schematic.location.ILocation; import cz.trailsthroughshadows.api.table.schematic.part.Part; -import java.util.List; - public abstract class LocationImpl implements ILocation { public Part getPart(Hex hex) { @@ -15,21 +12,4 @@ public Part getPart(Hex hex) { .findFirst() .orElse(null); } - - public int getDistance(Hex hex1, Hex hex2) { - Vec3 vec = new Vec3<>(hex1.getQ() - hex2.getQ(), hex1.getR() - hex2.getR(), hex1.getS() - hex2.getS()); - return (Math.abs(vec.x()) + Math.abs(vec.y()) + Math.abs(vec.z())) / 2; - } - - public List getNeighbors(Hex hex) { - return getNeighbors(hex, 1); - } - - public List getNeighbors(Hex hex, int range) { - List neighbors = getPart(hex).getHexes().stream() - .filter(neighbor -> hex != neighbor && getDistance(hex, neighbor) <= range) - .toList(); - - return neighbors; - } } diff --git a/src/main/java/cz/trailsthroughshadows/algorithm/location/Navigation.java b/src/main/java/cz/trailsthroughshadows/algorithm/location/Navigation.java new file mode 100644 index 0000000..1511eb9 --- /dev/null +++ b/src/main/java/cz/trailsthroughshadows/algorithm/location/Navigation.java @@ -0,0 +1,93 @@ +package cz.trailsthroughshadows.algorithm.location; + +import cz.trailsthroughshadows.algorithm.util.Vec3; +import cz.trailsthroughshadows.api.table.schematic.hex.Hex; +import cz.trailsthroughshadows.api.table.schematic.part.Part; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.*; + +@RequiredArgsConstructor +@Slf4j +public class Navigation { + + private final List parts; + + public Navigation(Part... parts) { + this.parts = Arrays.asList(parts); + } + + public Part getPart(Hex hex) { + return parts.stream() + .filter(part -> part.getId() == hex.getKey().getIdPart()) + .findFirst() + .orElse(null); + } + + public int getDistance(Hex hex1, Hex hex2) { + Vec3 vec = new Vec3<>(hex1.getQ() - hex2.getQ(), hex1.getR() - hex2.getR(), hex1.getS() - hex2.getS()); + return (Math.abs(vec.x()) + Math.abs(vec.y()) + Math.abs(vec.z())) / 2; + } + + public List getNeighbors(Hex hex) { + return getNeighbors(hex, 1); + } + + public List getNeighbors(Hex hex, int range) { + return getPart(hex).getHexes().stream() + .filter(neighbor -> hex != neighbor && getDistance(hex, neighbor) <= range) + .toList(); + } + + public List getPath(Hex hex1, Hex hex2) { + Map distances = new HashMap<>(); + Queue queue = new LinkedList<>(); + List path = new ArrayList<>(); + + distances.put(hex1, 0); + queue.add(hex1); + + // calculate distance from hex1 + while (!queue.isEmpty()) { + Hex current = queue.poll(); + int distance = distances.get(current); + + List neighbors = getNeighbors(current); + for (Hex neighbor : neighbors) { + if (distances.containsKey(neighbor)) + continue; + + distances.put(neighbor, distance + 1); + queue.add(neighbor); + } + } + + // hex2 is not reachable from hex1 + if (!distances.containsKey(hex2)) + return null; + + // create path to hex2 + Hex currentHex = hex2; + int currentDistance = distances.get(hex2); + + while (!currentHex.equals(hex1)) { + path.add(currentHex); + + List neighbors = getNeighbors(currentHex); + for (Hex neighbor : neighbors) { + int neighborDistance = distances.get(neighbor); + + if (neighborDistance < currentDistance) { + currentHex = neighbor; + currentDistance = neighborDistance; + break; + } + } + } + path.add(hex1); + + Collections.reverse(path); + return path; + } +} diff --git a/src/main/java/cz/trailsthroughshadows/algorithm/util/ListUtil.java b/src/main/java/cz/trailsthroughshadows/algorithm/util/List.java similarity index 66% rename from src/main/java/cz/trailsthroughshadows/algorithm/util/ListUtil.java rename to src/main/java/cz/trailsthroughshadows/algorithm/util/List.java index 798a653..06829d1 100644 --- a/src/main/java/cz/trailsthroughshadows/algorithm/util/ListUtil.java +++ b/src/main/java/cz/trailsthroughshadows/algorithm/util/List.java @@ -1,17 +1,16 @@ package cz.trailsthroughshadows.algorithm.util; import java.util.ArrayList; -import java.util.List; -public class ListUtil { +public class List { public static T getRandom(T[] array) { return array[(int) (Math.random() * array.length)]; } @SafeVarargs - public static List union(List... lists) { + public static java.util.List union(java.util.List... lists) { ArrayList union = new ArrayList<>(); - for (List list : lists) { + for (java.util.List list : lists) { union.addAll(list); } return union; diff --git a/src/main/java/cz/trailsthroughshadows/algorithm/util/Vec3.java b/src/main/java/cz/trailsthroughshadows/algorithm/util/Vec3.java new file mode 100644 index 0000000..e483fa3 --- /dev/null +++ b/src/main/java/cz/trailsthroughshadows/algorithm/util/Vec3.java @@ -0,0 +1,4 @@ +package cz.trailsthroughshadows.algorithm.util; + +public record Vec3(T x, T y, T z) { +} diff --git a/src/main/java/cz/trailsthroughshadows/algorithm/utils/Vec3.java b/src/main/java/cz/trailsthroughshadows/algorithm/utils/Vec3.java deleted file mode 100644 index 719ef3d..0000000 --- a/src/main/java/cz/trailsthroughshadows/algorithm/utils/Vec3.java +++ /dev/null @@ -1,4 +0,0 @@ -package cz.trailsthroughshadows.algorithm.utils; - -public record Vec3(T x, T y, T z) { -} diff --git a/src/main/java/cz/trailsthroughshadows/api/rest/endpoints/ValidationController.java b/src/main/java/cz/trailsthroughshadows/api/rest/endpoints/ValidationController.java index 6458b98..baf0cb4 100644 --- a/src/main/java/cz/trailsthroughshadows/api/rest/endpoints/ValidationController.java +++ b/src/main/java/cz/trailsthroughshadows/api/rest/endpoints/ValidationController.java @@ -5,7 +5,8 @@ import cz.trailsthroughshadows.api.rest.model.error.RestError; import cz.trailsthroughshadows.api.rest.model.error.type.MessageError; import cz.trailsthroughshadows.api.table.schematic.part.Part; -import lombok.extern.slf4j.Slf4j; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; @@ -13,42 +14,37 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import java.util.ArrayList; import java.util.List; -@Slf4j +@Log4j2 @Component @RestController(value = "validation") public class ValidationController { + private ValidationService validationService; + @PostMapping("/validate/part") public ResponseEntity validatePart(@RequestBody Part part) { - List errors = new ArrayList<>(); - - // TODO: Implement Part validation @Bačkorče - // Validations?: - // 1. Part must have at least 5 hexes - // 2. Part must have at most 50 hexes - // 3. Part is maximum 8 hexes wide and 8 hexes tall - // 4. All hexes must be connected - - if (part.getHexes().size() < 5) { - errors.add("Part must have at least 5 hexes!"); - } - - if (part.getHexes().size() > 50) { - errors.add("Part must have at most 50 hexes!"); - } + log.debug("Validating part " + part.getTag()); + List errors = validationService.validatePart(part); if (errors.isEmpty()) { - return RestResponse.of(HttpStatus.OK,"Part is valid."); + log.debug("Part is valid!"); + return new ResponseEntity<>(new RestResponse(HttpStatus.OK, "Part is valid!"), HttpStatus.OK); } + log.debug("Part is not valid!"); RestError error = new RestError(HttpStatus.NOT_ACCEPTABLE, "Part is not valid!"); for (var e : errors) { + log.debug(" > " + e); error.addSubError(new MessageError(e)); } throw new RestException(error); } + + @Autowired + public void setValidationService(ValidationService validationService) { + this.validationService = validationService; + } } diff --git a/src/main/java/cz/trailsthroughshadows/api/rest/endpoints/ValidationService.java b/src/main/java/cz/trailsthroughshadows/api/rest/endpoints/ValidationService.java new file mode 100644 index 0000000..7d234d7 --- /dev/null +++ b/src/main/java/cz/trailsthroughshadows/api/rest/endpoints/ValidationService.java @@ -0,0 +1,94 @@ +package cz.trailsthroughshadows.api.rest.endpoints; + +import cz.trailsthroughshadows.algorithm.location.Navigation; +import cz.trailsthroughshadows.api.table.schematic.hex.Hex; +import cz.trailsthroughshadows.api.table.schematic.part.Part; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +@Slf4j +public class ValidationService { + + // TODO: Implement Part validation @Bačkorče + // Validations?: + // 1. Part must have at least 5 hexes + // 2. Part must have at most 50 hexes + // 3. Part is maximum 8 hexes wide and 8 hexes tall + // 4. All hexes must be connected + public List validatePart(Part part) { + List errors = new ArrayList<>(); + + int minHexes = 5; + int maxHexes = 50; + int maxHexesWide = 8; + + // min 5 hexes + if (part.getHexes().size() < minHexes) { + errors.add("Part must have at least %d hexes!".formatted(minHexes)); + } + + // max 50 hexes + if (part.getHexes().size() > maxHexes) { + errors.add("Part must have at most 50 hexes!"); + } + + // max 8 hexes wide + int diffQ = part.getHexes().stream().mapToInt(Hex::getQ).max().getAsInt() - part.getHexes().stream().mapToInt(Hex::getQ).min().getAsInt(); + int diffR = part.getHexes().stream().mapToInt(Hex::getR).max().getAsInt() - part.getHexes().stream().mapToInt(Hex::getR).min().getAsInt(); + int diffS = part.getHexes().stream().mapToInt(Hex::getS).max().getAsInt() - part.getHexes().stream().mapToInt(Hex::getS).min().getAsInt(); + if (diffQ > maxHexesWide || diffR > maxHexesWide || diffS > maxHexesWide) { + errors.add("Part must not be wider than %d hexes!".formatted(maxHexesWide)); + } + + // no hexes can be on the same position + int duplicates = 0; + for (Hex hex1 : part.getHexes()) { + for (Hex hex2 : part.getHexes()) { + if (hex1 == hex2) + continue; + + if (hex1.getQ() == hex2.getQ() && hex1.getR() == hex2.getR() && hex1.getS() == hex2.getS()) { + duplicates++; + break; + } + } + } + if (duplicates > 0) + errors.add("Part must not have duplicate hexes!"); + + // every hex has to have correct coordinates + for (Hex hex : part.getHexes()) { + if (hex.getQ() + hex.getR() + hex.getS() != 0) { + errors.add("Every hex has to have correct coordinates!"); + break; + } + } + + // must include center hex + Optional centerHex = part.getHexes().stream().filter(hex -> hex.getQ() == 0 && hex.getR() == 0 && hex.getS() == 0).findFirst(); + if (centerHex.isEmpty()) { + errors.add("Part must include a center hex!"); + return errors; + } + + // all hexes must be connected + Navigation navigation = new Navigation(part); + + for (Hex hex : part.getHexes()) { + if (hex == centerHex.get()) + continue; + + if (navigation.getPath(centerHex.get(), hex) == null) { + errors.add("All hexes must be connected!"); + break; + } + } + + return errors; + } +} diff --git a/src/main/java/cz/trailsthroughshadows/api/table/schematic/hex/Hex.java b/src/main/java/cz/trailsthroughshadows/api/table/schematic/hex/Hex.java index 1d1a48d..8a337e9 100644 --- a/src/main/java/cz/trailsthroughshadows/api/table/schematic/hex/Hex.java +++ b/src/main/java/cz/trailsthroughshadows/api/table/schematic/hex/Hex.java @@ -26,13 +26,6 @@ public class Hex { @Column(name = "sCord", nullable = false) private int s; - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Hex hex)) return false; - return q == hex.q && r == hex.r && s == hex.s && key.equals(hex.key); - } - @Data @Embeddable public static class HexId implements Serializable { diff --git a/src/main/java/cz/trailsthroughshadows/api/table/schematic/part/Part.java b/src/main/java/cz/trailsthroughshadows/api/table/schematic/part/Part.java index ef1a023..ac47ea5 100644 --- a/src/main/java/cz/trailsthroughshadows/api/table/schematic/part/Part.java +++ b/src/main/java/cz/trailsthroughshadows/api/table/schematic/part/Part.java @@ -1,21 +1,18 @@ package cz.trailsthroughshadows.api.table.schematic.part; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import cz.trailsthroughshadows.api.table.schematic.hex.Hex; -import cz.trailsthroughshadows.api.table.schematic.hex.LocationDoor; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import java.util.Set; +import java.util.List; @Data @Entity @NoArgsConstructor @AllArgsConstructor @Table(name = "Part") -@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) public class Part { @Id @@ -25,11 +22,11 @@ public class Part { @Column(name = "tag") private String tag; - @OneToMany(mappedBy = "key.idPart", cascade = CascadeType.ALL) - private Set hexes; + @OneToMany(mappedBy = "key.idPart", cascade = CascadeType.ALL, orphanRemoval = true) + private List hexes; - @OneToMany(mappedBy = "key.fromPart", cascade = CascadeType.ALL) - private Set doors; +// @OneToMany(mappedBy = "key.fromPart", cascade = CascadeType.ALL) +// private Set doors; @Column(name = "usages", columnDefinition = "INT default 0") private int usages = 0; diff --git a/src/main/java/cz/trailsthroughshadows/api/table/schematic/part/PartController.java b/src/main/java/cz/trailsthroughshadows/api/table/schematic/part/PartController.java index cfa01b7..549f03c 100644 --- a/src/main/java/cz/trailsthroughshadows/api/table/schematic/part/PartController.java +++ b/src/main/java/cz/trailsthroughshadows/api/table/schematic/part/PartController.java @@ -78,12 +78,16 @@ public ResponseEntity updatePartById(@PathVariable int id, @Reques .orElseThrow(() -> RestException.of(HttpStatus.NOT_FOUND, "Part with id '%d' not found!", id)); partToUpdate.setTag(part.getTag()); - partToUpdate.setHexes(part.getHexes()); + +// partToUpdate.setHexes(part.getHexes()); + partToUpdate.getHexes().retainAll(part.getHexes()); + partToUpdate.getHexes().addAll(part.getHexes()); // TODO: It's not removing part hexes, but it's adding new ones or updating existing ones // TODO: Validation for new or updates parts // Issue: https://github.com/Trails-Through-Shadows/TTS-API/issues/28 partRepo.save(partToUpdate); + return RestResponse.of(HttpStatus.OK,"Part updated!"); } diff --git a/src/main/java/cz/trailsthroughshadows/api/table/schematic/part/PartRepo.java b/src/main/java/cz/trailsthroughshadows/api/table/schematic/part/PartRepo.java index 2415e39..9ffe9e5 100644 --- a/src/main/java/cz/trailsthroughshadows/api/table/schematic/part/PartRepo.java +++ b/src/main/java/cz/trailsthroughshadows/api/table/schematic/part/PartRepo.java @@ -11,11 +11,11 @@ public interface PartRepo extends JpaRepository { @Override - @EntityGraph(attributePaths = {"hexes", "doors"}) + @EntityGraph(attributePaths = {"hexes"}) List findAll(); @Override - @EntityGraph(attributePaths = {"hexes", "doors"}) + @EntityGraph(attributePaths = {"hexes"}) Optional findById(Integer id); diff --git a/src/main/java/cz/trailsthroughshadows/api/util/Pair.java b/src/main/java/cz/trailsthroughshadows/api/util/Pair.java index 4f716a5..3ed2319 100644 --- a/src/main/java/cz/trailsthroughshadows/api/util/Pair.java +++ b/src/main/java/cz/trailsthroughshadows/api/util/Pair.java @@ -1,5 +1,5 @@ package cz.trailsthroughshadows.api.util; -public record Pair() { +public record Pair(A first, B second) { } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d94ac70..004bd72 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -19,7 +19,7 @@ server.forward-headers-strategy=framework # Logging logging.level.root=INFO logging.level.cz.trailsthroughshadows.api=DEBUG -logging.level.cz.trailsthroughshadows.algorithm=INFO +logging.level.cz.trailsthroughshadows.algorithm=DEBUG logging.config=classpath:log4j2.xml #jackson configuration