diff --git a/src/ch/epfl/chacun/ActionEncoder.java b/src/ch/epfl/chacun/ActionEncoder.java
index 633663b..1fd94dc 100644
--- a/src/ch/epfl/chacun/ActionEncoder.java
+++ b/src/ch/epfl/chacun/ActionEncoder.java
@@ -1,6 +1,5 @@
package ch.epfl.chacun;
-import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
@@ -17,6 +16,30 @@ public class ActionEncoder {
*/
public static final String NO_OCCUPANT_ENCODED_ACTION = Base32.encodeBits5(0x1F);
+ /**
+ * The number of bits to shift to encode the placed tile index.
+ * Encoded format: ppppp-ppprr, where p is a bit of the placed tile index and r is a bit of the rotation.
+ */
+ private static final int PLACED_TILE_INDEX_SHIFT = 2;
+
+ /**
+ * The mask to apply to the encoded action to get the placed tile index.
+ * Encoded format: ppppp-ppprr, where p is a bit of the placed tile index and r is a bit of the rotation.
+ */
+ private static final int PLACED_TILE_ROTATION_MASK = (1 << PLACED_TILE_INDEX_SHIFT) - 1;
+
+ /**
+ * The number of bits to shift to encode the occupant kind.
+ * Encoded format: kzzzz, where k is a bit of the occupant kind and z is a bit of the zone id.
+ */
+ private static final int OCCUPANT_KIND_SHIFT = 4;
+
+ /**
+ * The mask to apply to the encoded action to get the occupant zone id.
+ * Encoded format: kzzzz, where k is a bit of the occupant kind and z is a bit of the zone id.
+ */
+ private static final int OCCUPANT_ZONE_MASK = (1 << OCCUPANT_KIND_SHIFT) - 1;
+
/**
* Manages the encoding of the action of placing a tile.
*
@@ -31,10 +54,10 @@ public class ActionEncoder {
public static StateAction withPLacedTile(GameState gameState, PlacedTile placedTile) {
List sortedFringe = sortPos(gameState);
// Encode the placed tile index then shift it of two positions to the left to merge the encoded rotation
- int fringeBits = sortedFringe.indexOf(placedTile.pos()) << 2;
+ int fringeBits = sortedFringe.indexOf(placedTile.pos()) << PLACED_TILE_INDEX_SHIFT;
int rotationBits = placedTile.rotation().ordinal();
return new StateAction(
- gameState.withPlacedTile(placedTile), Base32.encodeBits10(fringeBits + rotationBits));
+ gameState.withPlacedTile(placedTile), Base32.encodeBits10(fringeBits | rotationBits));
}
/**
@@ -63,10 +86,10 @@ public static StateAction withNewOccupant(GameState gameState, Occupant occupant
assert gameState.board().lastPlacedTile() != null;
if (occupantToPlace != null) {
// Format the action data
- int kindBits = occupantToPlace.kind().ordinal() << 4;
+ int kindBits = occupantToPlace.kind().ordinal() << OCCUPANT_KIND_SHIFT;
int zoneBits = gameState.board().lastPlacedTile().idOfZoneOccupiedBy(occupantToPlace.kind());
return new StateAction(
- gameState.withNewOccupant(occupantToPlace), Base32.encodeBits5(kindBits + zoneBits));
+ gameState.withNewOccupant(occupantToPlace), Base32.encodeBits5(kindBits | zoneBits));
}
return new StateAction(gameState.withNewOccupant(null), NO_OCCUPANT_ENCODED_ACTION);
}
@@ -94,33 +117,75 @@ public static StateAction withOccupantRemoved(GameState gameState, Occupant occu
}
public static StateAction decodeAndApply(GameState gameState, String action) {
- return null;
+ try {
+ return unsafeDecodeAndApply(gameState, action);
+ } catch (IllegalActionException e) {
+ return null;
+ }
}
- private static StateAction decodeAndApplySlave(GameState gameState, String action) {
+ /**
+ * Unsafely decodes the given action and applies it to the given game state based on the next action.
+ *
+ * @param gameState the game state
+ * @param action the encoded action
+ * @return a new state action with an updated game state
+ * @throws IllegalActionException if the action is illegal
+ */
+ private static StateAction unsafeDecodeAndApply(GameState gameState, String action) throws IllegalActionException {
Preconditions.checkArgument(Base32.isValid(action) || action.isEmpty() || action.length() > 2);
int decodedAction = Base32.decode(action);
- switch (gameState.nextAction()) {
+ // Execute the provided action based on the next action context
+ return switch (gameState.nextAction()) {
case PLACE_TILE -> {
- Rotation rotationToApply = Arrays.stream(Rotation.values()).toList().get(decodedAction & (1 << 2 - 1));
- Pos placedTilePosition = sortPos(gameState).get(decodedAction);
+ Rotation placedTileRotation = Rotation.ALL.get(decodedAction & PLACED_TILE_ROTATION_MASK);
+ Pos placedTilePos = sortPos(gameState).get(decodedAction >> PLACED_TILE_INDEX_SHIFT);
PlacedTile placedTile = new PlacedTile(gameState.tileToPlace(), gameState.currentPlayer(),
- rotationToApply, placedTilePosition);
- Preconditions.checkArgument(gameState.board().couldPlaceTile(placedTile.tile()));
- GameState updatedGameState = gameState.withPlacedTile(placedTile);
- return new StateAction(updatedGameState, action);
+ placedTileRotation, placedTilePos);
+
+ // Check if the tile can be placed
+ if (!gameState.board().canAddTile(placedTile))
+ throw new IllegalStateException();
+
+ yield new StateAction(gameState.withPlacedTile(placedTile), action);
}
case OCCUPY_TILE -> {
- Occupant.Kind occupantKind = decodedAction >> 4 == 0 ? Occupant.Kind.PAWN : Occupant.Kind.HUT;
- int zoneID = decodedAction & (1 << 4 - 1);
- Occupant newOccupant = action.equals(NO_OCCUPANT_ENCODED_ACTION)
- ? new Occupant(occupantKind, zoneID)
- : null;
- GameState updatedGameState = gameState.withNewOccupant(newOccupant);
- return new StateAction(updatedGameState, action);
+ // Check if the player doesn't want to add an occupant
+ if (action.equals(NO_OCCUPANT_ENCODED_ACTION))
+ yield new StateAction(gameState.withNewOccupant(null), action);
+
+ // Decode the action
+ int occupantZoneId = decodedAction & OCCUPANT_ZONE_MASK;
+ int occupantKindIndex = decodedAction >> OCCUPANT_KIND_SHIFT;
+ Occupant.Kind occupantKind = Occupant.Kind.values()[occupantKindIndex];
+ Occupant newOccupant = new Occupant(occupantKind, occupantZoneId);
+
+ // Check if the occupant can be placed
+ PlacedTile occupantPlacedTile = gameState.board().lastPlacedTile();
+ if (occupantPlacedTile == null || !occupantPlacedTile.potentialOccupants().contains(newOccupant))
+ throw new IllegalActionException();
+
+ yield new StateAction(gameState.withNewOccupant(newOccupant), action);
}
- }
- return null;
+ case RETAKE_PAWN -> {
+ // Check if the player doesn't want to retake an occupant
+ if (action.equals(NO_OCCUPANT_ENCODED_ACTION))
+ yield new StateAction(gameState.withOccupantRemoved(null), action);
+
+ // Decode the action
+ List sortedOccupants = sortOccupants(gameState);
+ Occupant pawnToRemove = sortedOccupants.get(decodedAction);
+
+ // Check if the occupant can be removed
+ PlacedTile placedTileWithPawn = gameState.board().tileWithId(Zone.tileId(pawnToRemove.zoneId()));
+ if (placedTileWithPawn.placer() != gameState.currentPlayer()
+ || placedTileWithPawn.occupant().equals(pawnToRemove))
+ throw new IllegalActionException();
+
+ yield new StateAction(gameState.withOccupantRemoved(pawnToRemove), action);
+ }
+ default -> throw new IllegalActionException();
+ };
}
/**
@@ -131,9 +196,35 @@ private static StateAction decodeAndApplySlave(GameState gameState, String actio
*
* @param gameState the game state
* @param action the encoded action
+ * @author Maxence Espagnet (sciper: 372808)
+ * @author Balthazar Baillat (sciper: 373420)
*/
public record StateAction(GameState gameState, String action) {
}
+ /**
+ * Represents an illegal action exception.
+ *
+ * @author Maxence Espagnet (sciper: 372808)
+ * @author Balthazar Baillat (sciper: 373420)
+ */
+ public static class IllegalActionException extends Exception {
+
+ /**
+ * Constructs an illegal action exception.
+ */
+ public IllegalActionException() {
+ super();
+ }
+
+ /**
+ * Constructs an illegal action exception with a message.
+ * @param message the message
+ */
+ public IllegalActionException(String message) {
+ super(message);
+ }
+ }
+
}