feat: Wordle interaction

This commit is contained in:
jzitnik-dev 2024-12-30 20:41:48 +01:00
parent 7167b4c34c
commit 171f760d4e
Signed by: jzitnik
GPG Key ID: C577A802A6AF4EF3
15 changed files with 453 additions and 8 deletions

View File

@ -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\<username>\AppData\Chronos\config.json`
- MaxOS: `~/Library/Application Support/Chronos/config.json`
- MacOS: `~/Library/Application Support/Chronos/config.json`

View File

@ -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();
}
}

View File

@ -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;

View File

@ -25,6 +25,7 @@ public class Interaction {
@JsonIgnore
private Character character;
@Lob
@JsonIgnore
private String memory;

View File

@ -2,5 +2,6 @@ package cz.jzitnik.chronos.interactions;
public enum Interaction {
Farmer,
Cashier
Cashier,
Librarian
}

View File

@ -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<T, U, V, W> {
@ -25,6 +27,7 @@ public class InteractionService {
return switch (interaction) {
case Farmer -> rockPaperScissors::play;
case Cashier -> hangman::play;
case Librarian -> wordle::play;
};
}
}

View File

@ -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
}
}

View File

@ -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<CharacterFeedback> feedback;
}
// Memory
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
private static class WordleMemory {
private String word;
private List<GuessFeedback> 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<GuessFeedback> 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<GuessFeedback> 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<CharacterFeedback> 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<Item>();
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<CharacterFeedback> generateFeedback(String word, String guess) {
List<CharacterFeedback> 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;
}
}

View File

@ -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;
}

View File

@ -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
}
}

View File

@ -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<Character>();
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);

View File

@ -2,5 +2,6 @@ package cz.jzitnik.api.types;
public enum Interaction {
Farmer,
Cashier
Cashier,
Librarian
}

View File

@ -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;

View File

@ -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();
}
}
}
}

View File

@ -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<GuessFeedback> feedbackHistory;
}
@Getter
private static class GuessFeedback {
private String guess;
private List<CharacterFeedback> 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<GuessFeedback> 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<GuessFeedback> feedbackHistory) {
for (var feedback : feedbackHistory) {
if (feedback.getFeedback().stream().allMatch(f -> f.getFeedbackType() == FeedbackType.CORRECT_POSITION)) {
return feedback.getGuess();
}
}
return "???";
}
}