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 30559bb..7ff312e 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/interactions/Interaction.java +++ b/backend/src/main/java/cz/jzitnik/chronos/interactions/Interaction.java @@ -3,5 +3,6 @@ package cz.jzitnik.chronos.interactions; public enum Interaction { Farmer, Cashier, - Librarian + Librarian, + Innkeeper, } \ 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 9b5b567..af8f92f 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.TicTacToe; import cz.jzitnik.chronos.interactions.list.wordle.Wordle; import cz.jzitnik.chronos.payload.responses.InteractionResponse; import org.springframework.beans.factory.annotation.Autowired; @@ -17,6 +18,8 @@ public class InteractionService { private Hangman hangman; @Autowired private Wordle wordle; + @Autowired + private TicTacToe ticTacToe; @FunctionalInterface public interface Function3 { @@ -28,6 +31,7 @@ public class InteractionService { case Farmer -> rockPaperScissors::play; case Cashier -> hangman::play; case Librarian -> wordle::play; + case Innkeeper -> ticTacToe::play; }; } } \ No newline at end of file diff --git a/backend/src/main/java/cz/jzitnik/chronos/interactions/list/TicTacToe.java b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/TicTacToe.java new file mode 100644 index 0000000..1ef280e --- /dev/null +++ b/backend/src/main/java/cz/jzitnik/chronos/interactions/list/TicTacToe.java @@ -0,0 +1,213 @@ +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.Random; + +@Service +public class TicTacToe implements InteractionPlayer { + @Autowired + private CharacterRepository characterRepository; + @Autowired + private ItemService itemService; + + @AllArgsConstructor + @NoArgsConstructor + @Getter + @Setter + private static class TicTacToeMemory { + private String[] board; + private boolean playerTurn; + } + + private static TicTacToeMemory readMemory(String memory) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.readValue(memory, TicTacToeMemory.class); + } catch (Exception e) { + throw new RuntimeException("Error reading memory."); + } + } + + private static String writeMemory(TicTacToeMemory memory) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.writeValueAsString(memory); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + @AllArgsConstructor + @Getter + @Setter + private static class TicTacToeResponse { + public enum Type { + GAME_CREATED, + GAME_WON, + GAME_TIED, + GAME_CONTINUE, + } + + private Type type; + private String[] board; + private boolean playerTurn; + } + + @Override + public InteractionResponse play(Player player, Character character, String data) { + if (character.getInteractionData().getPlayer() == null) { + try { + return initGame(player, character); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + if (!character.getInteractionData().getPlayer().getId().equals(player.getId())) { + return new InteractionResponse(false, "already_playing", new ArrayList<>()); + } + + try { + return makeMove(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); + + String[] board = new String[9]; + for (int i = 0; i < 9; i++) { + board[i] = "_"; + } + + interactionData.setMemory(writeMemory(new TicTacToeMemory(board, true))); + + characterRepository.save(character); + + var response = new TicTacToeResponse(TicTacToeResponse.Type.GAME_CREATED, board, true); + ObjectMapper objectMapper = new ObjectMapper(); + + return new InteractionResponse(true, objectMapper.writeValueAsString(response), new ArrayList<>()); + } + + private InteractionResponse makeMove(Player player, Character character, String data) throws JsonProcessingException { + TicTacToeMemory gameMemory = readMemory(character.getInteractionData().getMemory()); + String[] board = gameMemory.getBoard(); + boolean playerTurn = gameMemory.isPlayerTurn(); + + if (data.equals("get_data")) { + var response = new TicTacToeResponse(TicTacToeResponse.Type.GAME_CONTINUE, board, playerTurn); + ObjectMapper objectMapper = new ObjectMapper(); + return new InteractionResponse(false, objectMapper.writeValueAsString(response), new ArrayList<>()); + } + + if (playerTurn) { + int move = Integer.parseInt(data); + if (move < 0 || move > 8 || !board[move].equals("_")) { + return new InteractionResponse(false, "invalid_move", new ArrayList<>()); + } + + board[move] = "O"; + if (checkWin(board, "O")) { + return gameOver(character, TicTacToeResponse.Type.GAME_WON, board); + } else if (isBoardFull(board)) { + return gameOver(character, TicTacToeResponse.Type.GAME_TIED, board); + } else { + gameMemory.setPlayerTurn(false); + character.getInteractionData().setMemory(writeMemory(gameMemory)); + characterRepository.save(character); + return computerMove(player, character, board); + } + } + + return new InteractionResponse(false, "not_your_turn", new ArrayList<>()); + } + + private InteractionResponse computerMove(Player player, Character character, String[] board) throws JsonProcessingException { + Random random = new Random(); + int move; + do { + move = random.nextInt(9); + } while (!board[move].equals("_")); + + board[move] = "X"; + if (checkWin(board, "X")) { + return gameOver(character, TicTacToeResponse.Type.GAME_WON, board); + } else if (isBoardFull(board)) { + return gameOver(character, TicTacToeResponse.Type.GAME_TIED, board); + } + + TicTacToeMemory gameMemory = readMemory(character.getInteractionData().getMemory()); + gameMemory.setPlayerTurn(true); + gameMemory.setBoard(board); + character.getInteractionData().setMemory(writeMemory(gameMemory)); + characterRepository.save(character); + + var response = new TicTacToeResponse(TicTacToeResponse.Type.GAME_CONTINUE, board, true); + ObjectMapper objectMapper = new ObjectMapper(); + return new InteractionResponse(false, objectMapper.writeValueAsString(response), new ArrayList<>()); + } + + private boolean checkWin(String[] board, String player) { + String[][] winPatterns = { + { "0", "1", "2" }, { "3", "4", "5" }, { "6", "7", "8" }, // rows + { "0", "3", "6" }, { "1", "4", "7" }, { "2", "5", "8" }, // columns + { "0", "4", "8" }, { "2", "4", "6" } // diagonals + }; + + for (String[] pattern : winPatterns) { + if (board[Integer.parseInt(pattern[0])].equals(player) && + board[Integer.parseInt(pattern[1])].equals(player) && + board[Integer.parseInt(pattern[2])].equals(player)) { + return true; + } + } + + return false; + } + + private boolean isBoardFull(String[] board) { + for (String cell : board) { + if (cell.equals("_")) { + return false; + } + } + return true; + } + + private InteractionResponse gameOver(Character character, TicTacToeResponse.Type type, String[] board) throws JsonProcessingException { + if (type != TicTacToeResponse.Type.GAME_TIED) { + character.setInteractedWith(true); + characterRepository.save(character); + } + + if (type == TicTacToeResponse.Type.GAME_WON) { + var item = new Item(ItemType.KEY_FRAGMENT, character.getInteractionData().getPlayer()); + itemService.addItem(character.getInteractionData().getPlayer(), item); + } + + var response = new TicTacToeResponse(type, board, false); + ObjectMapper objectMapper = new ObjectMapper(); + return new InteractionResponse(true, objectMapper.writeValueAsString(response), 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 3bf3a74..a79424f 100644 --- a/backend/src/main/java/cz/jzitnik/chronos/services/InitGameService.java +++ b/backend/src/main/java/cz/jzitnik/chronos/services/InitGameService.java @@ -89,11 +89,32 @@ public class InitGameService { library_characters.add(librarian); library.setCharacters(library_characters); + // Hospoda + var inn = new Room( + "Hospoda", + game + ); + var inn_characters = new ArrayList(); + var innkeeper = new Character( + "Hostinský", + inn, + "Ahoj já jsem hostinský a budeš se mnou hrát piškvorky." + ); + innkeeper.setInteraction(Interaction.Innkeeper); + innkeeper.setInteractionData(new cz.jzitnik.chronos.entities.Interaction( + "Tak si zahrajeme piškvorky.", + "Se mnou někdo již hrál piškvorky. Další fragment klíče nemám.", + innkeeper + )); + inn_characters.add(innkeeper); + inn.setCharacters(inn_characters); + rooms.add(outside); rooms.add(stodola); rooms.add(shop); rooms.add(library); + rooms.add(inn); 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 97e1bfe..4c33d28 100644 --- a/frontend/src/main/java/cz/jzitnik/api/types/Interaction.java +++ b/frontend/src/main/java/cz/jzitnik/api/types/Interaction.java @@ -3,5 +3,6 @@ package cz.jzitnik.api.types; public enum Interaction { Farmer, Cashier, - Librarian + Librarian, + Innkeeper } 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 e4ac418..b475ab1 100644 --- a/frontend/src/main/java/cz/jzitnik/game/interactions/Interactions.java +++ b/frontend/src/main/java/cz/jzitnik/game/interactions/Interactions.java @@ -45,6 +45,10 @@ public class Interactions { Wordle wordle = new Wordle(character, apiService, playerKey); wordle.play(); } + case Innkeeper -> { + TicTacToe ticTacToe = new TicTacToe(character, apiService, playerKey); + ticTacToe.play(); + } } } } diff --git a/frontend/src/main/java/cz/jzitnik/game/interactions/TicTacToe.java b/frontend/src/main/java/cz/jzitnik/game/interactions/TicTacToe.java new file mode 100644 index 0000000..8f9921c --- /dev/null +++ b/frontend/src/main/java/cz/jzitnik/game/interactions/TicTacToe.java @@ -0,0 +1,125 @@ +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; + +@AllArgsConstructor +public class TicTacToe { + private Character character; + private ApiService apiService; + private String playerKey; + + @Getter + private static class TicTacToeResponse { + public enum Type { + GAME_CREATED, + GAME_WON, + GAME_TIED, + GAME_CONTINUE, + } + + private Type type; + private String[] board; + private boolean playerTurn; + } + + 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 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 ticTacToeData = objectMapper.readValue(data.getResponseText(), TicTacToeResponse.class); + + switch (ticTacToeData.getType()) { + case GAME_WON -> { + System.out.println(formatBoard(ticTacToeData.getBoard())); + Cli.type(character, "Vyhrál jsi!"); + Cli.gotItems(data.getItems()); + } + case GAME_TIED -> Cli.type(character, "Prohrál jsi! Hra skončila remízou."); + case GAME_CONTINUE, GAME_CREATED -> { + System.out.println("Aktuální stav pole:"); + System.out.println(formatBoard(ticTacToeData.getBoard())); + + if (ticTacToeData.isPlayerTurn()) { + var move = askForMove(ticTacToeData.getBoard()); + + var response = apiService.interact(playerKey, new InteractionRequest(String.valueOf(move), character.getId())).execute(); + var body = response.body().getData().get(); + + gameContinue(body); + } else { + System.out.println("Čekání na tah počítače..."); + // Wait for computer to play its move + var response = apiService.interact(playerKey, new InteractionRequest("get_data", character.getId())).execute(); + var body = response.body().getData().get(); + + gameContinue(body); + } + } + } + } + + private String formatBoard(String[] board) { + StringBuilder boardRepresentation = new StringBuilder(); + for (int i = 0; i < 9; i++) { + boardRepresentation.append(board[i]); + if ((i + 1) % 3 == 0) { + boardRepresentation.append("\n"); + } else { + boardRepresentation.append(" "); + } + } + return boardRepresentation.toString(); + } + + private int askForMove(String[] board) { + Console console = System.console(); + String data = console.readLine("Zadejte číslo pole (1-9): "); + + try { + int move = Integer.parseInt(data); + + if (move < 1 || move > 9) { + Cli.error("Neplatný vstup! Číslo musí být mezi 1 a 9."); + return askForMove(board); + } + + if (!board[move].equals("_")) { + Cli.error("Toto pole již bylo obsazeno!"); + return askForMove(board); + } + + return move - 1; + } catch (NumberFormatException e) { + Cli.error("Neplatný vstup! Zadejte číslo."); + return askForMove(board); + } + } +}