feat: Wordle interaction

This commit is contained in:
2024-12-30 20:41:48 +01:00
parent 7167b4c34c
commit 171f760d4e
15 changed files with 453 additions and 8 deletions

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