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 extends Target> query;
public DamageExpression(int amount) {
this.amount = amount;
+ this.query = null;
+ }
+
+ public DamageExpression(Query extends Target> 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 extends HasPosition> centerQuery;
+ private final int radius;
+
+ public TilesInRadiusQuery(Query extends HasPosition> centerQuery, int radius) {
+ this.centerQuery = centerQuery;
+ this.radius = radius;
+ }
+
+ @Override
+ public List find(World world, CardPlayer source) {
+ List extends HasPosition> 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());
+ }
+}