feat: TicTacToe interaction

This commit is contained in:
jzitnik-dev 2024-12-30 21:22:13 +01:00
parent 171f760d4e
commit 910808eb6e
Signed by: jzitnik
GPG Key ID: C577A802A6AF4EF3
7 changed files with 371 additions and 2 deletions

View File

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

View File

@ -4,6 +4,7 @@ import cz.jzitnik.chronos.entities.Player;
import cz.jzitnik.chronos.entities.Character; import cz.jzitnik.chronos.entities.Character;
import cz.jzitnik.chronos.interactions.list.Hangman; import cz.jzitnik.chronos.interactions.list.Hangman;
import cz.jzitnik.chronos.interactions.list.RockPaperScissors; 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.interactions.list.wordle.Wordle;
import cz.jzitnik.chronos.payload.responses.InteractionResponse; import cz.jzitnik.chronos.payload.responses.InteractionResponse;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -17,6 +18,8 @@ public class InteractionService {
private Hangman hangman; private Hangman hangman;
@Autowired @Autowired
private Wordle wordle; private Wordle wordle;
@Autowired
private TicTacToe ticTacToe;
@FunctionalInterface @FunctionalInterface
public interface Function3<T, U, V, W> { public interface Function3<T, U, V, W> {
@ -28,6 +31,7 @@ public class InteractionService {
case Farmer -> rockPaperScissors::play; case Farmer -> rockPaperScissors::play;
case Cashier -> hangman::play; case Cashier -> hangman::play;
case Librarian -> wordle::play; case Librarian -> wordle::play;
case Innkeeper -> ticTacToe::play;
}; };
} }
} }

View File

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

View File

@ -89,11 +89,32 @@ public class InitGameService {
library_characters.add(librarian); library_characters.add(librarian);
library.setCharacters(library_characters); library.setCharacters(library_characters);
// Hospoda
var inn = new Room(
"Hospoda",
game
);
var inn_characters = new ArrayList<Character>();
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(outside);
rooms.add(stodola); rooms.add(stodola);
rooms.add(shop); rooms.add(shop);
rooms.add(library); rooms.add(library);
rooms.add(inn);
game.setRooms(rooms); game.setRooms(rooms);

View File

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

View File

@ -45,6 +45,10 @@ public class Interactions {
Wordle wordle = new Wordle(character, apiService, playerKey); Wordle wordle = new Wordle(character, apiService, playerKey);
wordle.play(); wordle.play();
} }
case Innkeeper -> {
TicTacToe ticTacToe = new TicTacToe(character, apiService, playerKey);
ticTacToe.play();
}
} }
} }
} }

View File

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