diff --git a/backend/src/main/java/cz/jzitnik/chronos/controllers/GameController.java b/backend/src/main/java/cz/jzitnik/chronos/controllers/GameController.java index feec699..a43b7ca 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/controllers/GameController.java +++ b/backend/src/main/java/cz/jzitnik/chronos/controllers/GameController.java @@ -1,10 +1,12 @@ package cz.jzitnik.chronos.controllers; -import cz.jzitnik.chronos.entities.Game; +import cz.jzitnik.chronos.entities.*; import cz.jzitnik.chronos.payload.errors.NotFoundError; +import cz.jzitnik.chronos.payload.responses.GameWonResponse; import cz.jzitnik.chronos.payload.responses.TestGameKeyResponse; import cz.jzitnik.chronos.payload.responses.UnifiedResponse; import cz.jzitnik.chronos.repository.GameRepository; +import cz.jzitnik.chronos.repository.ItemRepository; import cz.jzitnik.chronos.repository.PlayerRepository; import cz.jzitnik.chronos.services.GameService; import cz.jzitnik.chronos.utils.anotations.CheckUser; @@ -13,6 +15,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.Optional; + @CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/game") @@ -26,6 +30,9 @@ public class GameController { @Autowired private PlayerRepository playerRepository; + @Autowired + private ItemRepository itemRepository; + @PostMapping("/new") public UnifiedResponse createGame() { var game = gameService.createGame(); @@ -90,4 +97,51 @@ public class GameController { return ResponseEntity.ok(UnifiedResponse.success(game.winnable())); } + + @PostMapping("/fragments") + @CheckUser + public ResponseEntity> putKeyFragments(@RequestParam String playerKey) { + var player = playerRepository.findByPlayerKey(playerKey).get(); + var game = player.getGame(); + + if (!player.getCurrentRoom().getName().equals("Outside")) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(UnifiedResponse.failure(new Error("You are not in the correct room!"))); + } + + for (Item item : player.getInventory()) { + if (item.getItemType().equals(ItemType.KEY_FRAGMENT)) { + item.setOwner(null); + itemRepository.save(item); + game.setKeyFragmentsAmount(game.getKeyFragmentsAmount() + 1); + + var message = new Message(player, "", MessageType.KEY_FRAGMENT_HANDED_OVER); + + player.getMessages().add(message); + } + } + + playerRepository.save(player); + + if (game.getKeyFragmentsAmount() >= 4) { + // All key fragments were found + game.setWon(true); + } + + gameRepository.save(game); + + return ResponseEntity.ok(UnifiedResponse.success(null)); + } + + @GetMapping("/won") + @CheckUser + public ResponseEntity> gameWon(@RequestParam String playerKey) { + var player = playerRepository.findByPlayerKey(playerKey).get(); + var game = player.getGame(); + + if (!game.isWon()) { + return ResponseEntity.ok(UnifiedResponse.success(new GameWonResponse(false, Optional.empty()))); + } + + return ResponseEntity.ok(UnifiedResponse.success(new GameWonResponse(true, Optional.of(game.getWonMessage())))); + } } diff --git a/backend/src/main/java/cz/jzitnik/chronos/entities/Game.java b/backend/src/main/java/cz/jzitnik/chronos/entities/Game.java index 540c7bc..1211304 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/entities/Game.java +++ b/backend/src/main/java/cz/jzitnik/chronos/entities/Game.java @@ -39,6 +39,15 @@ public class Game { @JsonIgnore private Player adminPlayer; + @JsonIgnore + private int keyFragmentsAmount = 0; + + @JsonIgnore + private boolean won = false; + + @JsonIgnore + private String wonMessage; + private boolean started = false; public void addPlayer(Player player) { @@ -71,6 +80,6 @@ public class Game { var unplayedCharacters = characters.stream().filter(character -> !character.isInteractedWith()).toList().size(); - return roomItems + unplayedCharacters + ownedFragments >= 5; + return (roomItems + unplayedCharacters + ownedFragments) >= (4 - keyFragmentsAmount); } } diff --git a/backend/src/main/java/cz/jzitnik/chronos/entities/ItemType.java b/backend/src/main/java/cz/jzitnik/chronos/entities/ItemType.java index 784f483..0c3bd0b 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/entities/ItemType.java +++ b/backend/src/main/java/cz/jzitnik/chronos/entities/ItemType.java @@ -5,7 +5,10 @@ import cz.jzitnik.chronos.payload.errors.ItemNotUsableException; public enum ItemType { KEY_FRAGMENT, LUCK_POTION, - GOLDEN_WATCH; + GOLDEN_WATCH, + COAL, + FLOUR, + WATER; public void useItem(Player player) throws ItemNotUsableException { switch (this) { diff --git a/backend/src/main/java/cz/jzitnik/chronos/entities/MessageType.java b/backend/src/main/java/cz/jzitnik/chronos/entities/MessageType.java index 026422a..e43ecd6 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/entities/MessageType.java +++ b/backend/src/main/java/cz/jzitnik/chronos/entities/MessageType.java @@ -6,5 +6,6 @@ public enum MessageType { GOT_ITEM, LOST_ITEM, ITEM_USED, + KEY_FRAGMENT_HANDED_OVER, JOINED, } diff --git a/backend/src/main/java/cz/jzitnik/chronos/interactions/Interaction.java b/backend/src/main/java/cz/jzitnik/chronos/interactions/Interaction.java index 3f9c82c..ce83093 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/interactions/Interaction.java +++ b/backend/src/main/java/cz/jzitnik/chronos/interactions/Interaction.java @@ -5,5 +5,7 @@ public enum Interaction { Cashier, Librarian, Innkeeper, - Mayor + Mayor, + Blacksmith, + Baker } \ No newline at end of file diff --git a/backend/src/main/java/cz/jzitnik/chronos/interactions/InteractionService.java b/backend/src/main/java/cz/jzitnik/chronos/interactions/InteractionService.java index 9784f47..3750dd9 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/interactions/InteractionService.java +++ b/backend/src/main/java/cz/jzitnik/chronos/interactions/InteractionService.java @@ -2,10 +2,7 @@ package cz.jzitnik.chronos.interactions; import cz.jzitnik.chronos.entities.Player; import cz.jzitnik.chronos.entities.Character; -import cz.jzitnik.chronos.interactions.list.Hangman; -import cz.jzitnik.chronos.interactions.list.Mayor; -import cz.jzitnik.chronos.interactions.list.RockPaperScissors; -import cz.jzitnik.chronos.interactions.list.TicTacToe; +import cz.jzitnik.chronos.interactions.list.*; import cz.jzitnik.chronos.interactions.list.wordle.Wordle; import cz.jzitnik.chronos.payload.responses.InteractionResponse; import org.springframework.beans.factory.annotation.Autowired; @@ -23,6 +20,10 @@ public class InteractionService { private TicTacToe ticTacToe; @Autowired private Mayor mayor; + @Autowired + private NumberGuessingGame numberGuessingGame; + @Autowired + private Baker baker; @FunctionalInterface public interface Function3 { @@ -36,6 +37,8 @@ public class InteractionService { case Librarian -> wordle::play; case Innkeeper -> ticTacToe::play; case Mayor -> mayor::play; + case Blacksmith -> numberGuessingGame::play; + case Baker -> baker::play; }; } } \ No newline at end of file diff --git a/backend/src/main/java/cz/jzitnik/chronos/interactions/list/Baker.java b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/Baker.java new file mode 100644 index 0000000..1bf3315 --- /dev/null +++ b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/Baker.java @@ -0,0 +1,57 @@ +package cz.jzitnik.chronos.interactions.list; + +import cz.jzitnik.chronos.entities.Character; +import cz.jzitnik.chronos.entities.Item; +import cz.jzitnik.chronos.entities.ItemType; +import cz.jzitnik.chronos.entities.Player; +import cz.jzitnik.chronos.interactions.InteractionPlayer; +import cz.jzitnik.chronos.payload.responses.InteractionResponse; +import cz.jzitnik.chronos.repository.CharacterRepository; +import cz.jzitnik.chronos.repository.ItemRepository; +import cz.jzitnik.chronos.services.ItemService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; + +@Service +public class Baker implements InteractionPlayer { + @Autowired + private ItemService itemService; + @Autowired + private CharacterRepository characterRepository; + @Autowired + private ItemRepository itemRepository; + + @Override + public InteractionResponse play(Player player, Character character, String data) { + var playerItems = player.getInventory(); + var flours = playerItems.stream().filter(item -> item.getItemType().equals(ItemType.FLOUR)).toList(); + var water = playerItems.stream().filter(item -> item.getItemType().equals(ItemType.WATER)).toList(); + + if (flours.isEmpty() || water.isEmpty()) { + return new InteractionResponse(false, "Nemáš pro mě buď mouku nebo vodu. Dej mi obojí najednou!", new ArrayList<>()); + } + + var flour = flours.getFirst(); + flour.setOwner(null); + itemRepository.save(flour); + playerItems.remove(flour); + + var onewater = water.getFirst(); + onewater.setOwner(null); + itemRepository.save(onewater); + playerItems.remove(onewater); + + var item = new Item(ItemType.KEY_FRAGMENT, player); + itemService.addItem(player, item); + + character.setInteractedWith(true); + characterRepository.save(character); + + var items = new ArrayList(); + items.add(item); + + return new InteractionResponse(false, "Díky moc za mouku a vodu. Doplnit dialog!!!", items); + } +} diff --git a/backend/src/main/java/cz/jzitnik/chronos/interactions/list/Mayor.java b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/Mayor.java index 031db0d..5a1db8f 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/interactions/list/Mayor.java +++ b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/Mayor.java @@ -26,16 +26,16 @@ public class Mayor implements InteractionPlayer { @Override public InteractionResponse play(Player player, Character character, String data) { var playerItems = player.getInventory(); - var golderWatches = playerItems.stream().filter(item -> item.getItemType().equals(ItemType.GOLDEN_WATCH)).toList(); + var goldenWatches = playerItems.stream().filter(item -> item.getItemType().equals(ItemType.GOLDEN_WATCH)).toList(); - if (golderWatches.isEmpty()) { - return new InteractionResponse(false, "Golden watch is not present in your inventory!", new ArrayList<>()); + if (goldenWatches.isEmpty()) { + return new InteractionResponse(false, "Nemáš zlaté hodinky v inventáři!", new ArrayList<>()); } - var golderWatch = golderWatches.getFirst(); - golderWatch.setOwner(null); - itemRepository.save(golderWatch); - playerItems.remove(golderWatch); + var goldenWatch = goldenWatches.getFirst(); + goldenWatch.setOwner(null); + itemRepository.save(goldenWatch); + playerItems.remove(goldenWatch); var item = new Item(ItemType.KEY_FRAGMENT, player); itemService.addItem(player, item); diff --git a/backend/src/main/java/cz/jzitnik/chronos/interactions/list/NumberGuessingGame.java b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/NumberGuessingGame.java new file mode 100644 index 0000000..86d6014 --- /dev/null +++ b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/NumberGuessingGame.java @@ -0,0 +1,210 @@ +package cz.jzitnik.chronos.interactions.list; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import cz.jzitnik.chronos.entities.Character; +import cz.jzitnik.chronos.entities.Item; +import cz.jzitnik.chronos.entities.ItemType; +import cz.jzitnik.chronos.entities.Player; +import cz.jzitnik.chronos.interactions.InteractionPlayer; +import cz.jzitnik.chronos.payload.responses.InteractionResponse; +import cz.jzitnik.chronos.repository.CharacterRepository; +import cz.jzitnik.chronos.repository.ItemRepository; +import cz.jzitnik.chronos.repository.PlayerRepository; +import cz.jzitnik.chronos.services.ItemService; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Random; + +@Service +public class NumberGuessingGame implements InteractionPlayer { + @Autowired + private CharacterRepository characterRepository; + @Autowired + private ItemService itemService; + @Autowired + private PlayerRepository playerRepository; + @Autowired + private ItemRepository itemRepository; + + private static final int RANGE_MIN = 1; + private static final int RANGE_MAX = 100; + private static final int MAX_ATTEMPTS = 7; + + @AllArgsConstructor + @NoArgsConstructor + @Getter + @Setter + private static class GuessingGameMemory { + private int targetNumber; + private int attemptsLeft; + } + + private static GuessingGameMemory readMemory(String memory) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.readValue(memory, GuessingGameMemory.class); + } catch (Exception e) { + throw new RuntimeException("Error reading memory from DB."); + } + } + + private static String writeMemory(GuessingGameMemory memory) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.writeValueAsString(memory); + } catch (JsonProcessingException e) { + throw new RuntimeException("Error writing memory to DB."); + } + } + + @AllArgsConstructor + @Getter + @Setter + private static class GuessingGameResponse { + public enum Type { + GAME_CREATED, + GAME_WON, + GAME_LOST, + GUESS_FEEDBACK + } + + private Type type; + private String message; + private int attemptsLeft; + } + + @Override + public InteractionResponse play(Player player, Character character, String data) { + if (character.getInteractionData().getPlayer() == null) { + // New game initialization + try { + return initGame(player, character); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + // Ongoing game + if (!character.getInteractionData().getPlayer().getId().equals(player.getId())) { + return new InteractionResponse(false, "already_playing", new ArrayList<>()); + } + + try { + return processGuess(player, character, data); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private InteractionResponse initGame(Player player, Character character) throws JsonProcessingException { + var coals = player.getInventory().stream().filter(item -> item.getItemType().equals(ItemType.COAL)).toList(); + if (coals.isEmpty()) { + return new InteractionResponse(false, "Nemáš uhlí pro mě!", new ArrayList<>()); + } + var coal = coals.getFirst(); + coal.setOwner(null); + itemRepository.save(coal); + player.getInventory().remove(coal); + playerRepository.save(player); + + var interactionData = character.getInteractionData(); + interactionData.setPlayer(player); + + Random random = new Random(); + int targetNumber = random.nextInt(RANGE_MAX - RANGE_MIN + 1) + RANGE_MIN; + + interactionData.setMemory(writeMemory( + new GuessingGameMemory(targetNumber, MAX_ATTEMPTS) + )); + + characterRepository.save(character); + + var response = new GuessingGameResponse( + GuessingGameResponse.Type.GAME_CREATED, + String.format("Uhodni číslo mezi %d a %d.", RANGE_MIN, RANGE_MAX), + MAX_ATTEMPTS + ); + + ObjectMapper objectMapper = new ObjectMapper(); + return new InteractionResponse(true, objectMapper.writeValueAsString(response), new ArrayList<>()); + } + + private InteractionResponse processGuess(Player player, Character character, String data) throws JsonProcessingException { + var memory = readMemory(character.getInteractionData().getMemory()); + + int guess; + try { + guess = Integer.parseInt(data); + } catch (NumberFormatException e) { + return new InteractionResponse(false, "invalid_input", new ArrayList<>()); + } + + if (guess < RANGE_MIN || guess > RANGE_MAX) { + return new InteractionResponse(false, "out_of_range", new ArrayList<>()); + } + + memory.setAttemptsLeft(memory.getAttemptsLeft() - 1); + String feedbackMessage; + boolean isGameWon = false; + + if (guess == memory.getTargetNumber()) { + feedbackMessage = "Gratuluji! Uhodl jsi číslo. Tady máš fragment klíče."; + isGameWon = true; + } else if (guess < memory.getTargetNumber()) { + feedbackMessage = "Tvoje číslo je moc nízké. Zkus to znovu."; + } else { + feedbackMessage = "Tvoje číslo je moc vysoké. Zkus to znovu."; + } + + if (isGameWon || memory.getAttemptsLeft() <= 0) { + character.setInteractedWith(true); + characterRepository.save(character); + + if (isGameWon) { + var item = new Item(ItemType.KEY_FRAGMENT, player); + itemService.addItem(player, item); + + var items = new ArrayList(); + items.add(item); + + var response = new GuessingGameResponse( + GuessingGameResponse.Type.GAME_WON, + feedbackMessage, + memory.getAttemptsLeft() + ); + + ObjectMapper objectMapper = new ObjectMapper(); + return new InteractionResponse(true, objectMapper.writeValueAsString(response), items); + } else { + var response = new GuessingGameResponse( + GuessingGameResponse.Type.GAME_LOST, + String.format("Prohrál jsi! Správné číslo bylo %d.", memory.getTargetNumber()), + memory.getAttemptsLeft() + ); + + ObjectMapper objectMapper = new ObjectMapper(); + return new InteractionResponse(false, objectMapper.writeValueAsString(response), new ArrayList<>()); + } + } + + // Update memory and save + character.getInteractionData().setMemory(writeMemory(memory)); + characterRepository.save(character); + + var response = new GuessingGameResponse( + GuessingGameResponse.Type.GUESS_FEEDBACK, + feedbackMessage, + memory.getAttemptsLeft() + ); + + ObjectMapper objectMapper = new ObjectMapper(); + return new InteractionResponse(false, objectMapper.writeValueAsString(response), new ArrayList<>()); + } +} diff --git a/backend/src/main/java/cz/jzitnik/chronos/payload/responses/GameWonResponse.java b/backend/src/main/java/cz/jzitnik/chronos/payload/responses/GameWonResponse.java new file mode 100644 index 0000000..6f8834a --- /dev/null +++ b/backend/src/main/java/cz/jzitnik/chronos/payload/responses/GameWonResponse.java @@ -0,0 +1,15 @@ +package cz.jzitnik.chronos.payload.responses; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Optional; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class GameWonResponse { + private boolean won; + private Optional msg; +} diff --git a/backend/src/main/java/cz/jzitnik/chronos/services/InitGameService.java b/backend/src/main/java/cz/jzitnik/chronos/services/InitGameService.java index 9dbb222..74a5f92 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/services/InitGameService.java +++ b/backend/src/main/java/cz/jzitnik/chronos/services/InitGameService.java @@ -137,6 +137,55 @@ public class InitGameService { // Zlaté hodinky pro starostu garden.getItems().add(new Item(ItemType.GOLDEN_WATCH, garden)); + // Kovárna + var forge = new Room( + "Kovárna", + game + ); + var forge_characters = new ArrayList(); + var blacksmith = new Character( + "Kovář", + forge, + "Ahoj já jsem kovář. Dones mi uhlí pro moji pec a pak si spolu můžeme zahrát number guessing game" + ); + blacksmith.setInteraction(Interaction.Blacksmith); + blacksmith.setInteractionData(new cz.jzitnik.chronos.entities.Interaction( + "", + "Uhlí už mám. Díky moc za uhlí!", + mayor + )); + forge_characters.add(blacksmith); + forge.setCharacters(forge_characters); + + // Sklad + var warehouse = new Room( + "Sklad", + game + ); + warehouse.getItems().add(new Item(ItemType.COAL, warehouse)); + warehouse.getItems().add(new Item(ItemType.WATER, warehouse)); + warehouse.getItems().add(new Item(ItemType.FLOUR, warehouse)); + + // Pekárna + var bakery = new Room( + "Pekárna", + game + ); + var bakery_characters = new ArrayList(); + var baker = new Character( + "Pekař", + bakery, + "Ahoj já jsem pekař a potřebuji mouku a láhev vody. Prosím dones mi je a dostaneš fragment klíče" + ); + baker.setInteraction(Interaction.Baker); + baker.setInteractionData(new cz.jzitnik.chronos.entities.Interaction( + "", + "Mouku a vodu už mám. Díky", + baker + )); + bakery_characters.add(baker); + bakery.setCharacters(bakery_characters); + rooms.add(outside); rooms.add(stodola); @@ -145,9 +194,14 @@ public class InitGameService { rooms.add(inn); rooms.add(mayor_house); rooms.add(garden); + rooms.add(forge); + rooms.add(warehouse); + rooms.add(bakery); game.setRooms(rooms); + game.setWonMessage("Gratuluji vyhráli jste!"); + // Return a room that all players will spawn in return outside; } diff --git a/frontend/src/main/java/cz/jzitnik/api/ApiService.java b/frontend/src/main/java/cz/jzitnik/api/ApiService.java index ea2c500..27d846a 100644 --- a/frontend/src/main/java/cz/jzitnik/api/ApiService.java +++ b/frontend/src/main/java/cz/jzitnik/api/ApiService.java @@ -3,6 +3,7 @@ package cz.jzitnik.api; import cz.jzitnik.api.requests.InteractionRequest; import cz.jzitnik.api.requests.MessageRequest; import cz.jzitnik.api.requests.PlayerNameRequest; +import cz.jzitnik.api.responses.GameWonResponse; import cz.jzitnik.api.responses.InteractionResponse; import cz.jzitnik.api.responses.InventoryFullResponse; import cz.jzitnik.api.responses.UnifiedResponse; @@ -127,4 +128,14 @@ public interface ApiService { @Query("playerKey") String playerKey, @Query("roomId") Long roomId ); + + @POST("game/fragments") + Call> putKeyFragments( + @Query("playerKey") String playerKey + ); + + @GET("game/won") + Call> gameWon( + @Query("playerKey") String playerKey + ); } diff --git a/frontend/src/main/java/cz/jzitnik/api/responses/GameWonResponse.java b/frontend/src/main/java/cz/jzitnik/api/responses/GameWonResponse.java new file mode 100644 index 0000000..57bdb24 --- /dev/null +++ b/frontend/src/main/java/cz/jzitnik/api/responses/GameWonResponse.java @@ -0,0 +1,10 @@ +package cz.jzitnik.api.responses; + +import lombok.Getter; + +@Getter +public class GameWonResponse { + private boolean won; + + private String msg; +} diff --git a/frontend/src/main/java/cz/jzitnik/api/types/Interaction.java b/frontend/src/main/java/cz/jzitnik/api/types/Interaction.java index 03e27f6..2e17d68 100644 --- a/frontend/src/main/java/cz/jzitnik/api/types/Interaction.java +++ b/frontend/src/main/java/cz/jzitnik/api/types/Interaction.java @@ -5,5 +5,7 @@ public enum Interaction { Cashier, Librarian, Innkeeper, - Mayor + Mayor, + Blacksmith, + Baker } diff --git a/frontend/src/main/java/cz/jzitnik/api/types/ItemType.java b/frontend/src/main/java/cz/jzitnik/api/types/ItemType.java index 125fed1..c2a574c 100644 --- a/frontend/src/main/java/cz/jzitnik/api/types/ItemType.java +++ b/frontend/src/main/java/cz/jzitnik/api/types/ItemType.java @@ -6,7 +6,10 @@ import java.util.Set; public enum ItemType { KEY_FRAGMENT, LUCK_POTION, - GOLDEN_WATCH; + GOLDEN_WATCH, + COAL, + WATER, + FLOUR; private static final Set usableItems = new HashSet<>(); @@ -24,6 +27,9 @@ public enum ItemType { case LUCK_POTION -> "Lektvar štěstí"; case KEY_FRAGMENT -> "Fragment klíče"; case GOLDEN_WATCH -> "Zlaté hodinky"; + case COAL -> "Uhlí"; + case WATER -> "Láhev vody"; + case FLOUR -> "Mouka"; }; } } diff --git a/frontend/src/main/java/cz/jzitnik/api/types/Message.java b/frontend/src/main/java/cz/jzitnik/api/types/Message.java index cb62268..8334408 100644 --- a/frontend/src/main/java/cz/jzitnik/api/types/Message.java +++ b/frontend/src/main/java/cz/jzitnik/api/types/Message.java @@ -65,7 +65,7 @@ public class Message { yield "Hráč " + Cli.Colors.BLUE + author.getName() + Cli.Colors.RESET + " sice získal " + Cli.Colors.BLUE + item + Cli.Colors.RESET + ", ale měl plný inventář a tím pádem ho " + Cli.Colors.RED + "navždy ztatil" + Cli.Colors.RESET + "."; } catch (JsonProcessingException e) { - yield "Hráč " + author.getName() + " sice získal item, ale měl plný inventář a tím pádem ho navždy ztratil."; + yield "Hráč " + Cli.Colors.BLUE + author.getName() + Cli.Colors.RESET + " sice získal item, ale měl plný inventář a tím pádem ho navždy ztratil."; } } case ITEM_USED -> { @@ -85,6 +85,7 @@ public class Message { yield "Hráč " + Cli.Colors.BLUE + author.getName() + Cli.Colors.RESET + " využil item unknown."; } } + case KEY_FRAGMENT_HANDED_OVER -> "Hráč " + Cli.Colors.BLUE + author.getName() + Cli.Colors.RESET + " odevzdal " + Cli.Colors.BLUE + "Fragment klíče" + Cli.Colors.RESET + "."; }; } } diff --git a/frontend/src/main/java/cz/jzitnik/api/types/MessageType.java b/frontend/src/main/java/cz/jzitnik/api/types/MessageType.java index 5b06336..47c78f8 100644 --- a/frontend/src/main/java/cz/jzitnik/api/types/MessageType.java +++ b/frontend/src/main/java/cz/jzitnik/api/types/MessageType.java @@ -6,5 +6,6 @@ public enum MessageType { GOT_ITEM, LOST_ITEM, ITEM_USED, + KEY_FRAGMENT_HANDED_OVER, JOINED, } diff --git a/frontend/src/main/java/cz/jzitnik/game/Chronos.java b/frontend/src/main/java/cz/jzitnik/game/Chronos.java index ecbba87..2a62127 100644 --- a/frontend/src/main/java/cz/jzitnik/game/Chronos.java +++ b/frontend/src/main/java/cz/jzitnik/game/Chronos.java @@ -238,7 +238,7 @@ public class Chronos { if (characters.isEmpty()) { Cli.type("V místnosti se nenachází jediná duše..."); } - talk(characters); + talk(characters, room.getName().equals("Outside")); var responseRooms = apiService.getAllRooms(localData.getUserSecret()).execute(); var rooms = responseRooms.body().getData().get().stream().filter(rm -> !rm.getId().equals(room.getId())).toList(); @@ -251,12 +251,35 @@ public class Chronos { visit(rooms.get(selectedIndex).getId(), true); } - public void talk(List characters) throws IOException { + public void talk(List characters, boolean isOutside) throws IOException { List commands = new ArrayList<>(characters.stream().map(chachar -> "Promluvit si s " + chachar.getName()).toList()); + if (isOutside) { + commands.add(Cli.Colors.GREEN + "Odevzdat fragmenty klíče" + Cli.Colors.RESET); + } commands.add("Přejít do jiné místnosti"); CommandPalette commandPalette = new CommandPalette(commands, apiService, localData.getUserSecret()); int selectedIndex = commandPalette.displayIndex(); + if (isOutside && selectedIndex == commands.size() - 2) { + // Odevzdat fragmenty klíče + var response = apiService.getMe(localData.getUserSecret()).execute(); + var me = response.body().getData().get(); + var keyFragments = me.getInventory().stream().filter(item -> item.getItemType().equals(ItemType.KEY_FRAGMENT)).toList(); + + if (keyFragments.isEmpty()) { + Cli.error("Nemáte žádné fragmenty klíče v inventáři!"); + talk(characters, false); + return; + } + + apiService.putKeyFragments(getLocalData().getUserSecret()).execute(); + + Cli.success("Odevzdal jsi " + keyFragments.size() + "x fragment klíče."); + + talk(characters, false); + return; + } + if (selectedIndex == commands.size() - 1) { // Přejít do jiné místnosti return; @@ -287,6 +310,6 @@ public class Chronos { var res = apiService.getCurrentRoom(localData.getUserSecret()).execute(); var room = res.body().getData().get(); - talk(room.getCharacters()); + talk(room.getCharacters(), isOutside); } } \ No newline at end of file diff --git a/frontend/src/main/java/cz/jzitnik/game/CommandPalette.java b/frontend/src/main/java/cz/jzitnik/game/CommandPalette.java index 97773a3..94fc6bd 100644 --- a/frontend/src/main/java/cz/jzitnik/game/CommandPalette.java +++ b/frontend/src/main/java/cz/jzitnik/game/CommandPalette.java @@ -2,10 +2,12 @@ package cz.jzitnik.game; import cz.jzitnik.api.ApiService; import cz.jzitnik.utils.Cli; +import cz.jzitnik.utils.ConfigPathProvider; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -56,6 +58,22 @@ public class CommandPalette { } public int displayIndex() throws IOException { + var res = apiService.gameWon(playerKey).execute(); + var content = res.body().getData().get(); + + if (content.isWon()) { + System.out.println("\n\n"); + Cli.printHeader("Hra byla vyhrána"); + Cli.typeSkritek(content.getMsg()); + + var configPath = ConfigPathProvider.getPath(); + + File file = new File(configPath); + file.delete(); + + System.exit(0); + } + List allCommands = new ArrayList<>(commands); // Commands that can be used anywhere diff --git a/frontend/src/main/java/cz/jzitnik/game/interactions/Interactions.java b/frontend/src/main/java/cz/jzitnik/game/interactions/Interactions.java index 82813af..05f4bca 100644 --- a/frontend/src/main/java/cz/jzitnik/game/interactions/Interactions.java +++ b/frontend/src/main/java/cz/jzitnik/game/interactions/Interactions.java @@ -12,7 +12,7 @@ public class Interactions { var response = apiService.getInteractionData(playerKey, character.getId()).execute(); var interactionData = response.body().getData().get(); - if (character.isInteractedWith()) { + if (character.isInteractedWith() && !interactionData.getInteractedWithText().isBlank()) { Cli.type(character, interactionData.getInteractedWithText()); return; } @@ -42,6 +42,14 @@ public class Interactions { Mayor mayor = new Mayor(character, apiService, playerKey); mayor.play(); } + case Blacksmith -> { + NumberGuessingGame numberGuessingGame = new NumberGuessingGame(character, apiService, playerKey); + numberGuessingGame.play(); + } + case Baker -> { + Baker baker = new Baker(character, apiService, playerKey); + baker.play(); + } } } } diff --git a/frontend/src/main/java/cz/jzitnik/game/interactions/list/Baker.java b/frontend/src/main/java/cz/jzitnik/game/interactions/list/Baker.java new file mode 100644 index 0000000..cd0a58f --- /dev/null +++ b/frontend/src/main/java/cz/jzitnik/game/interactions/list/Baker.java @@ -0,0 +1,43 @@ +package cz.jzitnik.game.interactions.list; + +import cz.jzitnik.api.ApiService; +import cz.jzitnik.api.requests.InteractionRequest; +import cz.jzitnik.api.types.Character; +import cz.jzitnik.api.types.ItemType; +import cz.jzitnik.utils.Cli; +import lombok.AllArgsConstructor; + +import java.io.IOException; +import java.util.Arrays; + + +@AllArgsConstructor +public class Baker { + private Character character; + private ApiService apiService; + private String playerKey; + + public void play() throws IOException { + var response = apiService.getMe(playerKey).execute(); + var me = response.body().getData().get(); + + var hasWater = me.getInventory().stream().anyMatch(item -> item.getItemType().equals(ItemType.WATER)); + var hasFlour = me.getInventory().stream().anyMatch(item -> item.getItemType().equals(ItemType.FLOUR)); + + if (!hasWater || !hasFlour) { + // Player does not have water or flour + return; + } + + var options = new String[]{"Dát pekařovi vodu a mouku", "Nedat pekařovi vodu a mouku"}; + var selected = Cli.selectOptionIndex(Arrays.stream(options).toList()); + + if (selected == 0) { + var res = apiService.interact(playerKey, new InteractionRequest("", character.getId())).execute(); + var data = res.body().getData().get(); + + Cli.type(character, data.getResponseText()); + Cli.gotItems(data.getItems()); + } + } +} diff --git a/frontend/src/main/java/cz/jzitnik/game/interactions/list/Mayor.java b/frontend/src/main/java/cz/jzitnik/game/interactions/list/Mayor.java index 1d125ff..550373f 100644 --- a/frontend/src/main/java/cz/jzitnik/game/interactions/list/Mayor.java +++ b/frontend/src/main/java/cz/jzitnik/game/interactions/list/Mayor.java @@ -21,12 +21,12 @@ public class Mayor { var response = apiService.getMe(playerKey).execute(); var me = response.body().getData().get(); - if (!me.getInventory().stream().anyMatch(item -> item.getItemType().equals(ItemType.GOLDEN_WATCH))) { + if (me.getInventory().stream().noneMatch(item -> item.getItemType().equals(ItemType.GOLDEN_WATCH))) { // Player does not have golden watch return; } - var options = new String[]{"Dát starostovi zlaté hodinky", "Nedát starostovi zlaté hodinky"}; + var options = new String[]{"Dát starostovi zlaté hodinky", "Nedat starostovi zlaté hodinky"}; var selected = Cli.selectOptionIndex(Arrays.stream(options).toList()); if (selected == 0) { diff --git a/frontend/src/main/java/cz/jzitnik/game/interactions/list/NumberGuessingGame.java b/frontend/src/main/java/cz/jzitnik/game/interactions/list/NumberGuessingGame.java new file mode 100644 index 0000000..6da245e --- /dev/null +++ b/frontend/src/main/java/cz/jzitnik/game/interactions/list/NumberGuessingGame.java @@ -0,0 +1,116 @@ +package cz.jzitnik.game.interactions.list; + +import com.fasterxml.jackson.databind.ObjectMapper; +import cz.jzitnik.api.ApiService; +import cz.jzitnik.api.requests.InteractionRequest; +import cz.jzitnik.api.responses.InteractionResponse; +import cz.jzitnik.api.types.Character; +import cz.jzitnik.api.types.ItemType; +import cz.jzitnik.utils.Cli; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.io.Console; +import java.io.IOException; +import java.util.Arrays; + +@AllArgsConstructor +public class NumberGuessingGame { + private Character character; + private ApiService apiService; + private String playerKey; + + @Getter + private static class NumberGuessingResponse { + public enum Type { + GAME_CREATED, + GAME_WON, + GAME_LOST, + GUESS_FEEDBACK + } + + private Type type; + private String message; + private int attemptsLeft; + } + + public void play() throws IOException { + var response = apiService.isInteracting(playerKey, character.getId()).execute(); + var interacting = response.body().getData().get(); + + if (!interacting) { + init(); + } else { + // Resume the game where it was left off + var body = apiService.interact(playerKey, new InteractionRequest("get_data", character.getId())).execute().body(); + var data = body.getData().get(); + gameContinue(data); + } + } + + private void init() throws IOException { + var response = apiService.getMe(playerKey).execute(); + var me = response.body().getData().get(); + + if (me.getInventory().stream().noneMatch(item -> item.getItemType().equals(ItemType.COAL))) { + // Hráč nemá uhlí + return; + } + + var options = new String[] { "Dát kovářovi uhlí", "Nedat kovářovi uhlí" }; + var index = Cli.selectOptionIndex(Arrays.asList(options)); + + if (index == 1) { + return; + } + + var body = apiService.interact(playerKey, new InteractionRequest("", character.getId())).execute().body(); + var data = body.getData().get(); + gameContinue(data); + } + + private void gameContinue(InteractionResponse data) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + var gameData = objectMapper.readValue(data.getResponseText(), NumberGuessingResponse.class); + + switch (gameData.getType()) { + case GAME_WON -> { + Cli.type(character, gameData.getMessage()); + Cli.gotItems(data.getItems()); + } + case GAME_LOST -> Cli.type(character, gameData.getMessage()); + case GUESS_FEEDBACK, GAME_CREATED -> { + Cli.type(character, gameData.getMessage()); + System.out.println("Počet pokusů: " + gameData.getAttemptsLeft()); + + var guess = askForGuess(); + + var response = apiService.interact(playerKey, new InteractionRequest(guess, character.getId())).execute(); + + var body = response.body().getData().get(); + + gameContinue(body); + } + } + } + + private String askForGuess() { + Console console = System.console(); + + String data = console.readLine("Zadejte číslo: "); + + try { + int num = Integer.parseInt(data); + + if (num < 1 || num > 100) { + Cli.error("Číslo musí být v rozmezí 1-100"); + return askForGuess(); + } + + return data; + } catch (NumberFormatException e) { + Cli.error("Neplatný vstup! Zadejte platné číslo."); + return askForGuess(); + } + } +} diff --git a/frontend/src/main/java/cz/jzitnik/utils/Cli.java b/frontend/src/main/java/cz/jzitnik/utils/Cli.java index 9363931..55af163 100644 --- a/frontend/src/main/java/cz/jzitnik/utils/Cli.java +++ b/frontend/src/main/java/cz/jzitnik/utils/Cli.java @@ -126,24 +126,27 @@ public class Cli { } var itemsMapped = items.stream().map(Item::toString).toList(); - Cli.info("Dostal jste: " + String.join(", ", itemsMapped)); + info("Dostal jste: " + String.join(", ", itemsMapped)); } public static void type(Character character, String text) { - Cli.type(Cli.Colors.YELLOW + character.getName() + ": " + Cli.Colors.RESET + text); + type(Colors.YELLOW + character.getName() + ": " + Colors.RESET + text); } public static void type(String text) { - Cli.typeText(Cli.wrapText(text)); + typeText(wrapText(text)); + } + public static void typeSkritek(String text) { + type(Colors.YELLOW + "Skřítek: " + Colors.RESET + text); } public static void info(String text) { - System.out.println(Colors.BLUE + "Info: " + Colors.RESET + Cli.wrapText(text, 6)); + System.out.println(Colors.BLUE + "Info: " + Colors.RESET + wrapText(text, 6)); } public static void success(String text) { - System.out.println(Colors.GREEN + "Úspěch: " + Colors.RESET + Cli.wrapText(text, 8)); + System.out.println(Colors.GREEN + "Úspěch: " + Colors.RESET + wrapText(text, 8)); } public static void error(String text) { - System.out.println(Colors.RED + "Chyba: " + Colors.RESET + Cli.wrapText(text, 7)); + System.out.println(Colors.RED + "Chyba: " + Colors.RESET + wrapText(text, 7)); }