diff --git a/backend/src/main/java/cz/jzitnik/chronos/controllers/CharacterController.java b/backend/src/main/java/cz/jzitnik/chronos/controllers/CharacterController.java index 2fbf48a..31225cd 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/controllers/CharacterController.java +++ b/backend/src/main/java/cz/jzitnik/chronos/controllers/CharacterController.java @@ -3,6 +3,7 @@ package cz.jzitnik.chronos.controllers; import cz.jzitnik.chronos.entities.Interaction; import cz.jzitnik.chronos.entities.Item; import cz.jzitnik.chronos.interactions.InteractionService; +import cz.jzitnik.chronos.payload.errors.NotFoundError; import cz.jzitnik.chronos.payload.requests.InteractionRequest; import cz.jzitnik.chronos.payload.responses.InteractionResponse; import cz.jzitnik.chronos.payload.responses.TakeItemsResponse; @@ -53,6 +54,27 @@ public class CharacterController { ); } + @GetMapping("/interacting") + @CheckUser + public ResponseEntity> isInteracting(@RequestParam String playerKey, @RequestParam Long characterId) { + var player = playerRepository.findByPlayerKey(playerKey).get(); + var characterOptional = characterRepository.findById(characterId); + + if (characterOptional.isEmpty()) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(UnifiedResponse.failure(new NotFoundError("Character was not found!"))); + } + + var character = characterOptional.get(); + + var playerInteractingWithCharacter = character.getInteractionData().getPlayer(); + + if (playerInteractingWithCharacter == null || !playerInteractingWithCharacter.getId().equals(player.getId())) { + return ResponseEntity.ok(UnifiedResponse.success(false)); + } + + return ResponseEntity.ok(UnifiedResponse.success(true)); + } + @PostMapping("/interact") @CheckUser public ResponseEntity> interact(@RequestParam String playerKey, @RequestBody InteractionRequest interactionRequest) { diff --git a/backend/src/main/java/cz/jzitnik/chronos/entities/Interaction.java b/backend/src/main/java/cz/jzitnik/chronos/entities/Interaction.java index 8036079..38c55ae 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/entities/Interaction.java +++ b/backend/src/main/java/cz/jzitnik/chronos/entities/Interaction.java @@ -25,16 +25,16 @@ public class Interaction { @JsonIgnore private Character character; - public Interaction(String startText, String interactedWithText, Character character) { - this.startText = startText; - this.interactedWithText = interactedWithText; - this.character = character; - } - @JsonIgnore private String memory; @ManyToOne @JsonIgnore private Player player; + + public Interaction(String startText, String interactedWithText, Character character) { + this.startText = startText; + this.interactedWithText = interactedWithText; + this.character = character; + } } diff --git a/backend/src/main/java/cz/jzitnik/chronos/interactions/list/Hangman.java b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/Hangman.java index ce3c544..bd1844e 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/interactions/list/Hangman.java +++ b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/Hangman.java @@ -1,16 +1,219 @@ 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.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.HashSet; +import java.util.Random; +import java.util.Set; + @Service public class Hangman implements InteractionPlayer { + @Autowired + private CharacterRepository characterRepository; + @Autowired + private ItemService itemService; + + private static final int ATTEMPTS = 6; // Arbitrary max incorrect guesses + + // This can be expanded at any time + private final String[] words = { + "dům", "pes", "les", "hra", "vlk", "kůň", "vůz", "most", "pán", "noc", "den", "čas", "hora", + "sýr", "chleba", "pole", "věk", "slon", "osel", "šnek", "pták", "koza", "míč", "lípa", "knedlík", + "čaj", "víno", "hráč", "strom", "nebe", "město", "stůl", "kniha", "kráva", "cesta", "divadlo", + "okno", "hory", "slunce", "přítel", "rodina", "jahoda", "židle", "mravenec", "mléko", "sukně", + "koberec", "tabule", "škola", "letadlo", "jablko", "písmeno", "košile", "ulice", "dopis", + "zpráva", "kolega", "kamarád", "postel", "zmrzlina", "autobus", "tramvaj", "pohádka", "vánočka", + "učitelka", "kancelář", "čokoláda", "letadlo", "zahrada", "studentka", "knihtisk", "televizor", + "polévka", "zázračný", "papírnictví", "šampaňské", "kinematografie", "obloha", "křišťálový" + }; + + // Memory + @AllArgsConstructor + @NoArgsConstructor + @Getter + @Setter + private static class HangmanMemory { + private String word; + private Set correctGuessesSet; + private Set incorrectGuessesSet; + private String currentProgress; + } + private static HangmanMemory readMemory(String memory) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.readValue(memory, HangmanMemory.class); + } catch (Exception e) { + System.out.println(e); + throw new RuntimeException("Somebody just fucked up db."); // If this happens idk what is wrong with my life + } + } + private static String writeMemory(HangmanMemory memory) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.writeValueAsString(memory); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); // Well hopefully this will never happen + } + } + + // Response + @AllArgsConstructor + @Getter + @Setter + private static class HangmanResponse { + public enum Type { + GAME_CREATED, + GAME_WON, + GAME_LOST, + GAME_CONTINUE, + } + + private Type type; + + private Set correctGuessesSet; + private Set incorrectGuessesSet; + private String currentProgress; + } @Override public InteractionResponse play(Player player, Character character, String data) { - return null; + if (character.getInteractionData().getPlayer() == null) { + // New game + try { + return initGame(player, character); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + // Game already exists + if (!character.getInteractionData().getPlayer().getId().equals(player.getId())) { + // Another player tries to interact with character while somebody else is interacting with him + return new InteractionResponse(false, "already_playing", new ArrayList<>()); + } + + try { + return guessChar(player, character, data); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); // I hate error handling. This code is just shit but whatever + } + } + + private InteractionResponse initGame(Player player, Character character) throws JsonProcessingException { + var interactionData = character.getInteractionData(); + interactionData.setPlayer(player); + + Random random = new Random(); + int index = random.nextInt(words.length); + String randomWord = words[index]; + + String initialProgress = randomWord.replaceAll(".", "_"); + + interactionData.setMemory(writeMemory( + new HangmanMemory(randomWord, new HashSet<>(), new HashSet<>(), initialProgress) + )); + + characterRepository.save(character); + + var response = new HangmanResponse(HangmanResponse.Type.GAME_CREATED, new HashSet<>(), new HashSet<>(), initialProgress); + ObjectMapper objectMapper = new ObjectMapper(); + + return new InteractionResponse(true, objectMapper.writeValueAsString(response), new ArrayList<>()); + } + + private InteractionResponse guessChar(Player player, Character character, String data) throws JsonProcessingException { + var interactionMemory = readMemory(character.getInteractionData().getMemory()); + + if (data.equals("get_data")) { + var response = new HangmanResponse(HangmanResponse.Type.GAME_CONTINUE, interactionMemory.getCorrectGuessesSet(), interactionMemory.getIncorrectGuessesSet(), interactionMemory.getCurrentProgress()); + ObjectMapper objectMapper = new ObjectMapper(); + var responseText = objectMapper.writeValueAsString(response); + + return new InteractionResponse(false, responseText, new ArrayList<>()); + } + + String word = interactionMemory.getWord(); + var correctGuessesSet = interactionMemory.getCorrectGuessesSet(); + var incorrectGuessesSet = interactionMemory.getIncorrectGuessesSet(); + StringBuilder currentProgress = new StringBuilder(interactionMemory.getCurrentProgress()); + + if (data == null || data.isEmpty()) { + return new InteractionResponse(false, "invalid_input", new ArrayList<>()); + } + + char guessedChar = data.charAt(0); + + if (correctGuessesSet.contains(guessedChar) || incorrectGuessesSet.contains(guessedChar)) { + return new InteractionResponse(false, "already_guessed", new ArrayList<>()); + } + + boolean isCorrect = false; + for (int i = 0; i < word.length(); i++) { + if (word.charAt(i) == guessedChar) { + currentProgress.setCharAt(i, guessedChar); + isCorrect = true; + } + } + + if (isCorrect) { + correctGuessesSet.add(guessedChar); + } else { + incorrectGuessesSet.add(guessedChar); + } + + interactionMemory.setCorrectGuessesSet(correctGuessesSet); + interactionMemory.setIncorrectGuessesSet(incorrectGuessesSet); + interactionMemory.setCurrentProgress(currentProgress.toString()); + + character.getInteractionData().setMemory(writeMemory(interactionMemory)); + + characterRepository.save(character); + + + // Submit response + ObjectMapper objectMapper = new ObjectMapper(); + + if (currentProgress.toString().equals(word)) { + 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); + + var response = new HangmanResponse(HangmanResponse.Type.GAME_WON, correctGuessesSet, incorrectGuessesSet, currentProgress.toString()); + var responseText = objectMapper.writeValueAsString(response); + return new InteractionResponse(true, responseText, items); + } else if (incorrectGuessesSet.size() >= ATTEMPTS) { + character.setInteractedWith(true); + characterRepository.save(character); + + var response = new HangmanResponse(HangmanResponse.Type.GAME_LOST, correctGuessesSet, incorrectGuessesSet, currentProgress.toString()); + var responseText = objectMapper.writeValueAsString(response); + return new InteractionResponse(false, responseText, new ArrayList<>()); + } + + var response = new HangmanResponse(HangmanResponse.Type.GAME_CONTINUE, correctGuessesSet, incorrectGuessesSet, currentProgress.toString()); + var responseText = objectMapper.writeValueAsString(response); + + return new InteractionResponse(false, responseText, new ArrayList<>()); } } 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 7967c2e..c55a0a7 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/services/InitGameService.java +++ b/backend/src/main/java/cz/jzitnik/chronos/services/InitGameService.java @@ -43,14 +43,35 @@ public class InitGameService { farmar.setInteraction(Interaction.Farmer); farmar.setInteractionData(new cz.jzitnik.chronos.entities.Interaction( "Tak si zahrajeme kámen nůžky papír", - "Se mnou jsi již hrál kámen nůžky papír. Znovu už ti fragment klíče nedám", + "Se mnou někdo již hrál kámen nůžky papír. Další fragment klíče nemám.", farmar )); stodola_characters.add(farmar); stodola.setCharacters(stodola_characters); + // Shop + var shop = new Room( + "Obchod", + game + ); + var shop_characters = new ArrayList(); + var cashier = new Character( + "Prodavač", + shop, + "Ahoj já jsem prodavač a budeš se mnou hrát šibenici." + ); + cashier.setInteraction(Interaction.Cashier); + cashier.setInteractionData(new cz.jzitnik.chronos.entities.Interaction( + "Tak si zahrajeme šibenici", + "Se mnou někdo již hrál šibenici. Další fragment klíče nemám.", + cashier + )); + shop_characters.add(cashier); + shop.setCharacters(shop_characters); + rooms.add(outside); rooms.add(stodola); + rooms.add(shop); game.setRooms(rooms); diff --git a/frontend/src/main/java/cz/jzitnik/api/ApiService.java b/frontend/src/main/java/cz/jzitnik/api/ApiService.java index ac3f633..33b561c 100644 --- a/frontend/src/main/java/cz/jzitnik/api/ApiService.java +++ b/frontend/src/main/java/cz/jzitnik/api/ApiService.java @@ -83,6 +83,12 @@ public interface ApiService { @Query("characterId") Long characterId ); + @GET("game/characters/interacting") + Call> isInteracting( + @Query("playerKey") String playerKey, + @Query("characterId") Long characterId + ); + @POST("game/characters/interact") Call> interact( @Query("playerKey") String playerKey, 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 319a1d2..86b0ef1 100644 --- a/frontend/src/main/java/cz/jzitnik/api/types/Interaction.java +++ b/frontend/src/main/java/cz/jzitnik/api/types/Interaction.java @@ -1,5 +1,6 @@ package cz.jzitnik.api.types; public enum Interaction { - Farmer + Farmer, + Cashier } diff --git a/frontend/src/main/java/cz/jzitnik/game/interactions/Hangman.java b/frontend/src/main/java/cz/jzitnik/game/interactions/Hangman.java new file mode 100644 index 0000000..629fcda --- /dev/null +++ b/frontend/src/main/java/cz/jzitnik/game/interactions/Hangman.java @@ -0,0 +1,104 @@ +package cz.jzitnik.game.interactions; + +import com.fasterxml.jackson.core.JsonProcessingException; +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.utils.Cli; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.io.Console; +import java.io.IOException; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@AllArgsConstructor +public class Hangman { + private Character character; + private ApiService apiService; + private String playerKey; + + @Getter + private static class HangmanResponse { + public enum Type { + GAME_CREATED, + GAME_WON, + GAME_LOST, + GAME_CONTINUE, + } + + private Type type; + + private Set correctGuessesSet; + private Set incorrectGuessesSet; + private String currentProgress; + } + + public void play() throws IOException { + var response = apiService.isInteracting(playerKey, character.getId()).execute(); + var interacting = response.body().getData().get(); + + if (!interacting) { + init(); + } else { + // Get data where we have left of + 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 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 hangmanData = objectMapper.readValue(data.getResponseText(), HangmanResponse.class); + + switch (hangmanData.getType()) { + case GAME_WON -> { + Cli.type(character, "Vyhrál jsi! Slovo bylo: " + hangmanData.getCurrentProgress()); + Cli.gotItems(data.getItems()); + } + case GAME_LOST -> Cli.type(character, "Prohrál jsi! Slovo jsi celé neuhodl. Fragment klíče ti nedám!"); + case GAME_CONTINUE, GAME_CREATED -> { + System.out.println("Špatně uhodnuté písmena: " + hangmanData.getIncorrectGuessesSet()); + System.out.println("Slovo: " + hangmanData.getCurrentProgress()); + + // I hate my life + var guessingChar = askForChar(Stream.concat(hangmanData.getCorrectGuessesSet().stream(), hangmanData.getIncorrectGuessesSet().stream()).collect(Collectors.toSet())); + + var response = apiService.interact(playerKey, new InteractionRequest(guessingChar, character.getId())).execute(); + + var body = response.body().getData().get(); + + gameContinue(body); + } + } + } + + private String askForChar(Set guessed) { + Console console = System.console(); + + String data = console.readLine("Zadejte jedno písmeno: "); + + if (data.length() != 1) { + Cli.error("Neplatný vstup!"); + return askForChar(guessed); + } + + if (guessed.contains(data.charAt(0))) { + Cli.error("Toto písmeno jste již se snažil uhodnout!"); + return askForChar(guessed); + } + + return data; + } +} 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 1f37e16..6e55c97 100644 --- a/frontend/src/main/java/cz/jzitnik/game/interactions/Interactions.java +++ b/frontend/src/main/java/cz/jzitnik/game/interactions/Interactions.java @@ -20,9 +20,10 @@ public class Interactions { Cli.type(character, interactionData.getStartText()); + // "Object _ =" is just for java compiler to ensure that all interactions are covered. switch (character.getInteraction()) { - case Farmer -> { - var options = new String[] {"Kámen", "Nůžky", "Papír"}; + case Farmer -> { + var options = new String[]{"Kámen", "Nůžky", "Papír"}; var selected = Cli.selectOptionIndex(Arrays.stream(options).toList()); var requestData = new InteractionRequest(String.valueOf(selected), character.getId()); @@ -35,6 +36,11 @@ public class Interactions { if (interactionResponse.isSuccess()) { Cli.gotItems(interactionResponse.getItems()); } + + } + case Cashier -> { + Hangman hangman = new Hangman(character, apiService, playerKey); + hangman.play(); } } }