diff --git a/README.md b/README.md index d748433..e12151d 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,4 @@ Frontend je napsaný ve Vanilla Javě. Config soubor se automaticky ukládá v n - Unix-like operační systémy: `~/.config/Chronos/config.json` - Windows: `C:\Users\\AppData\Chronos\config.json` -- MaxOS: `~/Library/Application Support/Chronos/config.json` +- MacOS: `~/Library/Application Support/Chronos/config.json` diff --git a/backend/src/main/java/cz/jzitnik/chronos/config/AppConfig.java b/backend/src/main/java/cz/jzitnik/chronos/config/AppConfig.java new file mode 100644 index 0000000..f4dfd13 --- /dev/null +++ b/backend/src/main/java/cz/jzitnik/chronos/config/AppConfig.java @@ -0,0 +1,13 @@ +package cz.jzitnik.chronos.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/backend/src/main/java/cz/jzitnik/chronos/controllers/PlayerController.java b/backend/src/main/java/cz/jzitnik/chronos/controllers/PlayerController.java index 49846af..a31a10e 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/controllers/PlayerController.java +++ b/backend/src/main/java/cz/jzitnik/chronos/controllers/PlayerController.java @@ -1,6 +1,5 @@ package cz.jzitnik.chronos.controllers; -import com.fasterxml.jackson.databind.ObjectMapper; import cz.jzitnik.chronos.entities.Item; import cz.jzitnik.chronos.entities.Message; import cz.jzitnik.chronos.entities.MessageType; 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 38c55ae..9b7a723 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/entities/Interaction.java +++ b/backend/src/main/java/cz/jzitnik/chronos/entities/Interaction.java @@ -25,6 +25,7 @@ public class Interaction { @JsonIgnore private Character character; + @Lob @JsonIgnore private String memory; 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 d6d3557..30559bb 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/interactions/Interaction.java +++ b/backend/src/main/java/cz/jzitnik/chronos/interactions/Interaction.java @@ -2,5 +2,6 @@ package cz.jzitnik.chronos.interactions; public enum Interaction { Farmer, - Cashier + Cashier, + Librarian } \ 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 ec65d85..9b5b567 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/interactions/InteractionService.java +++ b/backend/src/main/java/cz/jzitnik/chronos/interactions/InteractionService.java @@ -4,6 +4,7 @@ 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.RockPaperScissors; +import cz.jzitnik.chronos.interactions.list.wordle.Wordle; import cz.jzitnik.chronos.payload.responses.InteractionResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -12,9 +13,10 @@ import org.springframework.stereotype.Service; public class InteractionService { @Autowired private RockPaperScissors rockPaperScissors; - @Autowired private Hangman hangman; + @Autowired + private Wordle wordle; @FunctionalInterface public interface Function3 { @@ -25,6 +27,7 @@ public class InteractionService { return switch (interaction) { case Farmer -> rockPaperScissors::play; case Cashier -> hangman::play; + case Librarian -> wordle::play; }; } } \ No newline at end of file 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 bd1844e..40b1c6f 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 @@ -59,7 +59,6 @@ public class Hangman implements InteractionPlayer { 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 } } diff --git a/backend/src/main/java/cz/jzitnik/chronos/interactions/list/wordle/Wordle.java b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/wordle/Wordle.java new file mode 100644 index 0000000..1861b4b --- /dev/null +++ b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/wordle/Wordle.java @@ -0,0 +1,234 @@ +package cz.jzitnik.chronos.interactions.list.wordle; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import cz.jzitnik.chronos.entities.Item; +import cz.jzitnik.chronos.entities.ItemType; +import cz.jzitnik.chronos.entities.Player; +import cz.jzitnik.chronos.entities.Character; +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.List; + +@Service +public class Wordle implements InteractionPlayer { + @Autowired + private CharacterRepository characterRepository; + @Autowired + private ItemService itemService; + @Autowired + private WordleService wordleService; + + private static final int ATTEMPTS = 6; // Max attempts for Wordle + + public enum FeedbackType { + NOT_IN_WORD, + WRONG_POSITION, + CORRECT_POSITION + } + + @AllArgsConstructor + @NoArgsConstructor + @Getter + public static class CharacterFeedback { + private char character; + private FeedbackType feedbackType; + } + + @AllArgsConstructor + @NoArgsConstructor + @Getter + public static class GuessFeedback { + private String guess; + private List feedback; + } + + // Memory + @AllArgsConstructor + @NoArgsConstructor + @Getter + @Setter + private static class WordleMemory { + private String word; + private List feedbackHistory; + private int attemptsRemaining; + } + + private static WordleMemory readMemory(String memory) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.readValue(memory, WordleMemory.class); + } catch (Exception e) { + System.out.println(e); + throw new RuntimeException("Error reading memory from DB."); + } + } + + private static String writeMemory(WordleMemory memory) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.writeValueAsString(memory); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + // Response + @AllArgsConstructor + @Getter + @Setter + private static class WordleResponse { + public enum Type { + GAME_CREATED, + GAME_WON, + GAME_LOST, + GAME_CONTINUE, + } + + private Type type; + private List feedbackHistory; + private int attemptsRemaining; + } + + @Override + public InteractionResponse play(Player player, Character character, String data) { + 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())) { + return new InteractionResponse(false, "already_playing", new ArrayList<>()); + } + + try { + return makeGuess(player, character, data); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private InteractionResponse initGame(Player player, Character character) throws JsonProcessingException { + var interactionData = character.getInteractionData(); + interactionData.setPlayer(player); + + interactionData.setMemory(writeMemory(new WordleMemory(wordleService.fetchWordleSolution(), new ArrayList<>(), ATTEMPTS))); + + characterRepository.save(character); + + var response = new WordleResponse(WordleResponse.Type.GAME_CREATED, new ArrayList<>(), ATTEMPTS); + ObjectMapper objectMapper = new ObjectMapper(); + + return new InteractionResponse(true, objectMapper.writeValueAsString(response), new ArrayList<>()); + } + + private InteractionResponse makeGuess(Player player, Character character, String data) throws JsonProcessingException { + var interactionMemory = readMemory(character.getInteractionData().getMemory()); + + if (data.equals("get_data")) { + var response = new WordleResponse(WordleResponse.Type.GAME_CONTINUE, interactionMemory.getFeedbackHistory(), interactionMemory.getAttemptsRemaining()); + ObjectMapper objectMapper = new ObjectMapper(); + return new InteractionResponse(false, objectMapper.writeValueAsString(response), new ArrayList<>()); + } + + String word = interactionMemory.getWord(); + List feedbackHistory = interactionMemory.getFeedbackHistory(); + + if (data.length() != 5) { + return new InteractionResponse(false, "invalid_input", new ArrayList<>()); + } + + if (feedbackHistory.stream().anyMatch(f -> f.getGuess().equals(data))) { + return new InteractionResponse(false, "already_guessed", new ArrayList<>()); + } + + List feedback = generateFeedback(word, data); + feedbackHistory.add(new GuessFeedback(data, feedback)); + interactionMemory.setFeedbackHistory(feedbackHistory); + + interactionMemory.setAttemptsRemaining(interactionMemory.getAttemptsRemaining() - 1); + + if (data.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 WordleResponse(WordleResponse.Type.GAME_WON, feedbackHistory, interactionMemory.getAttemptsRemaining()); + ObjectMapper objectMapper = new ObjectMapper(); + return new InteractionResponse(true, objectMapper.writeValueAsString(response), items); + } + + if (interactionMemory.getAttemptsRemaining() == 0) { + character.setInteractedWith(true); + characterRepository.save(character); + + var response = new WordleResponse(WordleResponse.Type.GAME_LOST, feedbackHistory, interactionMemory.getAttemptsRemaining()); + ObjectMapper objectMapper = new ObjectMapper(); + return new InteractionResponse(false, objectMapper.writeValueAsString(response), new ArrayList<>()); + } + + character.getInteractionData().setMemory(writeMemory(interactionMemory)); + characterRepository.save(character); + + var response = new WordleResponse(WordleResponse.Type.GAME_CONTINUE, feedbackHistory, interactionMemory.getAttemptsRemaining()); + ObjectMapper objectMapper = new ObjectMapper(); + return new InteractionResponse(false, objectMapper.writeValueAsString(response), new ArrayList<>()); + } + + private List generateFeedback(String word, String guess) { + List feedback = new ArrayList<>(); + boolean[] matched = new boolean[word.length()]; + + // First pass: Check for correct positions + for (int i = 0; i < word.length(); i++) { + if (guess.charAt(i) == word.charAt(i)) { + feedback.add(new CharacterFeedback(guess.charAt(i), FeedbackType.CORRECT_POSITION)); + matched[i] = true; + } else { + feedback.add(null); + } + } + + // Second pass: Check for incorrect positions + for (int i = 0; i < word.length(); i++) { + if (feedback.get(i) != null) continue; + + char guessChar = guess.charAt(i); + boolean found = false; + for (int j = 0; j < word.length(); j++) { + if (!matched[j] && word.charAt(j) == guessChar) { + feedback.set(i, new CharacterFeedback(guessChar, FeedbackType.WRONG_POSITION)); + matched[j] = true; + found = true; + break; + } + } + + if (!found) { + feedback.set(i, new CharacterFeedback(guessChar, FeedbackType.NOT_IN_WORD)); + } + } + + return feedback; + } +} \ No newline at end of file diff --git a/backend/src/main/java/cz/jzitnik/chronos/interactions/list/wordle/WordleResponse.java b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/wordle/WordleResponse.java new file mode 100644 index 0000000..091fe9d --- /dev/null +++ b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/wordle/WordleResponse.java @@ -0,0 +1,10 @@ +package cz.jzitnik.chronos.interactions.list.wordle; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class WordleResponse { + private String solution; +} \ No newline at end of file diff --git a/backend/src/main/java/cz/jzitnik/chronos/interactions/list/wordle/WordleService.java b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/wordle/WordleService.java new file mode 100644 index 0000000..5fc3df2 --- /dev/null +++ b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/wordle/WordleService.java @@ -0,0 +1,24 @@ +package cz.jzitnik.chronos.interactions.list.wordle; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDate; + +@Service +public class WordleService { + @Autowired + private RestTemplate restTemplate; + + public String fetchWordleSolution() { + LocalDate today = LocalDate.now(); + String formattedDate = today.toString(); // Format: YYYY-MM-DD + + String url = "https://www.nytimes.com/svc/wordle/v2/" + formattedDate + ".json"; + + WordleResponse response = restTemplate.getForObject(url, WordleResponse.class); + + return response != null ? response.getSolution() : "ahead"; // Idk another word than ahead + } +} 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 c55a0a7..3bf3a74 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/services/InitGameService.java +++ b/backend/src/main/java/cz/jzitnik/chronos/services/InitGameService.java @@ -69,9 +69,31 @@ public class InitGameService { shop_characters.add(cashier); shop.setCharacters(shop_characters); + // Knihovna + var library = new Room( + "Knihovna", + game + ); + var library_characters = new ArrayList(); + var librarian = new Character( + "Knihovník", + library, + "Ahoj já jsem knihovník a budeš se mnou hrát wordle." + ); + librarian.setInteraction(Interaction.Librarian); + librarian.setInteractionData(new cz.jzitnik.chronos.entities.Interaction( + "Tak si zahrajeme wordle.", + "Se mnou někdo již hrál wordle. Další fragment klíče nemám.", + librarian + )); + library_characters.add(librarian); + library.setCharacters(library_characters); + + rooms.add(outside); rooms.add(stodola); rooms.add(shop); + rooms.add(library); game.setRooms(rooms); 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 86b0ef1..97e1bfe 100644 --- a/frontend/src/main/java/cz/jzitnik/api/types/Interaction.java +++ b/frontend/src/main/java/cz/jzitnik/api/types/Interaction.java @@ -2,5 +2,6 @@ package cz.jzitnik.api.types; public enum Interaction { Farmer, - Cashier + Cashier, + Librarian } diff --git a/frontend/src/main/java/cz/jzitnik/game/interactions/Hangman.java b/frontend/src/main/java/cz/jzitnik/game/interactions/Hangman.java index 629fcda..de95771 100644 --- a/frontend/src/main/java/cz/jzitnik/game/interactions/Hangman.java +++ b/frontend/src/main/java/cz/jzitnik/game/interactions/Hangman.java @@ -1,6 +1,5 @@ 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; 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 6e55c97..e4ac418 100644 --- a/frontend/src/main/java/cz/jzitnik/game/interactions/Interactions.java +++ b/frontend/src/main/java/cz/jzitnik/game/interactions/Interactions.java @@ -20,7 +20,6 @@ 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"}; @@ -42,6 +41,10 @@ public class Interactions { Hangman hangman = new Hangman(character, apiService, playerKey); hangman.play(); } + case Librarian -> { + Wordle wordle = new Wordle(character, apiService, playerKey); + wordle.play(); + } } } } diff --git a/frontend/src/main/java/cz/jzitnik/game/interactions/Wordle.java b/frontend/src/main/java/cz/jzitnik/game/interactions/Wordle.java new file mode 100644 index 0000000..df96c27 --- /dev/null +++ b/frontend/src/main/java/cz/jzitnik/game/interactions/Wordle.java @@ -0,0 +1,136 @@ +package cz.jzitnik.game.interactions; + +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.List; +import java.util.stream.Collectors; + +@AllArgsConstructor +public class Wordle { + private Character character; + private ApiService apiService; + private String playerKey; + + @Getter + private static class WordleResponse { + public enum Type { + GAME_CREATED, + GAME_WON, + GAME_LOST, + GAME_CONTINUE, + } + + private Type type; + private int attemptsRemaining; + private List feedbackHistory; + } + + @Getter + private static class GuessFeedback { + private String guess; + private List feedback; + } + + @Getter + private static class CharacterFeedback { + private char character; + private FeedbackType feedbackType; + } + + public enum FeedbackType { + NOT_IN_WORD, + WRONG_POSITION, + CORRECT_POSITION + } + + 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 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 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 wordleData = objectMapper.readValue(data.getResponseText(), WordleResponse.class); + + switch (wordleData.getType()) { + case GAME_WON -> { + Cli.type(character, "Vyhrál jsi! Slovo bylo: " + getCurrentWord(wordleData.getFeedbackHistory())); + Cli.gotItems(data.getItems()); + } + case GAME_LOST -> Cli.type(character, "Prohrál jsi! Bohužel jste neuhodl slovo. Fragment klíče nedostanete."); + case GAME_CONTINUE, GAME_CREATED -> { + System.out.println("Zbývající pokusy: " + wordleData.getAttemptsRemaining()); + displayFeedback(wordleData.getFeedbackHistory()); + + var guess = askForWord(); + var response = apiService.interact(playerKey, new InteractionRequest(guess, character.getId())).execute(); + var body = response.body().getData().get(); + + gameContinue(body); + } + } + } + + private String askForWord() { + Console console = System.console(); + + String word = console.readLine("Zadejte slovo (5 písmen): "); + + if (word.length() != 5) { + Cli.error("Neplatný vstup! Slovo musí mít 5 písmen."); + return askForWord(); + } + + return word.toLowerCase(); + } + + private void displayFeedback(List feedbackHistory) { + for (var feedback : feedbackHistory) { + System.out.print(feedback.getGuess() + " -> "); + + for (var charFeedback : feedback.getFeedback()) { + String color = switch (charFeedback.getFeedbackType()) { + case CORRECT_POSITION -> Cli.Colors.GREEN; + case WRONG_POSITION -> Cli.Colors.YELLOW; + case NOT_IN_WORD -> ""; + }; + + System.out.print(color + charFeedback.getCharacter() + Cli.Colors.RESET); + } + System.out.println(); + } + } + + private String getCurrentWord(List feedbackHistory) { + for (var feedback : feedbackHistory) { + if (feedback.getFeedback().stream().allMatch(f -> f.getFeedbackType() == FeedbackType.CORRECT_POSITION)) { + return feedback.getGuess(); + } + } + return "???"; + } +}