feat: Baker, Blacksmith and winning game

This commit is contained in:
2025-01-01 12:43:11 +01:00
parent 057aaeb858
commit 5dc191c842
24 changed files with 680 additions and 30 deletions

View File

@ -1,10 +1,12 @@
package cz.jzitnik.chronos.controllers;
import cz.jzitnik.chronos.entities.Game;
import cz.jzitnik.chronos.entities.*;
import cz.jzitnik.chronos.payload.errors.NotFoundError;
import cz.jzitnik.chronos.payload.responses.GameWonResponse;
import cz.jzitnik.chronos.payload.responses.TestGameKeyResponse;
import cz.jzitnik.chronos.payload.responses.UnifiedResponse;
import cz.jzitnik.chronos.repository.GameRepository;
import cz.jzitnik.chronos.repository.ItemRepository;
import cz.jzitnik.chronos.repository.PlayerRepository;
import cz.jzitnik.chronos.services.GameService;
import cz.jzitnik.chronos.utils.anotations.CheckUser;
@ -13,6 +15,8 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/game")
@ -26,6 +30,9 @@ public class GameController {
@Autowired
private PlayerRepository playerRepository;
@Autowired
private ItemRepository itemRepository;
@PostMapping("/new")
public UnifiedResponse<Game, Error> createGame() {
var game = gameService.createGame();
@ -90,4 +97,51 @@ public class GameController {
return ResponseEntity.ok(UnifiedResponse.success(game.winnable()));
}
@PostMapping("/fragments")
@CheckUser
public ResponseEntity<UnifiedResponse<Object, Error>> putKeyFragments(@RequestParam String playerKey) {
var player = playerRepository.findByPlayerKey(playerKey).get();
var game = player.getGame();
if (!player.getCurrentRoom().getName().equals("Outside")) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(UnifiedResponse.failure(new Error("You are not in the correct room!")));
}
for (Item item : player.getInventory()) {
if (item.getItemType().equals(ItemType.KEY_FRAGMENT)) {
item.setOwner(null);
itemRepository.save(item);
game.setKeyFragmentsAmount(game.getKeyFragmentsAmount() + 1);
var message = new Message(player, "", MessageType.KEY_FRAGMENT_HANDED_OVER);
player.getMessages().add(message);
}
}
playerRepository.save(player);
if (game.getKeyFragmentsAmount() >= 4) {
// All key fragments were found
game.setWon(true);
}
gameRepository.save(game);
return ResponseEntity.ok(UnifiedResponse.success(null));
}
@GetMapping("/won")
@CheckUser
public ResponseEntity<UnifiedResponse<GameWonResponse, Error>> gameWon(@RequestParam String playerKey) {
var player = playerRepository.findByPlayerKey(playerKey).get();
var game = player.getGame();
if (!game.isWon()) {
return ResponseEntity.ok(UnifiedResponse.success(new GameWonResponse(false, Optional.empty())));
}
return ResponseEntity.ok(UnifiedResponse.success(new GameWonResponse(true, Optional.of(game.getWonMessage()))));
}
}

View File

