diff --git a/src/main/java/com/jcloisterzone/action/MoveBlackDragonAction.java b/src/main/java/com/jcloisterzone/action/MoveBlackDragonAction.java new file mode 100644 index 00000000..b0c16dfc --- /dev/null +++ b/src/main/java/com/jcloisterzone/action/MoveBlackDragonAction.java @@ -0,0 +1,19 @@ +package com.jcloisterzone.action; + +import com.jcloisterzone.board.Position; +import io.vavr.collection.Set; + +//TODO generic NeutralMeepleAction ? +public class MoveBlackDragonAction extends SelectTileAction { + + private final String figureId; + + public MoveBlackDragonAction(String figureId, Set options) { + super(options); + this.figureId = figureId; + } + + public String getFigureId() { + return figureId; + } +} \ No newline at end of file diff --git a/src/main/java/com/jcloisterzone/engine/Engine.java b/src/main/java/com/jcloisterzone/engine/Engine.java index b706da3a..73eaae7f 100644 --- a/src/main/java/com/jcloisterzone/engine/Engine.java +++ b/src/main/java/com/jcloisterzone/engine/Engine.java @@ -148,6 +148,7 @@ private GameSetup createSetupFromMessage(GameSetupMessage setupMsg) { capabilities = addCapabilities(capabilities, setupMsg,"watchtower", WatchtowerCapability.class); capabilities = addCapabilities(capabilities, setupMsg,"robbers-son", RobbersSonCapability.class); + capabilities = addCapabilities(capabilities, setupMsg,"black-dragon", BlackDragonCapability.class); Map rules = HashMap.empty(); if (setupMsg.getElements().containsKey("farmers")) { diff --git a/src/main/java/com/jcloisterzone/engine/StateGsonBuilder.java b/src/main/java/com/jcloisterzone/engine/StateGsonBuilder.java index 738abb87..368bc47b 100644 --- a/src/main/java/com/jcloisterzone/engine/StateGsonBuilder.java +++ b/src/main/java/com/jcloisterzone/engine/StateGsonBuilder.java @@ -11,18 +11,21 @@ import com.jcloisterzone.feature.Tower; import com.jcloisterzone.figure.Follower; import com.jcloisterzone.figure.Meeple; +import com.jcloisterzone.figure.neutral.BlackDragon; import com.jcloisterzone.figure.neutral.Dragon; import com.jcloisterzone.game.capability.*; import com.jcloisterzone.game.capability.FerriesCapability.FerryToken; import com.jcloisterzone.game.capability.GoldminesCapability.GoldToken; import com.jcloisterzone.game.capability.LittleBuildingsCapability.LittleBuilding; import com.jcloisterzone.game.capability.SheepToken; +import com.jcloisterzone.game.phase.BlackDragonMovePhase; import com.jcloisterzone.game.phase.DragonMovePhase; import com.jcloisterzone.game.phase.Phase; import com.jcloisterzone.game.phase.RussianPromosTrapPhase; import com.jcloisterzone.game.state.*; import com.jcloisterzone.io.MessageParser; import io.vavr.Tuple2; +import io.vavr.Tuple3; import io.vavr.collection.*; import java.lang.reflect.Type; @@ -70,6 +73,7 @@ public Gson create() { builder.registerTypeAdapter(RemoveMageOrWitchAction.class, new ActionSerializer("RemoveMageOrWitch")); builder.registerTypeAdapter(LittleBuildingAction.class, new LittleBuildingActionSerializer()); builder.registerTypeAdapter(ScoreAcrobatsAction.class, new SelectFeatureActionSerializer()); + builder.registerTypeAdapter(MoveBlackDragonAction.class, new MoveBlackDragonActionSerializer()); return builder.create(); } @@ -288,6 +292,19 @@ public JsonElement serializeNeutralFigures(GameState root, JsonSerializationCont data.add("placement", context.serialize(pos)); neutral.add("bigtop", data); } + pos = state.getBlackDragonDeployment(); + if (pos != null) { + Tuple3,Integer,Array> blackdragonmoves = root.getCapabilityModel(BlackDragonCapability.class); + JsonObject data = new JsonObject(); + data.add("position", context.serialize(pos)); + if (root.getPhase() instanceof BlackDragonMovePhase) { + JsonArray visitedData = new JsonArray(); + blackdragonmoves._1.forEach(p -> visitedData.add(context.serialize(p))); + data.add("visited", visitedData); + data.addProperty("remaining", blackdragonmoves._2 - blackdragonmoves._1.length()); + } + neutral.add("blackdragon", data); + } return neutral; } @@ -374,6 +391,7 @@ public JsonArray serializePlayEvents(GameState root, JsonSerializationContext co JsonObject item = null; JsonArray turnEvents = null; JsonArray dragonPath = null; + JsonArray blackdragonPath = null; for (PlayEvent ev : root.getEvents()) { if (ev instanceof PlayerTurnEvent) { player = ((PlayerTurnEvent) ev).getPlayer(); @@ -387,6 +405,7 @@ public JsonArray serializePlayEvents(GameState root, JsonSerializationContext co events.add(item); // clean-up dragonPath = null; + blackdragonPath = null; continue; } if (item == null) { @@ -529,6 +548,19 @@ public JsonArray serializePlayEvents(GameState root, JsonSerializationContext co } else { dragonPath.add(context.serialize(nev.getTo())); } + } else if (nev.getNeutralFigure() instanceof BlackDragon) { + if (blackdragonPath == null) { + JsonObject data = new JsonObject(); + blackdragonPath = new JsonArray(); + blackdragonPath.add(context.serialize(nev.getFrom())); + blackdragonPath.add(context.serialize(nev.getTo())); + data.addProperty("type", "blackdragon-moved"); + data.addProperty("figure", nev.getNeutralFigure().getId()); + data.add("path", blackdragonPath); + turnEvents.add(data); + } else { + blackdragonPath.add(context.serialize(nev.getTo())); + } } else { JsonObject data = new JsonObject(); data.addProperty("type", "neutral-moved"); @@ -539,6 +571,21 @@ public JsonArray serializePlayEvents(GameState root, JsonSerializationContext co } continue; } + if (ev instanceof NeutralFigureReturned) { + NeutralFigureReturned nfr = (NeutralFigureReturned) ev; + if (nfr.isForced()) { + JsonObject data = new JsonObject(); + data.addProperty("type", "neutralfigure-returned"); + data.addProperty("neutralfigure", nfr.getNeutralFigure().getClass().getSimpleName().toLowerCase()); + Player nfrp = nfr.getPlayer(); + if (nfrp != null) { + data.addProperty("player", nfr.getPlayer().getIndex()); + } + data.add("from", context.serialize(nfr.getFrom())); + turnEvents.add(data); + } + continue; + } if (ev instanceof RansomPaidEvent) { RansomPaidEvent rev = (RansomPaidEvent) ev; JsonObject data = new JsonObject(); @@ -897,4 +944,20 @@ public JsonElement serialize(LittleBuildingAction action, Type type, JsonSeriali return json; } } + + private class MoveBlackDragonActionSerializer implements JsonSerializer { + @Override + public JsonElement serialize(MoveBlackDragonAction action, Type type, JsonSerializationContext context) { + JsonObject json = new JsonObject(); + json.addProperty("type", "MoveDragon"); + json.addProperty("figureId", action.getFigureId()); + JsonArray options = new JsonArray(); + action.getOptions().forEach(pos -> { + options.add(context.serialize(pos)); + }); + json.add("options", options); + return json; + } + } + } diff --git a/src/main/java/com/jcloisterzone/event/NeutralFigureReturned.java b/src/main/java/com/jcloisterzone/event/NeutralFigureReturned.java index 8197206b..f3091134 100644 --- a/src/main/java/com/jcloisterzone/event/NeutralFigureReturned.java +++ b/src/main/java/com/jcloisterzone/event/NeutralFigureReturned.java @@ -2,16 +2,22 @@ import com.jcloisterzone.board.pointer.BoardPointer; import com.jcloisterzone.figure.neutral.NeutralFigure; +import com.jcloisterzone.Player; public class NeutralFigureReturned extends PlayEvent { private final BoardPointer from; private final NeutralFigure neutralFigure; + private final boolean forced; + /** true if meeple is returned different way than scoring feature */ + private final Player player; - public NeutralFigureReturned(PlayEventMeta metadata, NeutralFigure neutralFigure, BoardPointer from) { + public NeutralFigureReturned(PlayEventMeta metadata, NeutralFigure neutralFigure, BoardPointer from, Boolean forced, Player player) { super(metadata); this.neutralFigure = neutralFigure; this.from = from; + this.forced = forced; + this.player = player; } public BoardPointer getFrom() { @@ -21,4 +27,12 @@ public BoardPointer getFrom() { public NeutralFigure getNeutralFigure() { return neutralFigure; } + + public boolean isForced() { + return forced; + } + + public Player getPlayer() { + return player; + } } diff --git a/src/main/java/com/jcloisterzone/figure/Barn.java b/src/main/java/com/jcloisterzone/figure/Barn.java index 4ec68ed2..c04bd89c 100644 --- a/src/main/java/com/jcloisterzone/figure/Barn.java +++ b/src/main/java/com/jcloisterzone/figure/Barn.java @@ -19,6 +19,11 @@ public boolean canBeEatenByDragon(GameState state) { return false; } + @Override + public boolean canBeEatenByBlackDragon(GameState state) { + return false; + } + @Override public DeploymentCheckResult isDeploymentAllowed(GameState state, FeaturePointer fp, Structure feature) { if (!(feature instanceof Field)) { diff --git a/src/main/java/com/jcloisterzone/figure/Figure.java b/src/main/java/com/jcloisterzone/figure/Figure.java index b4dd1ef3..6e9d9c28 100644 --- a/src/main/java/com/jcloisterzone/figure/Figure.java +++ b/src/main/java/com/jcloisterzone/figure/Figure.java @@ -74,6 +74,10 @@ public boolean isInSupply(GameState state) { return !isDeployed(state); } + public boolean canBeEatenByBlackDragon(GameState state) { + return true; + } + @Override public int hashCode() { return id.hashCode(); diff --git a/src/main/java/com/jcloisterzone/figure/Follower.java b/src/main/java/com/jcloisterzone/figure/Follower.java index 4cf78151..92a85afb 100644 --- a/src/main/java/com/jcloisterzone/figure/Follower.java +++ b/src/main/java/com/jcloisterzone/figure/Follower.java @@ -28,6 +28,10 @@ public boolean canBeEatenByDragon(GameState state) { return !(getFeature(state) instanceof Castle); } + @Override + public boolean canBeEatenByBlackDragon(GameState state) { + return !(getFeature(state) instanceof Castle); + } public boolean isCaptured(GameState state) { Array> model = state.getCapabilityModel(TowerCapability.class); return model != null && Stream.concat(model).find(f -> f == this).isDefined(); diff --git a/src/main/java/com/jcloisterzone/figure/neutral/BigTop.java b/src/main/java/com/jcloisterzone/figure/neutral/BigTop.java index 58342de9..2e43a467 100644 --- a/src/main/java/com/jcloisterzone/figure/neutral/BigTop.java +++ b/src/main/java/com/jcloisterzone/figure/neutral/BigTop.java @@ -2,6 +2,7 @@ import com.jcloisterzone.Immutable; import com.jcloisterzone.board.Position; +import com.jcloisterzone.game.state.GameState; @Immutable public class BigTop extends NeutralFigure { @@ -11,4 +12,9 @@ public class BigTop extends NeutralFigure { public BigTop(String id) { super(id); } + + @Override + public boolean canBeEatenByBlackDragon(GameState state) { + return false; + } } diff --git a/src/main/java/com/jcloisterzone/figure/neutral/BlackDragon.java b/src/main/java/com/jcloisterzone/figure/neutral/BlackDragon.java new file mode 100644 index 00000000..996d979f --- /dev/null +++ b/src/main/java/com/jcloisterzone/figure/neutral/BlackDragon.java @@ -0,0 +1,15 @@ +package com.jcloisterzone.figure.neutral; + +import com.jcloisterzone.Immutable; +import com.jcloisterzone.board.Position; + +@Immutable +public class BlackDragon extends NeutralFigure { + + private static final long serialVersionUID = 1L; + + public BlackDragon(String id) { + super(id); + } + +} diff --git a/src/main/java/com/jcloisterzone/figure/neutral/Count.java b/src/main/java/com/jcloisterzone/figure/neutral/Count.java index 9dfc2659..413164cb 100644 --- a/src/main/java/com/jcloisterzone/figure/neutral/Count.java +++ b/src/main/java/com/jcloisterzone/figure/neutral/Count.java @@ -2,6 +2,7 @@ import com.jcloisterzone.Immutable; import com.jcloisterzone.board.pointer.FeaturePointer; +import com.jcloisterzone.game.state.GameState; @Immutable public class Count extends NeutralFigure { @@ -11,4 +12,9 @@ public class Count extends NeutralFigure { public Count(String id) { super(id); } + + @Override + public boolean canBeEatenByBlackDragon(GameState state) { + return false; + } } diff --git a/src/main/java/com/jcloisterzone/figure/neutral/NeutralFigure.java b/src/main/java/com/jcloisterzone/figure/neutral/NeutralFigure.java index 91ebc042..0cb13bd5 100644 --- a/src/main/java/com/jcloisterzone/figure/neutral/NeutralFigure.java +++ b/src/main/java/com/jcloisterzone/figure/neutral/NeutralFigure.java @@ -28,16 +28,4 @@ public boolean at(GameState state, Structure feature) { FeaturePointer fp = ptr.asFeaturePointer(); return feature.getPlaces().contains(fp); } - - -// @Override -// public void deploy(T at) { -// T origin = getDeployment(); -// game.replaceState(state -> { -// LinkedHashMap, BoardPointer> deployedNeutralFigures = state.getDeployedNeutralFigures(); -// return state.setDeployedNeutralFigures(deployedNeutralFigures.put(this, at)); -// }); -// game.post(new NeutralFigureMoveEvent(game.getActivePlayer(), this, origin, at)); -// } - } diff --git a/src/main/java/com/jcloisterzone/game/GameStatePhaseReducer.java b/src/main/java/com/jcloisterzone/game/GameStatePhaseReducer.java index 4677b242..3546ffbe 100644 --- a/src/main/java/com/jcloisterzone/game/GameStatePhaseReducer.java +++ b/src/main/java/com/jcloisterzone/game/GameStatePhaseReducer.java @@ -46,6 +46,8 @@ public GameStatePhaseReducer(GameSetup setup, double initialRandom) { next = cleanUpTurnPartPhase = new CleanUpTurnPartPhase(randomGenerator, next); if (setup.contains(CornCircleCapability.class)) next = new CornCirclePhase(randomGenerator, next); + if (setup.contains(BlackDragonCapability.class)) next = new BlackDragonPlacePhase(randomGenerator, next); + if (setup.contains(DragonCapability.class) && "after-scoring".equals(setup.getStringRule(Rule.DRAGON_MOVEMENT))) { next = new DragonPhase(randomGenerator, next); } diff --git a/src/main/java/com/jcloisterzone/game/capability/BlackDragonCapability.java b/src/main/java/com/jcloisterzone/game/capability/BlackDragonCapability.java new file mode 100644 index 00000000..5432d829 --- /dev/null +++ b/src/main/java/com/jcloisterzone/game/capability/BlackDragonCapability.java @@ -0,0 +1,100 @@ +package com.jcloisterzone.game.capability; + +import com.jcloisterzone.Immutable; +import com.jcloisterzone.board.Position; +import com.jcloisterzone.board.pointer.BoardPointer; +import com.jcloisterzone.board.pointer.FeaturePointer; +import com.jcloisterzone.feature.Completable; +import com.jcloisterzone.figure.Meeple; +import com.jcloisterzone.figure.neutral.BlackDragon; +import com.jcloisterzone.figure.neutral.NeutralFigure; +import com.jcloisterzone.game.Capability; +import com.jcloisterzone.game.state.GameState; +import com.jcloisterzone.reducers.MoveNeutralFigure; +import com.jcloisterzone.reducers.UndeployMeeple; +import com.jcloisterzone.reducers.UndeployNeutralFigure; + +import io.vavr.collection.Array; +import io.vavr.collection.Vector; +import io.vavr.Tuple2; +import io.vavr.Tuple3; + +/** + * @model Tuple3,Integer,Array : visited tiles by black dragon, count of finished features, score on start of turn + */ +@Immutable +public class BlackDragonCapability extends Capability,Integer,Array>> { + + private static final long serialVersionUID = 1L; + + public static final Vector EMPTY_VISITED = Vector.empty(); + + @Override + public GameState onStartGame(GameState state) { + state = state.mapNeutralFigures(nf -> nf.setBlackDragon(new BlackDragon("blackdragon.1"))); + state = setInitialTurnState(state); + return state; + } + + @Override + public boolean isMeepleDeploymentAllowed(GameState state, Position pos) { + return !pos.equals(state.getNeutralFigures().getBlackDragonDeployment()); + } + + @Override + public GameState beforeCompletableScore(GameState state, java.util.Set features) { + Tuple3,Integer,Array> model = getModel(state); + state = setModel(state, new Tuple3<>(EMPTY_VISITED,features.size(),model._3)); + return state; + } + + @Override + public GameState onTurnPartCleanUp(GameState state) { + return setInitialTurnState(state); + } + + public GameState setInitialTurnState(GameState state) { + return setModel(state, new Tuple3<>(EMPTY_VISITED,0,state.getPlayers().getScore())); + } + + public Vector getVisitedPositions(GameState state) { + Vector visitedpositions = getModel(state)._1; + return visitedpositions == null ? BlackDragonCapability.EMPTY_VISITED : visitedpositions; + } + + public Integer getMoves(GameState state) { + Integer finishedfeatures = getModel(state)._2; + return finishedfeatures == null ? 0 : finishedfeatures; + } + + public Array getScore(GameState state) { + return getModel(state)._3; + } + + public GameState moveBlackDragon(GameState state, Position pos) { + state = ( + new MoveNeutralFigure<>(state.getNeutralFigures().getBlackDragon(), pos) + ).apply(state); + + state = blackDragonOnTile(state, pos); + return state; + } + + public GameState blackDragonOnTile(GameState state, Position pos) { + for (Tuple2 t: state.getDeployedMeeples()) { + Meeple m = t._1; + FeaturePointer fp = t._2; + if (pos.equals(fp.getPosition()) && m.canBeEatenByBlackDragon(state)) { + state = (new UndeployMeeple(m, true)).apply(state); + } + } + for (Tuple2, BoardPointer> t: state.getNeutralFigures().getDeployedNeutralFigures().toSet()) { + NeutralFigure nf = t._1; + BoardPointer bp = t._2; + if (pos.equals(bp.getPosition()) && state.getNeutralFigures().getBlackDragon() != nf && nf.canBeEatenByBlackDragon(state)) { + state = (new UndeployNeutralFigure(nf, true)).apply(state); + } + } + return state; + } +} diff --git a/src/main/java/com/jcloisterzone/game/phase/BlackDragonMovePhase.java b/src/main/java/com/jcloisterzone/game/phase/BlackDragonMovePhase.java new file mode 100644 index 00000000..e9a1992d --- /dev/null +++ b/src/main/java/com/jcloisterzone/game/phase/BlackDragonMovePhase.java @@ -0,0 +1,106 @@ +package com.jcloisterzone.game.phase; + +import com.jcloisterzone.Player; +import com.jcloisterzone.action.MoveBlackDragonAction; +import com.jcloisterzone.board.Position; +import com.jcloisterzone.board.pointer.BoardPointer; +import com.jcloisterzone.figure.neutral.BlackDragon; +import com.jcloisterzone.figure.neutral.NeutralFigure; +import com.jcloisterzone.game.capability.CountCapability; +import com.jcloisterzone.game.capability.BlackDragonCapability; +import com.jcloisterzone.game.state.ActionsState; +import com.jcloisterzone.game.state.Flag; +import com.jcloisterzone.game.state.GameState; +import com.jcloisterzone.game.state.PlacedTile; +import com.jcloisterzone.io.message.MoveNeutralFigureMessage; +import com.jcloisterzone.random.RandomGenerator; +import com.jcloisterzone.reducers.MoveNeutralFigure; +import io.vavr.Tuple3; +import io.vavr.collection.HashSet; +import io.vavr.collection.Set; +import io.vavr.collection.Vector; + +public class BlackDragonMovePhase extends Phase { + + public BlackDragonMovePhase(RandomGenerator random, Phase defaultNext) { + super(random, defaultNext); + } + + @Override + public StepResult enter(GameState state) { + BlackDragonCapability blackdragonCap = state.getCapabilities().get(BlackDragonCapability.class); + Vector visited = blackdragonCap.getVisitedPositions(state); + Integer moves = blackdragonCap.getMoves(state); + if (visited.size() == moves) { + return next(endBlackDragonMove(state)); + } + Set availMoves = getAvailBlackDragonMoves(state, visited); + if (availMoves.isEmpty()) { + return next(endBlackDragonMove(state)); + } + BlackDragon blackdragon = state.getNeutralFigures().getBlackDragon(); + return promote(state.setPlayerActions( + new ActionsState(state.getTurnPlayer(), new MoveBlackDragonAction(blackdragon.getId(), availMoves), false) + )); + } + + private GameState endBlackDragonMove(GameState state) { + state = state.addFlag(Flag.BLACK_DRAGON_MOVED); + state = state.mapCapabilityModel(BlackDragonCapability.class, blackdragon -> { + return new Tuple3<>(BlackDragonCapability.EMPTY_VISITED,0,blackdragon._3); + }); + state = clearActions(state); + return state; + } + + public Set getAvailBlackDragonMoves(GameState state, Vector visited) { + Set result = HashSet.empty(); + Position blackdragonPosition = state.getNeutralFigures().getBlackDragonDeployment(); + + if (blackdragonPosition != null) { + for (Position offset: Position.ADJACENT.values()) { + Position pos = blackdragonPosition.add(offset); + PlacedTile pt = state.getPlacedTile(pos); + + if (pt == null || CountCapability.isTileForbidden(pt.getTile())) continue; + if (visited.contains(pos)) continue; + + result = result.add(pos); + } + } + return result; + } + + @PhaseMessageHandler + public StepResult handleMoveNeutralFigure(GameState state, MoveNeutralFigureMessage msg) { + BoardPointer ptr = msg.getTo(); + NeutralFigure fig = state.getNeutralFigures().getById(msg.getFigureId()); + + if (!(fig instanceof BlackDragon)) { + throw new IllegalArgumentException("Illegal neutral figure move"); + } + + BlackDragonCapability blackdragonCap = state.getCapabilities().get(BlackDragonCapability.class); + + Vector visited = blackdragonCap.getVisitedPositions(state); + Set availMoves = getAvailBlackDragonMoves(state, visited); + + Position pos = ptr.getPosition(); + if (!availMoves.contains(pos)) { + throw new IllegalArgumentException("Invalid black dragon move."); + } + + Position blackdragonPosition = state.getNeutralFigures().getBlackDragonDeployment(); + + state = ( + new MoveNeutralFigure<>((BlackDragon) fig, pos, state.getActivePlayer()) + ).apply(state); + + state = state.mapCapabilityModel(BlackDragonCapability.class, blackdragon -> { + return new Tuple3<>(blackdragon._1.append(blackdragonPosition),blackdragon._2,blackdragon._3); + }); + + state = blackdragonCap.blackDragonOnTile(state, pos); + return enter(state); + } +} diff --git a/src/main/java/com/jcloisterzone/game/phase/BlackDragonPlacePhase.java b/src/main/java/com/jcloisterzone/game/phase/BlackDragonPlacePhase.java new file mode 100644 index 00000000..3fc324db --- /dev/null +++ b/src/main/java/com/jcloisterzone/game/phase/BlackDragonPlacePhase.java @@ -0,0 +1,32 @@ +package com.jcloisterzone.game.phase; + +import com.jcloisterzone.Player; +import com.jcloisterzone.game.capability.BlackDragonCapability; +import com.jcloisterzone.game.state.GameState; +import com.jcloisterzone.random.RandomGenerator; +import io.vavr.collection.Array; + +public class BlackDragonPlacePhase extends Phase { + + public BlackDragonPlacePhase(RandomGenerator random, Phase defaultNext) { + super(random, defaultNext); + } + + @Override + public StepResult enter(GameState state) { + Array scoreOnStart = state.getCapabilityModel(BlackDragonCapability.class)._3; + BlackDragonCapability blackdragonCap = state.getCapabilities().get(BlackDragonCapability.class); + + for(Player player : state.getPlayers().getPlayers() ) { + Integer scoreBefore = scoreOnStart.get(player.getIndex()); + Integer scoreCurrent = state.getPlayers().getScore().get(player.getIndex()); + Integer diff = scoreCurrent - scoreBefore; + if (diff>0 && ((scoreBefore % 50) + diff > 50)) { + state = blackdragonCap.moveBlackDragon(state, state.getLastPlaced().getPosition()); + break; + } + } + + return next(state); + } +} diff --git a/src/main/java/com/jcloisterzone/game/phase/CleanUpTurnPartPhase.java b/src/main/java/com/jcloisterzone/game/phase/CleanUpTurnPartPhase.java index 70e10638..673aaa59 100644 --- a/src/main/java/com/jcloisterzone/game/phase/CleanUpTurnPartPhase.java +++ b/src/main/java/com/jcloisterzone/game/phase/CleanUpTurnPartPhase.java @@ -38,6 +38,7 @@ public StepResult enter(GameState state) { .remove(Flag.PORTAL_USED) .remove(Flag.NO_PHANTOM) .remove(Flag.FLYING_MACHINE_USED) + .remove(Flag.BLACK_DRAGON_MOVED) ); } diff --git a/src/main/java/com/jcloisterzone/game/phase/DragonMovePhase.java b/src/main/java/com/jcloisterzone/game/phase/DragonMovePhase.java index 7c8ce300..7e498bb1 100644 --- a/src/main/java/com/jcloisterzone/game/phase/DragonMovePhase.java +++ b/src/main/java/com/jcloisterzone/game/phase/DragonMovePhase.java @@ -67,6 +67,7 @@ public Set getAvailDragonMoves(GameState state, Vector visit BoardPointer fairyPtr = state.getNeutralFigures().getFairyDeployment(); Position fairyPosition = fairyPtr == null ? null : fairyPtr.getPosition(); Position dragonPosition = state.getNeutralFigures().getDragonDeployment(); + Position blackdragonPosition = state.getNeutralFigures().getBlackDragonDeployment(); for (Position offset: Position.ADJACENT.values()) { Position pos = dragonPosition.add(offset); @@ -75,6 +76,7 @@ public Set getAvailDragonMoves(GameState state, Vector visit if (pt == null || CountCapability.isTileForbidden(pt.getTile())) continue; if (visited.contains(pos)) continue; if (pos.equals(fairyPosition)) continue; + if (pos.equals(blackdragonPosition)) continue; result = result.add(pos); } diff --git a/src/main/java/com/jcloisterzone/game/phase/ScoringPhase.java b/src/main/java/com/jcloisterzone/game/phase/ScoringPhase.java index 6f40969d..c9ae3df7 100644 --- a/src/main/java/com/jcloisterzone/game/phase/ScoringPhase.java +++ b/src/main/java/com/jcloisterzone/game/phase/ScoringPhase.java @@ -13,24 +13,29 @@ import com.jcloisterzone.game.ScoreFeatureReducer; import com.jcloisterzone.game.capability.*; import com.jcloisterzone.game.capability.TunnelCapability.Tunnel; +import com.jcloisterzone.game.state.Flag; import com.jcloisterzone.game.state.GameState; import com.jcloisterzone.game.state.PlacedTile; import com.jcloisterzone.random.RandomGenerator; +import com.jcloisterzone.Player; import com.jcloisterzone.reducers.ScoreCompletable; import com.jcloisterzone.reducers.ScoreField; import com.jcloisterzone.reducers.ScoreFieldWhenBarnIsConnected; import com.jcloisterzone.reducers.UndeployMeeples; import io.vavr.Predicates; import io.vavr.Tuple2; +import io.vavr.Tuple3; import io.vavr.collection.*; - public class ScoringPhase extends Phase { private java.util.Map completedMutable = new java.util.HashMap<>(); + private BlackDragonMovePhase blackdragonMovePhase; + public ScoringPhase(RandomGenerator random, Phase defaultNext) { super(random, defaultNext); + blackdragonMovePhase = new BlackDragonMovePhase(random, this); } private void collectCompletedOnTile(GameState state, PlacedTile tile) { @@ -99,8 +104,6 @@ public StepResult enter(GameState state) { PlacedTile lastPlaced = state.getLastPlaced(); Position pos = lastPlaced.getPosition(); - Map deployedWagonsBefore = getDeployedWagons(state); - collectCompletedOnTile(state, lastPlaced); collectCompletedOnAdjacentEdges(state, pos); // closed by abbey, city gates ... etc @@ -132,6 +135,16 @@ public StepResult enter(GameState state) { } } + BlackDragonCapability blackdragonCap = state.getCapabilities().get(BlackDragonCapability.class); + + Array scoreOnStart = state.getPlayers().getScore(); + + if (blackdragonCap != null && completedMutable.keySet().size()>0 && !state.hasFlag(Flag.BLACK_DRAGON_MOVED)) { + state = blackdragonCap.setModel(state, new Tuple3<>(blackdragonCap.EMPTY_VISITED,completedMutable.keySet().size(),blackdragonCap.getScore(state))); + return next(state, blackdragonMovePhase); + } + + Map deployedWagonsBefore = getDeployedWagons(state); for (Capability cap : state.getCapabilities().toSeq()) { state = cap.beforeCompletableScore(state, completedMutable.keySet()); @@ -182,7 +195,7 @@ public StepResult enter(GameState state) { for (Capability cap : state.getCapabilities().toSeq()) { state = cap.onTurnScoring(state, scored); } - + if (!deployedWagonsBefore.isEmpty()) { Set deployedWagonsAfter = getDeployedWagons(state).keySet(); Set returnedVagons = deployedWagonsBefore.keySet().diff(deployedWagonsAfter); diff --git a/src/main/java/com/jcloisterzone/game/state/Flag.java b/src/main/java/com/jcloisterzone/game/state/Flag.java index 5f7336c7..b73bf953 100644 --- a/src/main/java/com/jcloisterzone/game/state/Flag.java +++ b/src/main/java/com/jcloisterzone/game/state/Flag.java @@ -5,5 +5,5 @@ public enum Flag { RANSOM_PAID, BAZAAR_AUCTION, TUNNEL_PLACED, // Cleared at the turn part end - PORTAL_USED, NO_PHANTOM, FLYING_MACHINE_USED + PORTAL_USED, NO_PHANTOM, FLYING_MACHINE_USED, BLACK_DRAGON_MOVED } \ No newline at end of file diff --git a/src/main/java/com/jcloisterzone/game/state/NeutralFiguresState.java b/src/main/java/com/jcloisterzone/game/state/NeutralFiguresState.java index 0538d157..5541c6f3 100644 --- a/src/main/java/com/jcloisterzone/game/state/NeutralFiguresState.java +++ b/src/main/java/com/jcloisterzone/game/state/NeutralFiguresState.java @@ -20,15 +20,16 @@ public class NeutralFiguresState implements Serializable { private final Witch witch; private final Count count; private final BigTop bigtop; + private final BlackDragon blackdragon; private final LinkedHashMap, BoardPointer> deployedNeutralFigures; public NeutralFiguresState() { - this(null, null, null, null, null, null, LinkedHashMap.empty()); + this(null, null, null, null, null, null, null,LinkedHashMap.empty()); } public NeutralFiguresState( - Dragon dragon, Fairy fairy, Mage mage, Witch witch, Count count, BigTop bigtop, + Dragon dragon, Fairy fairy, Mage mage, Witch witch, Count count, BigTop bigtop, BlackDragon blackdragon, LinkedHashMap, BoardPointer> deployedNeutralFigures ) { this.dragon = dragon; @@ -37,35 +38,40 @@ public NeutralFiguresState( this.witch = witch; this.count = count; this.bigtop = bigtop; + this.blackdragon = blackdragon; this.deployedNeutralFigures = deployedNeutralFigures; } public NeutralFiguresState setDragon(Dragon dragon) { - return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, deployedNeutralFigures); + return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, blackdragon, deployedNeutralFigures); } public NeutralFiguresState setFairy(Fairy fairy) { - return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, deployedNeutralFigures); + return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, blackdragon, deployedNeutralFigures); } public NeutralFiguresState setMage(Mage mage) { - return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, deployedNeutralFigures); + return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, blackdragon, deployedNeutralFigures); } public NeutralFiguresState setWitch(Witch witch) { - return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, deployedNeutralFigures); + return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, blackdragon, deployedNeutralFigures); } public NeutralFiguresState setCount(Count count) { - return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, deployedNeutralFigures); + return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, blackdragon, deployedNeutralFigures); } public NeutralFiguresState setBigTop(BigTop bigtop) { - return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, deployedNeutralFigures); + return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, blackdragon, deployedNeutralFigures); + } + + public NeutralFiguresState setBlackDragon(BlackDragon blackdragon) { + return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, blackdragon, deployedNeutralFigures); } public NeutralFiguresState setDeployedNeutralFigures(LinkedHashMap, BoardPointer> deployedNeutralFigures) { - return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, deployedNeutralFigures); + return new NeutralFiguresState(dragon, fairy, mage, witch, count, bigtop, blackdragon, deployedNeutralFigures); } public NeutralFigure getById(String figureId) { @@ -75,6 +81,7 @@ public NeutralFigure getById(String figureId) { if (witch != null && figureId.equals(witch.getId())) return witch; if (count != null && figureId.equals(count.getId())) return count; if (bigtop != null && figureId.equals(bigtop.getId())) return bigtop; + if (blackdragon != null && figureId.equals(blackdragon.getId())) return blackdragon; return null; } @@ -134,4 +141,16 @@ public LinkedHashMap, BoardPointer> getDeployedNeutralFigures() return deployedNeutralFigures; } + public BlackDragon getBlackDragon() { + return blackdragon; + } + + public Position getBlackDragonDeployment() { + var bp = deployedNeutralFigures.get(blackdragon).getOrNull(); + if (bp != null) { + return bp.getPosition(); + } + return null; + } + } diff --git a/src/main/java/com/jcloisterzone/reducers/AbstractUndeploy.java b/src/main/java/com/jcloisterzone/reducers/AbstractUndeploy.java index aa6f1a84..1dbed1bc 100644 --- a/src/main/java/com/jcloisterzone/reducers/AbstractUndeploy.java +++ b/src/main/java/com/jcloisterzone/reducers/AbstractUndeploy.java @@ -1,15 +1,20 @@ package com.jcloisterzone.reducers; import com.jcloisterzone.Player; +import com.jcloisterzone.board.pointer.BoardPointer; import com.jcloisterzone.board.pointer.FeaturePointer; import com.jcloisterzone.event.MeepleReturned; +import com.jcloisterzone.event.NeutralFigureReturned; import com.jcloisterzone.event.PlayEvent; import com.jcloisterzone.feature.Structure; import com.jcloisterzone.figure.Builder; import com.jcloisterzone.figure.Follower; import com.jcloisterzone.figure.Meeple; import com.jcloisterzone.figure.Pig; +import com.jcloisterzone.figure.neutral.NeutralFigure; import com.jcloisterzone.game.state.GameState; +import com.jcloisterzone.game.state.NeutralFiguresState; + import io.vavr.Tuple2; import io.vavr.collection.LinkedHashMap; import io.vavr.collection.Stream; @@ -20,7 +25,7 @@ protected GameState undeploy(GameState state, PlayEvent.PlayEventMeta meta, Meep LinkedHashMap deployedMeeples = state.getDeployedMeeples(); state = state.setDeployedMeeples(deployedMeeples.remove(meeple)); state = state.appendEvent( - new MeepleReturned(meta, meeple, source, forced) + new MeepleReturned(meta, meeple, source, forced) ); return state; } @@ -41,4 +46,16 @@ protected GameState undeployLonelySpecials(GameState state, Follower meeple, Fea return state; } + + protected GameState undeploy(GameState state, PlayEvent.PlayEventMeta meta, NeutralFigure figure, BoardPointer source, boolean forced, Player player) { + + NeutralFiguresState nfState = state.getNeutralFigures(); + nfState = nfState.setDeployedNeutralFigures(nfState.getDeployedNeutralFigures().remove(figure)); + state = state.setNeutralFigures(nfState); + + state = state.appendEvent( + new NeutralFigureReturned(meta, figure, source, forced, player) + ); + return state; + } } diff --git a/src/main/java/com/jcloisterzone/reducers/ReturnNeutralFigure.java b/src/main/java/com/jcloisterzone/reducers/ReturnNeutralFigure.java index 8d68d24b..7e089670 100644 --- a/src/main/java/com/jcloisterzone/reducers/ReturnNeutralFigure.java +++ b/src/main/java/com/jcloisterzone/reducers/ReturnNeutralFigure.java @@ -32,7 +32,7 @@ public GameState apply(GameState state) { ); state = state.setNeutralFigures(nfState); state = state.appendEvent( - new NeutralFigureReturned(PlayEventMeta.createWithPlayer(triggeringPlayer), figure, from) + new NeutralFigureReturned(PlayEventMeta.createWithPlayer(triggeringPlayer), figure, from, true, triggeringPlayer) ); return state; } diff --git a/src/main/java/com/jcloisterzone/reducers/UndeployNeutralFigure.java b/src/main/java/com/jcloisterzone/reducers/UndeployNeutralFigure.java new file mode 100644 index 00000000..f1db0990 --- /dev/null +++ b/src/main/java/com/jcloisterzone/reducers/UndeployNeutralFigure.java @@ -0,0 +1,34 @@ +package com.jcloisterzone.reducers; + +import com.jcloisterzone.board.pointer.BoardPointer; +import com.jcloisterzone.board.pointer.FeaturePointer; +import com.jcloisterzone.event.PlayEvent.PlayEventMeta; +import com.jcloisterzone.figure.neutral.NeutralFigure; +import com.jcloisterzone.game.state.GameState; +import com.jcloisterzone.game.state.NeutralFiguresState; + +public class UndeployNeutralFigure extends AbstractUndeploy { + + private final NeutralFigure figure; + /** true if meep le is returned different way than scoring feature */ + private final boolean forced; + + public UndeployNeutralFigure(NeutralFigure figure, boolean forced) { + this.figure = figure; + this.forced = forced; + } + + @Override + public GameState apply(GameState state) { + BoardPointer source = figure.getDeployment(state); + + PlayEventMeta metaWithPlayer = PlayEventMeta.createWithActivePlayer(state); + state = undeploy(state, metaWithPlayer, figure, source, forced, state.getActivePlayer()); + + return state; + } + + public boolean isForced() { + return forced; + } +}