diff --git a/src/main/java/sk/uniba/fmph/dcs/game_phase_controller/PlaceFiguresState.java b/src/main/java/sk/uniba/fmph/dcs/game_phase_controller/PlaceFiguresState.java new file mode 100644 index 00000000..4612271d --- /dev/null +++ b/src/main/java/sk/uniba/fmph/dcs/game_phase_controller/PlaceFiguresState.java @@ -0,0 +1,110 @@ +package sk.uniba.fmph.dcs.game_phase_controller; + +import java.util.Collection; +import java.util.Map; +import sk.uniba.fmph.dcs.stone_age.PlayerOrder; +import sk.uniba.fmph.dcs.stone_age.Location; +import sk.uniba.fmph.dcs.stone_age.ActionResult; +import sk.uniba.fmph.dcs.stone_age.HasAction; +import sk.uniba.fmph.dcs.stone_age.Effect; + +/** + * Represents the game phase where players place their figures on locations. + * Only figure placement actions are valid during this phase. + * + * Note: This class does not check if it's the player's turn - that validation + * is handled by GamePhaseController before any actions are delegated here. + * This separation of concerns keeps turn management logic in one place. + */ +public class PlaceFiguresState implements InterfaceGamePhaseState { + + /** + * Maps game locations to their corresponding figure placement handlers + */ + private final Map places; + + /** + * Creates a new PlaceFiguresState with the given location mappings + * + * @param places Map of locations to their figure placement handlers + */ + public PlaceFiguresState(Map places) { + this.places = places; + } + + /** + * Attempts to place figures at the specified location + * + * @param player The player attempting to place figures + * @param location The location where figures should be placed + * @param figuresCount Number of figures to place + * @return ACTION_DONE if placement successful, FAILURE otherwise + */ + @Override + public ActionResult placeFigures(PlayerOrder player, Location location, int figuresCount) { + InterfaceFigureLocation place = places.get(location); + if (place == null) { + return ActionResult.FAILURE; + } + + return place.placeFigures(player, figuresCount) + ? ActionResult.ACTION_DONE + : ActionResult.FAILURE; + } + + /** + * Checks if the player can make any automatic figure placements + * + * @param player The player to check for possible actions + * @return WAITING_FOR_PLAYER_ACTION if player has figures to place, + * NO_ACTION_POSSIBLE if no figures can be placed + */ + @Override + public HasAction tryToMakeAutomaticAction(PlayerOrder player) { + for (InterfaceFigureLocation place : places.values()) { + HasAction result = place.tryToPlaceFigures(player, 1); + if (result == HasAction.WAITING_FOR_PLAYER_ACTION) { + return HasAction.WAITING_FOR_PLAYER_ACTION; + } + } + return HasAction.NO_ACTION_POSSIBLE; + } + + // The following methods return FAILURE as they are not valid actions during the Place Figures phase + + @Override + public ActionResult makeAction(PlayerOrder player, Location location, + Collection inputResources, Collection outputResources) { + return ActionResult.FAILURE; + } + + @Override + public ActionResult skipAction(PlayerOrder player, Location location) { + return ActionResult.FAILURE; + } + + @Override + public ActionResult useTools(PlayerOrder player, int toolIndex) { + return ActionResult.FAILURE; + } + + @Override + public ActionResult noMoreToolsThisThrow(PlayerOrder player) { + return ActionResult.FAILURE; + } + + @Override + public ActionResult feedTribe(PlayerOrder player, Collection resources) { + return ActionResult.FAILURE; + } + + @Override + public ActionResult doNotFeedThisTurn(PlayerOrder player) { + return ActionResult.FAILURE; + } + + @Override + public ActionResult makeAllPlayersTakeARewardChoice(PlayerOrder player, Effect reward) { + return ActionResult.FAILURE; + } +} diff --git a/src/main/java/sk/uniba/fmph/dcs/stone_age/InterfaceFeedTribe.java b/src/main/java/sk/uniba/fmph/dcs/stone_age/InterfaceFeedTribe.java new file mode 100644 index 00000000..ca1288c9 --- /dev/null +++ b/src/main/java/sk/uniba/fmph/dcs/stone_age/InterfaceFeedTribe.java @@ -0,0 +1,10 @@ +package sk.uniba.fmph.dcs.stone_age; + +import java.util.Collection; + +public interface InterfaceFeedTribe { + boolean feedTribeIfEnoughFood(); + boolean feedTribe(Collection resources); + boolean doNotFeedThisTurn(); + boolean isTribeFed(); +} diff --git a/src/main/java/sk/uniba/fmph/dcs/stone_age/InterfaceFigureLocation.java b/src/main/java/sk/uniba/fmph/dcs/stone_age/InterfaceFigureLocation.java new file mode 100644 index 00000000..fa6d0d5c --- /dev/null +++ b/src/main/java/sk/uniba/fmph/dcs/stone_age/InterfaceFigureLocation.java @@ -0,0 +1,12 @@ +package sk.uniba.fmph.dcs.stone_age; + +import java.util.Collection; + +public interface InterfaceFigureLocation { + boolean placeFigures(PlayerOrder player, int figureCount); + HasAction tryToPlaceFigures(PlayerOrder player, int count); + ActionResult makeAction(PlayerOrder player, Collection inputResources, Collection outputResources); + boolean skipAction(PlayerOrder player); + HasAction tryToMakeAction(PlayerOrder player); + boolean newTurn(); +} diff --git a/src/main/java/sk/uniba/fmph/dcs/stone_age/InterfaceFigureLocationInternal.java b/src/main/java/sk/uniba/fmph/dcs/stone_age/InterfaceFigureLocationInternal.java new file mode 100644 index 00000000..058174e1 --- /dev/null +++ b/src/main/java/sk/uniba/fmph/dcs/stone_age/InterfaceFigureLocationInternal.java @@ -0,0 +1,12 @@ +package sk.uniba.fmph.dcs.stone_age; + +import java.util.Collection; + +public interface InterfaceFigureLocationInternal { + boolean placeFigures(Player player, int figureCount); + HasAction tryToPlaceFigures(Player player, int count); + ActionResult makeAction(Player player, Collection inputResources, Collection outputResources); + boolean skipAction(Player player); + HasAction tryToMakeAction(Player player); + boolean newTurn(); +} diff --git a/src/main/java/sk/uniba/fmph/dcs/stone_age/InterfaceNewTurn.java b/src/main/java/sk/uniba/fmph/dcs/stone_age/InterfaceNewTurn.java new file mode 100644 index 00000000..f3d860ae --- /dev/null +++ b/src/main/java/sk/uniba/fmph/dcs/stone_age/InterfaceNewTurn.java @@ -0,0 +1,5 @@ +package sk.uniba.fmph.dcs.stone_age; + +public interface InterfaceNewTurn { + void newTurn(); +} diff --git a/src/main/java/sk/uniba/fmph/dcs/stone_age/Player.java b/src/main/java/sk/uniba/fmph/dcs/stone_age/Player.java new file mode 100644 index 00000000..c89dc6fb --- /dev/null +++ b/src/main/java/sk/uniba/fmph/dcs/stone_age/Player.java @@ -0,0 +1,6 @@ +package sk.uniba.fmph.dcs.stone_age; + +public interface Player { + PlayerOrder playerOrder(); + PlayerBoard playerBoard(); +} diff --git a/src/main/java/sk/uniba/fmph/dcs/stone_age/PlayerBoard.java b/src/main/java/sk/uniba/fmph/dcs/stone_age/PlayerBoard.java new file mode 100644 index 00000000..4d1648ea --- /dev/null +++ b/src/main/java/sk/uniba/fmph/dcs/stone_age/PlayerBoard.java @@ -0,0 +1,5 @@ +package sk.uniba.fmph.dcs.stone_age; + +public interface PlayerBoard { + boolean hasFigures(int count); +} diff --git a/src/main/java/sk/uniba/fmph/dcs/stone_age/ResourceSource.java b/src/main/java/sk/uniba/fmph/dcs/stone_age/ResourceSource.java new file mode 100644 index 00000000..9211c611 --- /dev/null +++ b/src/main/java/sk/uniba/fmph/dcs/stone_age/ResourceSource.java @@ -0,0 +1,159 @@ +package sk.uniba.fmph.dcs.stone_age; + +import java.util.ArrayList; +import java.util.Collection; +import org.json.JSONObject; +import java.util.Map; + +public final class ResourceSource implements InterfaceFigureLocationInternal { + private final String name; + private final Effect resource; + private final int maxFigures; + private final int maxFigureColors; + private final ArrayList figures; + + public ResourceSource(String name, Effect resource, int maxFigures, int maxFigureColors) { + if (!resource.isResourceOrFood()) { + throw new IllegalArgumentException("Resource must be food or resource"); + } + this.name = name; + this.resource = resource; + this.maxFigures = maxFigures; + this.maxFigureColors = maxFigureColors; + this.figures = new ArrayList<>(); + } + + @Override + public boolean placeFigures(Player player, int figureCount) { + // Check if player can place figures here + if (!canPlaceFigures(player, figureCount)) { + return false; + } + + // Add the figures + for (int i = 0; i < figureCount; i++) { + figures.add(player.playerOrder()); + } + return true; + } + + @Override + public HasAction tryToPlaceFigures(Player player, int count) { + if (!player.playerBoard().hasFigures(count)) { + return HasAction.NO_ACTION_POSSIBLE; + } + + if (canPlaceFigures(player, count)) { + return HasAction.WAITING_FOR_PLAYER_ACTION; + } + + return HasAction.NO_ACTION_POSSIBLE; + } + + @Override + public ActionResult makeAction(Player player, Collection inputResources, + Collection outputResources) { + // Verify it's this player's figures + if (!hasFiguresFromPlayer(player.playerOrder())) { + return ActionResult.FAILURE; + } + + // Resource sources don't take input resources + if (!inputResources.isEmpty()) { + return ActionResult.FAILURE; + } + + // Resource sources must output exactly one type of resource per figure + int playerFigureCount = countPlayerFigures(player.playerOrder()); + if (outputResources.size() != playerFigureCount) { + return ActionResult.FAILURE; + } + + for (Effect output : outputResources) { + if (output != this.resource) { + return ActionResult.FAILURE; + } + } + + return ActionResult.ACTION_DONE_WAIT_FOR_TOOL_USE; + } + + @Override + public HasAction tryToMakeAction(Player player) { + if (hasFiguresFromPlayer(player.playerOrder())) { + return HasAction.WAITING_FOR_PLAYER_ACTION; + } + return HasAction.NO_ACTION_POSSIBLE; + } + + @Override + public boolean skipAction(Player player) { + return false; // Can't skip resource gathering + } + + @Override + public boolean newTurn() { + figures.clear(); + return false; // Resource sources don't trigger game end + } + + private boolean canPlaceFigures(Player player, int figureCount) { + // Check if player has enough figures + if (!player.playerBoard().hasFigures(figureCount)) { + return false; + } + + // Check if space available + if (figures.size() + figureCount > maxFigures) { + return false; + } + + // Check if player already has figures here + if (hasFiguresFromPlayer(player.playerOrder())) { + return false; + } + + // Check number of different players + if (!figures.isEmpty() && !containsPlayerOrder(figures, player.playerOrder())) { + int currentColors = countDistinctPlayers(); + if (currentColors >= maxFigureColors) { + return false; + } + } + + return true; + } + + private boolean hasFiguresFromPlayer(PlayerOrder player) { + return containsPlayerOrder(figures, player); + } + + private int countPlayerFigures(PlayerOrder player) { + int count = 0; + for (PlayerOrder p : figures) { + if (p.equals(player)) { + count++; + } + } + return count; + } + + private int countDistinctPlayers() { + return (int) figures.stream().distinct().count(); + } + + private boolean containsPlayerOrder(Collection collection, PlayerOrder player) { + return collection.stream().anyMatch(p -> p.equals(player)); + } + + public String state() { + Map state = Map.of( + "name", name, + "resource", resource, + "maxFigures", maxFigures, + "maxFigureColors", maxFigureColors, + "figures", figures.stream().map(PlayerOrder::getOrder).toList() + ); + return new JSONObject(state).toString(); + } +} diff --git a/src/main/java/sk/uniba/fmph/dcs/stone_age/TribeFedStatus.java b/src/main/java/sk/uniba/fmph/dcs/stone_age/TribeFedStatus.java new file mode 100644 index 00000000..83f22b87 --- /dev/null +++ b/src/main/java/sk/uniba/fmph/dcs/stone_age/TribeFedStatus.java @@ -0,0 +1,104 @@ +package sk.uniba.fmph.dcs.stone_age; + +import org.json.JSONObject; +import java.util.Collection; +import java.util.Map; + +public final class TribeFedStatus implements InterfaceFeedTribe, InterfaceNewTurn { + private static final int MAX_FIELDS = 10; + private static final int FOOD_PER_FIGURE = 1; + + private final PlayerFigures figures; + private boolean tribeFed; + private int fields; + + public TribeFedStatus(PlayerFigures figures) { + this.figures = figures; + this.tribeFed = false; + this.fields = 0; + } + + @Override + public boolean feedTribeIfEnoughFood() { + if (tribeFed) { + return false; + } + + int requiredFood = calculateRequiredFood(); + int foodFromFields = Math.min(fields, requiredFood); + int remainingFood = requiredFood - foodFromFields; + + // If we have enough fields to feed everyone, mark as fed + if (remainingFood == 0) { + tribeFed = true; + return true; + } + + return false; + } + + @Override + public boolean feedTribe(Collection resources) { + if (tribeFed) { + return false; + } + + int requiredFood = calculateRequiredFood(); + int foodFromFields = Math.min(fields, requiredFood); + int remainingFood = requiredFood - foodFromFields; + + // Count food resources provided + int foodProvided = 0; + for (Effect resource : resources) { + if (resource != Effect.FOOD) { + return false; // Only food resources allowed + } + foodProvided++; + } + + // Check if enough food was provided + if (foodProvided == remainingFood) { + tribeFed = true; + return true; + } + + return false; + } + + @Override + public boolean doNotFeedThisTurn() { + if (tribeFed) { + return false; + } + tribeFed = true; + return true; + } + + @Override + public boolean isTribeFed() { + return tribeFed; + } + + @Override + public void newTurn() { + tribeFed = false; + } + + public void addField() { + if (fields < MAX_FIELDS) { + fields++; + } + } + + private int calculateRequiredFood() { + return figures.getTotalFigures() * FOOD_PER_FIGURE; + } + + public String state() { + Map state = Map.of( + "tribeFed", tribeFed, + "fields", fields + ); + return new JSONObject(state).toString(); + } +} diff --git a/src/test/java/sk/uniba/fmph/dcs/game_phase_controller/PlaceFiguresStateTest.java b/src/test/java/sk/uniba/fmph/dcs/game_phase_controller/PlaceFiguresStateTest.java new file mode 100644 index 00000000..a7b31dd9 --- /dev/null +++ b/src/test/java/sk/uniba/fmph/dcs/game_phase_controller/PlaceFiguresStateTest.java @@ -0,0 +1,122 @@ +package sk.uniba.fmph.dcs.game_phase_controller; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.Collection; +import java.util.ArrayList; + +import sk.uniba.fmph.dcs.stone_age.*; + +public class PlaceFiguresStateTest { + private PlaceFiguresState state; + private MockFigureLocation mockLocation; + private PlayerOrder player; + + private class MockFigureLocation implements InterfaceFigureLocation { + private HasAction nextTryResponse = HasAction.NO_ACTION_POSSIBLE; + private boolean nextPlaceResponse = false; + + @Override + public boolean placeFigures(PlayerOrder player, int figureCount) { + return nextPlaceResponse; + } + + @Override + public HasAction tryToPlaceFigures(PlayerOrder player, int count) { + return nextTryResponse; + } + + @Override + public ActionResult makeAction(PlayerOrder player, Collection inputResources, Collection outputResources) { + return ActionResult.FAILURE; + } + + @Override + public boolean skipAction(PlayerOrder player) { + return false; + } + + @Override + public HasAction tryToMakeAction(PlayerOrder player) { + return HasAction.NO_ACTION_POSSIBLE; + } + + @Override + public boolean newTurn() { + return false; + } + + public void setNextTryResponse(HasAction response) { + this.nextTryResponse = response; + } + + public void setNextPlaceResponse(boolean response) { + this.nextPlaceResponse = response; + } + } + + @Before + public void setUp() { + mockLocation = new MockFigureLocation(); + Map places = new HashMap<>(); + places.put(Location.HUNTING_GROUNDS, mockLocation); + state = new PlaceFiguresState(places); + player = new PlayerOrder(0, 2); + } + + @Test + public void testPlaceFiguresSuccess() { + mockLocation.setNextPlaceResponse(true); + assertEquals(ActionResult.ACTION_DONE, + state.placeFigures(player, Location.HUNTING_GROUNDS, 1)); + } + + @Test + public void testPlaceFiguresFailure() { + mockLocation.setNextPlaceResponse(false); + assertEquals(ActionResult.FAILURE, + state.placeFigures(player, Location.HUNTING_GROUNDS, 1)); + } + + @Test + public void testPlaceFiguresInvalidLocation() { + assertEquals(ActionResult.FAILURE, + state.placeFigures(player, Location.TOOL_MAKER, 1)); + } + + @Test + public void testTryToMakeAutomaticActionWaiting() { + mockLocation.setNextTryResponse(HasAction.WAITING_FOR_PLAYER_ACTION); + assertEquals(HasAction.WAITING_FOR_PLAYER_ACTION, + state.tryToMakeAutomaticAction(player)); + } + + @Test + public void testTryToMakeAutomaticActionNoAction() { + mockLocation.setNextTryResponse(HasAction.NO_ACTION_POSSIBLE); + assertEquals(HasAction.NO_ACTION_POSSIBLE, + state.tryToMakeAutomaticAction(player)); + } + + @Test + public void testInvalidActionsReturnFailure() { + assertEquals(ActionResult.FAILURE, + state.makeAction(player, Location.HUNTING_GROUNDS, new ArrayList<>(), new ArrayList<>())); + assertEquals(ActionResult.FAILURE, + state.skipAction(player, Location.HUNTING_GROUNDS)); + assertEquals(ActionResult.FAILURE, + state.useTools(player, 0)); + assertEquals(ActionResult.FAILURE, + state.noMoreToolsThisThrow(player)); + assertEquals(ActionResult.FAILURE, + state.feedTribe(player, new ArrayList<>())); + assertEquals(ActionResult.FAILURE, + state.doNotFeedThisTurn(player)); + assertEquals(ActionResult.FAILURE, + state.makeAllPlayersTakeARewardChoice(player, Effect.WOOD)); + } +} diff --git a/src/test/java/sk/uniba/fmph/dcs/stone_age/ResourceSourceTest.java b/src/test/java/sk/uniba/fmph/dcs/stone_age/ResourceSourceTest.java new file mode 100644 index 00000000..36aac342 --- /dev/null +++ b/src/test/java/sk/uniba/fmph/dcs/stone_age/ResourceSourceTest.java @@ -0,0 +1,111 @@ +package sk.uniba.fmph.dcs.stone_age; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collection; + +public class ResourceSourceTest { + private ResourceSource source; + private Player mockPlayer1; + private Player mockPlayer2; + private PlayerBoard mockBoard1; + private PlayerBoard mockBoard2; + + private class MockPlayerBoard implements PlayerBoard { + private boolean hasFiguresResponse = true; + + @Override + public boolean hasFigures(int count) { + return hasFiguresResponse; + } + + public void setHasFiguresResponse(boolean response) { + this.hasFiguresResponse = response; + } + } + + private class MockPlayer implements Player { + private final PlayerOrder order; + private final PlayerBoard board; + + public MockPlayer(int orderNum, PlayerBoard board) { + this.order = new PlayerOrder(orderNum, 2); + this.board = board; + } + + @Override + public PlayerOrder playerOrder() { + return order; + } + + @Override + public PlayerBoard playerBoard() { + return board; + } + } + + @Before + public void setUp() { + mockBoard1 = new MockPlayerBoard(); + mockBoard2 = new MockPlayerBoard(); + mockPlayer1 = new MockPlayer(0, mockBoard1); + mockPlayer2 = new MockPlayer(1, mockBoard2); + source = new ResourceSource("Forest", Effect.WOOD, 7, 2); + } + + @Test + public void testPlaceFiguresSuccess() { + assertTrue(source.placeFigures(mockPlayer1, 2)); + } + + @Test + public void testPlaceFiguresNoFigures() { + ((MockPlayerBoard)mockBoard1).setHasFiguresResponse(false); + assertFalse(source.placeFigures(mockPlayer1, 2)); + } + + @Test + public void testPlaceFiguresExceedMax() { + assertTrue(source.placeFigures(mockPlayer1, 7)); + assertFalse(source.placeFigures(mockPlayer2, 1)); + } + + @Test + public void testPlaceFiguresExceedColors() { + assertTrue(source.placeFigures(mockPlayer1, 2)); + assertTrue(source.placeFigures(mockPlayer2, 2)); + MockPlayer mockPlayer3 = new MockPlayer(2, new MockPlayerBoard()); + assertFalse(source.placeFigures(mockPlayer3, 2)); + } + + @Test + public void testMakeActionSuccess() { + source.placeFigures(mockPlayer1, 2); + Collection input = new ArrayList<>(); + Collection output = new ArrayList<>(); + output.add(Effect.WOOD); + output.add(Effect.WOOD); + assertEquals(ActionResult.ACTION_DONE_WAIT_FOR_TOOL_USE, + source.makeAction(mockPlayer1, input, output)); + } + + @Test + public void testMakeActionWrongResourceCount() { + source.placeFigures(mockPlayer1, 2); + Collection input = new ArrayList<>(); + Collection output = new ArrayList<>(); + output.add(Effect.WOOD); + assertEquals(ActionResult.FAILURE, + source.makeAction(mockPlayer1, input, output)); + } + + @Test + public void testNewTurnClearsFigures() { + assertTrue(source.placeFigures(mockPlayer1, 2)); + source.newTurn(); + assertTrue(source.placeFigures(mockPlayer1, 2)); + } +} diff --git a/src/test/java/sk/uniba/fmph/dcs/stone_age/TribeFedStatusTest.java b/src/test/java/sk/uniba/fmph/dcs/stone_age/TribeFedStatusTest.java new file mode 100644 index 00000000..dcb1459b --- /dev/null +++ b/src/test/java/sk/uniba/fmph/dcs/stone_age/TribeFedStatusTest.java @@ -0,0 +1,76 @@ +package sk.uniba.fmph.dcs.stone_age; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collection; + +public class TribeFedStatusTest { + private TribeFedStatus status; + + @Before + public void setUp() { + status = new TribeFedStatus(); + } + + @Test + public void testInitialState() { + assertFalse(status.isTribeFed()); + assertEquals(0, status.getFields()); + } + + @Test + public void testAddField() { + status.addField(); + assertEquals(1, status.getFields()); + } + + @Test + public void testAddFieldLimit() { + for(int i = 0; i < 12; i++) { + status.addField(); + } + assertEquals(10, status.getFields()); + } + + @Test + public void testFeedTribeWithFood() { + Collection food = new ArrayList<>(); + food.add(Effect.FOOD); + assertTrue(status.feedTribe(food)); + assertTrue(status.isTribeFed()); + } + + @Test + public void testFeedTribeWithInsufficientFood() { + Collection food = new ArrayList<>(); + assertFalse(status.feedTribe(food)); + assertFalse(status.isTribeFed()); + } + + @Test + public void testNewTurnResetsFedStatus() { + Collection food = new ArrayList<>(); + food.add(Effect.FOOD); + status.feedTribe(food); + assertTrue(status.isTribeFed()); + + status.newTurn(); + assertFalse(status.isTribeFed()); + } + + @Test + public void testDoNotFeedThisTurn() { + assertTrue(status.doNotFeedThisTurn()); + assertTrue(status.isTribeFed()); + } + + @Test + public void testFeedTribeIfEnoughFood() { + // This would need integration with PlayerResourcesAndFood + // to test properly, as it depends on available food resources + assertFalse(status.feedTribeIfEnoughFood()); + } +}