@ -39,6 +39,15 @@ public class Game {
@JsonIgnore
private Player adminPlayer;
@JsonIgnore
private int keyFragmentsAmount = 0;
@JsonIgnore
private boolean won = false;
@JsonIgnore
private String wonMessage;
private boolean started = false;
public void addPlayer(Player player) {
@ -71,6 +80,6 @@ public class Game {
var unplayedCharacters = characters.stream().filter(character -> !character.isInteractedWith()).toList().size();
return roomItems + unplayedCharacters + ownedFragments >= 5;
return (roomItems + unplayedCharacters + ownedFragments) >= (4 - keyFragmentsAmount);
}
}

View File

@ -5,7 +5,10 @@ import cz.jzitnik.chronos.payload.errors.ItemNotUsableException;
public enum ItemType {
KEY_FRAGMENT,
LUCK_POTION,
GOLDEN_WATCH;
GOLDEN_WATCH,
COAL,
FLOUR,
WATER;
public void useItem(Player player) throws ItemNotUsableException {
switch (this) {

View File

@ -6,5 +6,6 @@ public enum MessageType {
GOT_ITEM,
LOST_ITEM,
ITEM_USED,
KEY_FRAGMENT_HANDED_OVER,
JOINED,
}

View File

@ -5,5 +5,7 @@ public enum Interaction {
Cashier,
Librarian,
Innkeeper,
Mayor
Mayor,
Blacksmith,
Baker
}

View File

@ -2,10 +2,7 @@ package cz.jzitnik.chronos.interactions;
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.Mayor;
import cz.jzitnik.chronos.interactions.list.RockPaperScissors;
import cz.jzitnik.chronos.interactions.list.TicTacToe;
import cz.jzitnik.chronos.interactions.list.*;
import cz.jzitnik.chronos.interactions.list.wordle.Wordle;
import cz.jzitnik.chronos.payload.responses.InteractionResponse;
import org.springframework.beans.factory.annotation.Autowired;
@ -23,6 +20,10 @@ public class InteractionService {
private TicTacToe ticTacToe;
@Autowired
private Mayor mayor;
@Autowired
private NumberGuessingGame numberGuessingGame;
@Autowired
private Baker baker;
@FunctionalInterface
public interface Function3<T, U, V, W> {
@ -36,6 +37,8 @@ public class InteractionService {
case Librarian -> wordle::play;
case Innkeeper -> ticTacToe::play;
case Mayor -> mayor::play;
case Blacksmith -> numberGuessingGame::play;
case Baker -> baker::play;
};
}
}

View File

@ -0,0 +1,57 @@
package cz.jzitnik.chronos.interactions.list;
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.repository.ItemRepository;
import cz.jzitnik.chronos.services.ItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@Service
public class Baker implements InteractionPlayer {
@Autowired
private ItemService itemService;
@Autowired
private CharacterRepository characterRepository;
@Autowired
private ItemRepository itemRepository;
@Override
public InteractionResponse play(Player player, Character character, String data) {
var playerItems = player.getInventory();
var flours = playerItems.stream().filter(item -> item.getItemType().equals(ItemType.FLOUR)).toList();
var water = playerItems.stream().filter(item -> item.getItemType().equals(ItemType.WATER)).toList();
if (flours.isEmpty() || water.isEmpty()) {
return new InteractionResponse(false, "Nemáš pro mě buď mouku nebo vodu. Dej mi obojí najednou!", new ArrayList<>());
}
var flour = flours.getFirst();
flour.setOwner(null);
itemRepository.save(flour);
playerItems.remove(flour);
var onewater = water.getFirst();
onewater.setOwner(null);
itemRepository.save(onewater);
playerItems.remove(onewater);
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);
return new InteractionResponse(false, "Díky moc za mouku a vodu. Doplnit dialog!!!", items);
}
}

View File

@ -26,16 +26,16 @@ public class Mayor implements InteractionPlayer {
@Override
public InteractionResponse play(Player player, Character character, String data) {
var playerItems = player.getInventory();
var golderWatches = playerItems.stream().filter(item -> item.getItemType().equals(ItemType.GOLDEN_WATCH)).toList();
var goldenWatches = playerItems.stream().filter(item -> item.getItemType().equals(ItemType.GOLDEN_WATCH)).toList();
if (golderWatches.isEmpty()) {
return new InteractionResponse(false, "Golden watch is not present in your inventory!", new ArrayList<>());
if (goldenWatches.isEmpty()) {
return new InteractionResponse(false, "Nemáš zlaté hodinky v inventáři!", new ArrayList<>());
}
var golderWatch = golderWatches.getFirst();
golderWatch.setOwner(null);
itemRepository.save(golderWatch);
playerItems.remove(golderWatch);
var goldenWatch = goldenWatches.getFirst();
goldenWatch.setOwner(null);
itemRepository.save(goldenWatch);
playerItems.remove(goldenWatch);
var item = new Item(ItemType.KEY_FRAGMENT, player);
itemService.addItem(player, item);

View File

@ -0,0 +1,210 @@
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.repository.ItemRepository;
import cz.jzitnik.chronos.repository.PlayerRepository;
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 NumberGuessingGame implements InteractionPlayer {
@Autowired
private CharacterRepository characterRepository;
@Autowired
private ItemService itemService;
@Autowired
private PlayerRepository playerRepository;
@Autowired
private ItemRepository itemRepository;
private static final int RANGE_MIN = 1;
private static final int RANGE_MAX = 100;
private static final int MAX_ATTEMPTS = 7;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
private static class GuessingGameMemory {
private int targetNumber;
private int attemptsLeft;
}
private static GuessingGameMemory readMemory(String memory) {
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.readValue(memory, GuessingGameMemory.class);
} catch (Exception e) {
throw new RuntimeException("Error reading memory from DB.");
}
}
private static String writeMemory(GuessingGameMemory memory) {
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.writeValueAsString(memory);
} catch (JsonProcessingException e) {
throw new RuntimeException("Error writing memory to DB.");
}
}
@AllArgsConstructor
@Getter
@Setter
private static class GuessingGameResponse {
public enum Type {
GAME_CREATED,
GAME_WON,
GAME_LOST,
GUESS_FEEDBACK
}
private Type type;
private String message;
private int attemptsLeft;
}
@Override
public InteractionResponse play(Player player, Character character, String data) {
if (character.getInteractionData().getPlayer() == null) {
// New game initialization
try {
return initGame(player, character);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
// Ongoing game
if (!character.getInteractionData().getPlayer().getId().equals(player.getId())) {
return new InteractionResponse(false, "already_playing", new ArrayList<>());
}
try {
return processGuess(player, character, data);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
private InteractionResponse initGame(Player player, Character character) throws JsonProcessingException {
var coals = player.getInventory().stream().filter(item -> item.getItemType().equals(ItemType.COAL)).toList();
if (coals.isEmpty()) {
return new InteractionResponse(false, "Nemáš uhlí pro mě!", new ArrayList<>());
}
var coal = coals.getFirst();
coal.setOwner(null);
itemRepository.save(coal);
player.getInventory().remove(coal);
playerRepository.save(player);
var interactionData = character.getInteractionData();
interactionData.setPlayer(player);
Random random = new Random();
int targetNumber = random.nextInt(RANGE_MAX - RANGE_MIN + 1) + RANGE_MIN;
interactionData.setMemory(writeMemory(
new GuessingGameMemory(targetNumber, MAX_ATTEMPTS)
));
characterRepository.save(character);
var response = new GuessingGameResponse(
GuessingGameResponse.Type.GAME_CREATED,
String.format("Uhodni číslo mezi %d a %d.", RANGE_MIN, RANGE_MAX),
MAX_ATTEMPTS
);
ObjectMapper objectMapper = new ObjectMapper();
return new InteractionResponse(true, objectMapper.writeValueAsString(response), new ArrayList<>());
}
private InteractionResponse processGuess(Player player, Character character, String data) throws JsonProcessingException {
var memory = readMemory(character.getInteractionData().getMemory());
int guess;
try {
guess = Integer.parseInt(data);
} catch (NumberFormatException e) {
return new InteractionResponse(false, "invalid_input", new ArrayList<>());
}
if (guess < RANGE_MIN || guess > RANGE_MAX) {
return new InteractionResponse(false, "out_of_range", new ArrayList<>());
}
memory.setAttemptsLeft(memory.getAttemptsLeft() - 1);
String feedbackMessage;
boolean isGameWon = false;
if (guess == memory.getTargetNumber()) {
feedbackMessage = "Gratuluji! Uhodl jsi číslo. Tady máš fragment klíče.";
isGameWon = true;
} else if (guess < memory.getTargetNumber()) {
feedbackMessage = "Tvoje číslo je moc nízké. Zkus to znovu.";
} else {
feedbackMessage = "Tvoje číslo je moc vysoké. Zkus to znovu.";
}
if (isGameWon || memory.getAttemptsLeft() <= 0) {
character.setInteractedWith(true);
characterRepository.save(character);
if (isGameWon) {
var item = new Item(ItemType.KEY_FRAGMENT, player);
itemService.addItem(player, item);
var items = new ArrayList<Item>();
items.add(item);
var response = new GuessingGameResponse(
GuessingGameResponse.Type.GAME_WON,
feedbackMessage,
memory.getAttemptsLeft()
);
ObjectMapper objectMapper = new ObjectMapper();
return new InteractionResponse(true, objectMapper.writeValueAsString(response), items);
} else {
var response = new GuessingGameResponse(
GuessingGameResponse.Type.GAME_LOST,
String.format("Prohrál jsi! Správné číslo bylo %d.", memory.getTargetNumber()),
memory.getAttemptsLeft()
);
ObjectMapper objectMapper = new ObjectMapper();
return new InteractionResponse(false, objectMapper.writeValueAsString(response), new ArrayList<>());
}
}
// Update memory and save
character.getInteractionData().setMemory(writeMemory(memory));
characterRepository.save(character);
var response = new GuessingGameResponse(
GuessingGameResponse.Type.GUESS_FEEDBACK,
feedbackMessage,
memory.getAttemptsLeft()
);
ObjectMapper objectMapper = new ObjectMapper();
return new InteractionResponse(false, objectMapper.writeValueAsString(response), new ArrayList<>());
}
}

View File

@ -0,0 +1,15 @@
package cz.jzitnik.chronos.payload.responses;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.Optional;
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class GameWonResponse {
private boolean won;
private Optional<String> msg;
}

View File

@ -137,6 +137,55 @@ public class InitGameService {
// Zlaté hodinky pro starostu
garden.getItems().add(new Item(ItemType.GOLDEN_WATCH, garden));
// Kovárna
var forge = new Room(
"Kovárna",
game
);
var forge_characters = new ArrayList<Character>();
var blacksmith = new Character(
"Kovář",
forge,
"Ahoj já jsem kovář. Dones mi uhlí pro moji pec a pak si spolu můžeme zahrát number guessing game"
);
blacksmith.setInteraction(Interaction.Blacksmith);
blacksmith.setInteractionData(new cz.jzitnik.chronos.entities.Interaction(
"",
"Uhlí už mám. Díky moc za uhlí!",
mayor
));
forge_characters.add(blacksmith);
forge.setCharacters(forge_characters);
// Sklad
var warehouse = new Room(
"Sklad",
game
);
warehouse.getItems().add(new Item(ItemType.COAL, warehouse));
warehouse.getItems().add(new Item(ItemType.WATER, warehouse));
warehouse.getItems().add(new Item(ItemType.FLOUR, warehouse));
// Pekárna
var bakery = new Room(
"Pekárna",
game
);
var bakery_characters = new ArrayList<Character>();
var baker = new Character(
"Pekař",
bakery,
"Ahoj já jsem pekař a potřebuji mouku a láhev vody. Prosím dones mi je a dostaneš fragment klíče"
);
baker.setInteraction(Interaction.Baker);
baker.setInteractionData(new cz.jzitnik.chronos.entities.Interaction(
"",
"Mouku a vodu už mám. Díky",
baker
));
bakery_characters.add(baker);
bakery.setCharacters(bakery_characters);
rooms.add(outside);
rooms.add(stodola);
@ -145,9 +194,14 @@ public class InitGameService {
rooms.add(inn);
rooms.add(mayor_house);
rooms.add(garden);
rooms.add(forge);
rooms.add(warehouse);
rooms.add(bakery);
game.setRooms(rooms);
game.setWonMessage("Gratuluji vyhráli jste!");
// Return a room that all players will spawn in
return outside;
}