feat: Hangman interaction

This commit is contained in:
jzitnik-dev 2024-12-30 16:31:40 +01:00
parent a05788c53b
commit 7167b4c34c
Signed by: jzitnik
GPG Key ID: C577A802A6AF4EF3
8 changed files with 374 additions and 11 deletions

View File

@ -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<UnifiedResponse<Boolean, Error>> 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<UnifiedResponse<InteractionResponse, Error>> interact(@RequestParam String playerKey, @RequestBody InteractionRequest interactionRequest) {

View File

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

View File

@ -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<java.lang.Character> correctGuessesSet;
private Set<java.lang.Character> 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<java.lang.Character> correctGuessesSet;
private Set<java.lang.Character> 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<Item>();
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<>());
}
}

View File

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

View File

@ -83,6 +83,12 @@ public interface ApiService {
@Query("characterId") Long characterId
);
@GET("game/characters/interacting")
Call<UnifiedResponse<Boolean, Error>> isInteracting(
@Query("playerKey") String playerKey,
@Query("characterId") Long characterId
);
@POST("game/characters/interact")
Call<UnifiedResponse<InteractionResponse, Error>> interact(
@Query("playerKey") String playerKey,

View File

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

View File

@ -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<java.lang.Character> correctGuessesSet;
private Set<java.lang.Character> 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<java.lang.Character> 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;
}
}

View File

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