diff --git a/pom.xml b/pom.xml index 967cf3f7..fb49d7e2 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,18 @@ 5.7.0 test + + org.mockito + mockito-core + 3.6.0 + test + + + org.mockito + mockito-junit-jupiter + 3.6.0 + test + com.esotericsoftware kryo diff --git a/src/main/java/nomadrealms/app/context/DeckEditingContext.java b/src/main/java/nomadrealms/app/context/DeckEditingContext.java index b92c4d44..10e3d775 100644 --- a/src/main/java/nomadrealms/app/context/DeckEditingContext.java +++ b/src/main/java/nomadrealms/app/context/DeckEditingContext.java @@ -40,7 +40,7 @@ public class DeckEditingContext extends GameContext { private DeckList deckList1 = BeginnerDecks.RUNNING_AND_WALKING.deckList(); private DeckList deckList2 = BeginnerDecks.AGRICULTURE_AND_LABOUR.deckList(); - private DeckList deckList3 = BeginnerDecks.CYCLE_AND_SEARCH.deckList(); + private DeckList deckList3 = BeginnerDecks.FIRE_AND_ICE.deckList(); private DeckList deckList4 = BeginnerDecks.PUNCH_AND_GRAPPLE.deckList(); @Override diff --git a/src/main/java/nomadrealms/context/game/card/GameCard.java b/src/main/java/nomadrealms/context/game/card/GameCard.java index 4d055b6c..643fd668 100644 --- a/src/main/java/nomadrealms/context/game/card/GameCard.java +++ b/src/main/java/nomadrealms/context/game/card/GameCard.java @@ -18,9 +18,11 @@ import nomadrealms.context.game.card.expression.SurfaceCardExpression; import nomadrealms.context.game.card.expression.TeleportExpression; import nomadrealms.context.game.card.expression.TeleportNoTargetExpression; +import nomadrealms.context.game.card.query.actor.ActorsOnTilesQuery; import nomadrealms.context.game.card.query.actor.SelfQuery; import nomadrealms.context.game.card.query.card.LastResolvedCardQuery; import nomadrealms.context.game.card.query.tile.PreviousTileQuery; +import nomadrealms.context.game.card.query.tile.TilesInRadiusQuery; import nomadrealms.context.game.card.target.TargetingInfo; import nomadrealms.context.game.world.map.tile.factory.TileType; @@ -100,7 +102,12 @@ public enum GameCard implements Card { "Wooden Chest", "Create a chest on target tile", new CreateStructureExpression(StructureType.CHEST), - new TargetingInfo(HEXAGON, 1)); + new TargetingInfo(HEXAGON, 1)), + FLAME_CIRCLE( + "Flame Circle", + "Deal 4 damage to all other actors within radius 3", + new DamageExpression(new ActorsOnTilesQuery(new TilesInRadiusQuery(new SelfQuery(), 3)), 4), + new TargetingInfo(NONE, 0)); private final String title; private final String description; diff --git a/src/main/java/nomadrealms/context/game/card/expression/DamageExpression.java b/src/main/java/nomadrealms/context/game/card/expression/DamageExpression.java index fca9602a..afa3ee4c 100644 --- a/src/main/java/nomadrealms/context/game/card/expression/DamageExpression.java +++ b/src/main/java/nomadrealms/context/game/card/expression/DamageExpression.java @@ -3,23 +3,37 @@ import static java.util.Collections.singletonList; import java.util.List; +import java.util.stream.Collectors; +import nomadrealms.context.game.actor.cardplayer.CardPlayer; import nomadrealms.context.game.card.intent.DamageIntent; import nomadrealms.context.game.card.intent.Intent; +import nomadrealms.context.game.card.query.Query; import nomadrealms.context.game.event.Target; -import nomadrealms.context.game.actor.cardplayer.CardPlayer; import nomadrealms.context.game.world.World; public class DamageExpression implements CardExpression { private final int amount; + private final Query query; public DamageExpression(int amount) { this.amount = amount; + this.query = null; + } + + public DamageExpression(Query query, int amount) { + this.amount = amount; + this.query = query; } @Override public List intents(World world, Target target, CardPlayer source) { + if (query != null) { + return query.find(world, source).stream() + .map(t -> new DamageIntent(t, source, amount)) + .collect(Collectors.toList()); + } return singletonList(new DamageIntent(target, source, amount)); } diff --git a/src/main/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQuery.java b/src/main/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQuery.java new file mode 100644 index 00000000..45fef9a3 --- /dev/null +++ b/src/main/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQuery.java @@ -0,0 +1,32 @@ +package nomadrealms.context.game.card.query.actor; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import nomadrealms.context.game.actor.Actor; +import nomadrealms.context.game.actor.cardplayer.CardPlayer; +import nomadrealms.context.game.card.query.Query; +import nomadrealms.context.game.world.World; +import nomadrealms.context.game.world.map.area.Tile; + +public class ActorsOnTilesQuery implements Query { + + private final Query tilesQuery; + + public ActorsOnTilesQuery(Query tilesQuery) { + this.tilesQuery = tilesQuery; + } + + @Override + public List find(World world, CardPlayer source) { + return tilesQuery.find(world, source).stream() + .map(tile -> world.getTargetOnTile(tile)) + .filter(Objects::nonNull) + .filter(entity -> entity instanceof Actor) + .map(entity -> (Actor) entity) + .filter(actor -> actor != source) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQuery.java b/src/main/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQuery.java new file mode 100644 index 00000000..6cacf45f --- /dev/null +++ b/src/main/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQuery.java @@ -0,0 +1,71 @@ +package nomadrealms.context.game.card.query.tile; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import nomadrealms.context.game.actor.HasPosition; +import nomadrealms.context.game.actor.cardplayer.CardPlayer; +import nomadrealms.context.game.card.query.Query; +import nomadrealms.context.game.world.World; +import nomadrealms.context.game.world.map.area.Tile; +import nomadrealms.context.game.world.map.area.coordinate.TileCoordinate; + +public class TilesInRadiusQuery implements Query { + + private final Query centerQuery; + private final int radius; + + public TilesInRadiusQuery(Query centerQuery, int radius) { + this.centerQuery = centerQuery; + this.radius = radius; + } + + @Override + public List find(World world, CardPlayer source) { + List centers = centerQuery.find(world, source); + if (centers.isEmpty()) { + return Collections.emptyList(); + } + + Tile centerTile = centers.get(0).tile(); + if (centerTile == null) { + return Collections.emptyList(); + } + + Set visited = new HashSet<>(); + List frontier = new ArrayList<>(); + List tilesInRadius = new ArrayList<>(); + + frontier.add(centerTile); + visited.add(centerTile); + tilesInRadius.add(centerTile); + + for (int i = 0; i < radius; i++) { + List nextFrontier = new ArrayList<>(); + for (Tile tile : frontier) { + addNeighbor(world, nextFrontier, visited, tilesInRadius, tile.coord().ul()); + addNeighbor(world, nextFrontier, visited, tilesInRadius, tile.coord().um()); + addNeighbor(world, nextFrontier, visited, tilesInRadius, tile.coord().ur()); + addNeighbor(world, nextFrontier, visited, tilesInRadius, tile.coord().dl()); + addNeighbor(world, nextFrontier, visited, tilesInRadius, tile.coord().dm()); + addNeighbor(world, nextFrontier, visited, tilesInRadius, tile.coord().dr()); + } + frontier = nextFrontier; + } + + return tilesInRadius; + } + + private void addNeighbor(World world, List nextFrontier, Set visited, List tilesInRadius, TileCoordinate neighborCoord) { + Tile neighbor = world.getTile(neighborCoord); + if (neighbor != null && !visited.contains(neighbor)) { + visited.add(neighbor); + nextFrontier.add(neighbor); + tilesInRadius.add(neighbor); + } + } +} diff --git a/src/main/java/nomadrealms/context/game/zone/BeginnerDecks.java b/src/main/java/nomadrealms/context/game/zone/BeginnerDecks.java index 8c7afe5d..819ae5b6 100644 --- a/src/main/java/nomadrealms/context/game/zone/BeginnerDecks.java +++ b/src/main/java/nomadrealms/context/game/zone/BeginnerDecks.java @@ -2,6 +2,7 @@ import static nomadrealms.context.game.card.GameCard.ATTACK; import static nomadrealms.context.game.card.GameCard.CREATE_ROCK; +import static nomadrealms.context.game.card.GameCard.FLAME_CIRCLE; import static nomadrealms.context.game.card.GameCard.GATHER; import static nomadrealms.context.game.card.GameCard.HEAL; import static nomadrealms.context.game.card.GameCard.MELEE_ATTACK; @@ -22,9 +23,14 @@ public enum BeginnerDecks { UNSTABLE_TELEPORT, REWIND )), - PUNCH_AND_GRAPPLE("Punch & Grapple", new DeckList(HEAL, ATTACK, HEAL, MELEE_ATTACK)), + PUNCH_AND_GRAPPLE("Punch & Grapple", new DeckList(HEAL, ATTACK, HEAL, MELEE_ATTACK, FLAME_CIRCLE)), CYCLE_AND_SEARCH("Cycle & Search ", new DeckList()), - AGRICULTURE_AND_LABOUR("Agriculture & Labour", new DeckList(GATHER, CREATE_ROCK, WOODEN_CHEST, TILL_SOIL, PLANT_SEED)); + AGRICULTURE_AND_LABOUR("Agriculture & Labour", new DeckList(GATHER, CREATE_ROCK, WOODEN_CHEST, TILL_SOIL, + PLANT_SEED)), + FIRE_AND_ICE("Fire & Ice", new DeckList( + FLAME_CIRCLE + )), + ; private final String name; private final DeckList deckList; diff --git a/src/test/java/nomadrealms/context/game/card/expression/DamageExpressionTest.java b/src/test/java/nomadrealms/context/game/card/expression/DamageExpressionTest.java new file mode 100644 index 00000000..0d9a3dd0 --- /dev/null +++ b/src/test/java/nomadrealms/context/game/card/expression/DamageExpressionTest.java @@ -0,0 +1,50 @@ +package nomadrealms.context.game.card.expression; + +import static java.util.Arrays.asList; +import static nomadrealms.context.game.card.GameCard.FLAME_CIRCLE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +import java.util.List; + +import nomadrealms.context.game.actor.Actor; +import nomadrealms.context.game.actor.cardplayer.CardPlayer; +import nomadrealms.context.game.card.intent.Intent; +import nomadrealms.context.game.card.query.Query; +import nomadrealms.context.game.world.World; +import org.junit.jupiter.api.Test; + +class DamageExpressionTest { + + @Test + void testQueryBasedDamage() { + World world = mock(World.class); + CardPlayer source = mock(CardPlayer.class); + Actor target1 = mock(Actor.class); + Actor target2 = mock(Actor.class); + + Query query = (w, s) -> asList(target1, target2); + + DamageExpression expression = new DamageExpression(query, 5); + List intents = expression.intents(world, null, source); + + assertEquals(2, intents.size()); + } + + @Test + void testFlameCircleCard() { + World world = mock(World.class); + CardPlayer source = mock(CardPlayer.class); + Actor target1 = mock(Actor.class); + Actor target2 = mock(Actor.class); + + // This is a simplified test. The query logic is tested separately. + // Here we just need to make sure the card uses the expression correctly. + CardExpression expression = FLAME_CIRCLE.expression(); + List intents = expression.intents(world, null, source); + + // We can't easily mock the chained queries, so we'll just check that + // the expression is a DamageExpression. + assertEquals(DamageExpression.class, expression.getClass()); + } +} diff --git a/src/test/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQueryTest.java b/src/test/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQueryTest.java new file mode 100644 index 00000000..6ab06ffa --- /dev/null +++ b/src/test/java/nomadrealms/context/game/card/query/actor/ActorsOnTilesQueryTest.java @@ -0,0 +1,60 @@ +package nomadrealms.context.game.card.query.actor; + +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import nomadrealms.context.game.actor.Actor; +import nomadrealms.context.game.actor.cardplayer.CardPlayer; +import nomadrealms.context.game.card.query.Query; +import nomadrealms.context.game.world.World; +import nomadrealms.context.game.world.map.area.Tile; +import org.junit.jupiter.api.Test; + +class ActorsOnTilesQueryTest { + + @Test + void testFind() { + World world = mock(World.class); + CardPlayer source = mock(CardPlayer.class); + Tile tile1 = mock(Tile.class); + Tile tile2 = mock(Tile.class); + Actor actor1 = mock(Actor.class); + Actor actor2 = mock(Actor.class); + + Query tilesQuery = (w, s) -> asList(tile1, tile2); + + when(world.getTargetOnTile(tile1)).thenReturn(actor1); + when(world.getTargetOnTile(tile2)).thenReturn(actor2); + + ActorsOnTilesQuery query = new ActorsOnTilesQuery(tilesQuery); + List result = query.find(world, source); + + assertEquals(2, result.size()); + assertEquals(actor1, result.get(0)); + assertEquals(actor2, result.get(1)); + } + + @Test + void testFind_excludesSource() { + World world = mock(World.class); + CardPlayer source = mock(CardPlayer.class); + Tile tile1 = mock(Tile.class); + Tile tile2 = mock(Tile.class); + Actor actor1 = mock(Actor.class); + + Query tilesQuery = (w, s) -> List.of(tile1, tile2); + + when(world.getTargetOnTile(tile1)).thenReturn(actor1); + when(world.getTargetOnTile(tile2)).thenReturn(source); + + ActorsOnTilesQuery query = new ActorsOnTilesQuery(tilesQuery); + List result = query.find(world, source); + + assertEquals(1, result.size()); + assertEquals(actor1, result.get(0)); + } +} diff --git a/src/test/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQueryTest.java b/src/test/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQueryTest.java new file mode 100644 index 00000000..5d33fb3a --- /dev/null +++ b/src/test/java/nomadrealms/context/game/card/query/tile/TilesInRadiusQueryTest.java @@ -0,0 +1,60 @@ +package nomadrealms.context.game.card.query.tile; + +import org.junit.jupiter.api.Test; +import nomadrealms.context.game.actor.cardplayer.CardPlayer; +import nomadrealms.context.game.card.query.actor.SelfQuery; +import nomadrealms.context.game.world.World; +import nomadrealms.context.game.world.map.area.Tile; +import nomadrealms.context.game.world.map.area.coordinate.TileCoordinate; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +class TilesInRadiusQueryTest { + + @Test + void testFind() { + World world = mock(World.class); + CardPlayer source = mock(CardPlayer.class); + Tile centerTile = mock(Tile.class); + TileCoordinate centerCoord = mock(TileCoordinate.class); + + when(source.tile()).thenReturn(centerTile); + when(centerTile.coord()).thenReturn(centerCoord); + + Tile ul = mock(Tile.class); + Tile um = mock(Tile.class); + Tile ur = mock(Tile.class); + Tile dl = mock(Tile.class); + Tile dm = mock(Tile.class); + Tile dr = mock(Tile.class); + + TileCoordinate ulCoord = mock(TileCoordinate.class); + TileCoordinate umCoord = mock(TileCoordinate.class); + TileCoordinate urCoord = mock(TileCoordinate.class); + TileCoordinate dlCoord = mock(TileCoordinate.class); + TileCoordinate dmCoord = mock(TileCoordinate.class); + TileCoordinate drCoord = mock(TileCoordinate.class); + + when(centerCoord.ul()).thenReturn(ulCoord); + when(centerCoord.um()).thenReturn(umCoord); + when(centerCoord.ur()).thenReturn(urCoord); + when(centerCoord.dl()).thenReturn(dlCoord); + when(centerCoord.dm()).thenReturn(dmCoord); + when(centerCoord.dr()).thenReturn(drCoord); + + when(world.getTile(ulCoord)).thenReturn(ul); + when(world.getTile(umCoord)).thenReturn(um); + when(world.getTile(urCoord)).thenReturn(ur); + when(world.getTile(dlCoord)).thenReturn(dl); + when(world.getTile(dmCoord)).thenReturn(dm); + when(world.getTile(drCoord)).thenReturn(dr); + + TilesInRadiusQuery query = new TilesInRadiusQuery(new SelfQuery(), 1); + List result = query.find(world, source); + + assertEquals(7, result.size()); + } +}