com.spankingrpgs.model.GameState.java Source code

Java tutorial

Introduction

Here is the source code for com.spankingrpgs.model.GameState.java

Source

/*
 * CrimsonGlow is an adult computer roleplaying game with spanking content.
 * Copyright (C) 2015 Andrew Russell
 *
 *      This program is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation, either version 3 of the License, or
 *      (at your option) any later version.
 *
 *      This program is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 *
 *      You should have received a copy of the GNU General Public License
 *      along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.spankingrpgs.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.spankingrpgs.model.characters.GameCharacter;
import com.spankingrpgs.model.characters.Gender;
import com.spankingrpgs.model.characters.SpankingRole;
import com.spankingrpgs.model.combat.CombatRange;
import com.spankingrpgs.model.music.MusicMap;
import com.spankingrpgs.model.options.Options;
import com.spankingrpgs.model.skills.Skill;
import com.spankingrpgs.model.items.Equipment;
import com.spankingrpgs.model.items.Item;
import com.spankingrpgs.model.options.ArtificialIntelligenceLevel;
import com.spankingrpgs.model.options.AttritionRate;
import com.spankingrpgs.model.story.Event;
import com.spankingrpgs.model.story.TextResolver;
import com.spankingrpgs.util.CollectionUtils;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import javafx.scene.media.MediaPlayer;

import java.io.IOException;
import java.io.StringWriter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 * Encapsulates the game's state.
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
public class GameState implements TextResolver {

    public static final String PC_NAME = "pc";

    public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM/dd 'at' HH:mm");

    private final static Logger LOG = Logger.getLogger(GameState.class.getCanonicalName());

    private static GameState instance;
    private static GameState cleanInstance;

    private static String gameTitle;
    /**
     * Multivalued map of characters. Note that there should be at most two characters per name: an NPC and a PC.
     * All characters are saved.
     */
    private final Map<String, GameCharacter> characters;
    /**
     * The player character. The player character is saved.
     */
    private GameCharacter playerCharacter;
    /**
     * The party is saved.
     */
    private final Map<CombatRange, List<String>> party;

    /**
     * A set of keywords. Keywords are used to track decisions made by the player, allowing us to write events that
     * change depending on past decisions.
     * These are saved.
     */
    private final Set<String> keywords;
    /**
     * Since items cannot be modified, they are not saved.
     */
    private Map<String, Item> items;

    private Map<String, Equipment> equipment;
    /**
     * The current event (if any) is saved
     */
    private Event currentEvent;

    /**
     * The text displayed as a part of `currentEvent`. This may not be the exact same as the text stored in
     * `currentEvent`. It may be that the currentEvent has changes that would change the rendered text if the event
     * were played again, or it may be that the game engine runs through all automatic choices at once, and displays
     * them as one event. Each entry is a paragraph of text.
     */
    private List<String> currentEventText;
    /**
     * Since the events cannot be modified by the player, they are not saved
     */
    private Map<String, Event> events;

    private int numTimesLost;

    private ArtificialIntelligenceLevel artificialIntelligenceLevel;

    private AttritionRate attritionRate;

    private boolean playerSpankable;

    private Calendar gameTime;

    private Map<String, Skill> skills;

    private int episodeNumber;

    private int dayNumber;

    private Gender spankerGender;
    private Gender spankeeGender;

    private String musicName;

    /**
     * The number of hours that a given activity takes
     */
    private int activityLength;

    private Map<String, GameCharacter> previousVillains;

    private GameState(GameCharacter playerCharacter) {
        this.playerCharacter = playerCharacter;
        characters = new LinkedHashMap<>();
        keywords = new HashSet<>();
        items = new HashMap<>();
        events = new HashMap<>();
        party = new HashMap<>(CombatRange.values().length);
        Arrays.stream(CombatRange.values()).forEach(range -> this.party.put(range, new ArrayList<>(6)));
        attritionRate = AttritionRate.MODERATE;
        artificialIntelligenceLevel = ArtificialIntelligenceLevel.AVERAGE;
        playerSpankable = true;
        numTimesLost = 0;
        skills = new HashMap<>();
        equipment = new HashMap<>();
        episodeNumber = 1;
        dayNumber = 1;
        activityLength = 4;
        gameTime = Calendar.getInstance();
        this.previousVillains = new HashMap<>();
        try {
            gameTime.setTime(DATE_FORMAT.parse("09/01 at 11:00"));
        } catch (ParseException e) {
            String msg = String.format("Encountered an error formatting the date when creating the state: %s", e);
            LOG.log(Level.SEVERE, msg);
            throw new RuntimeException(msg, e);
        }
        if (playerCharacter != null) {
            characters.put(PC_NAME, playerCharacter);
        }
    }

    /**
     * Performs a deep copy of the passed in GameState.
     *
     * @param copy  The state to perform a deep copy of
     */
    private GameState(GameState copy) {
        this.playerCharacter = copy.playerCharacter.copy();
        characters = new LinkedHashMap<>(
                copy.characters.entrySet().stream().filter(entry -> entry.getValue() != null)
                        .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().copy())));
        keywords = new HashSet<>(copy.keywords);
        items = new HashMap<>(copy.items);
        events = new HashMap<>(copy.events);
        party = new HashMap<>(copy.party.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,
                rangeCharacters -> rangeCharacters.getValue().stream().collect(Collectors.toList()))));
        attritionRate = copy.attritionRate;
        artificialIntelligenceLevel = copy.artificialIntelligenceLevel;
        playerSpankable = copy.playerSpankable;
        numTimesLost = copy.numTimesLost;
        skills = new HashMap<>(copy.skills);
        equipment = new HashMap<>(copy.equipment);
        episodeNumber = copy.episodeNumber;
        dayNumber = copy.dayNumber;
        activityLength = copy.activityLength;
        gameTime = copy.gameTime;
        this.previousVillains = copy.previousVillains.entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, entry -> characters.get(entry.getValue().getName())));
        this.spankerGender = copy.spankerGender;
        this.spankeeGender = copy.spankeeGender;
    }

    public static void setGameTitle(String title) {
        gameTitle = title;
    }

    public static String getGameTitle() {
        return gameTitle;
    }

    /**
     * Initializes the game state. Useful when creating unique GameStates for tests
     *
     * @param playerCharacter The player character.
     *
     * @throws IllegalStateException if the state has already been initialized
     */
    public static void create(GameCharacter playerCharacter) {
        cleanInstance = new GameState(playerCharacter);
    }

    /**
     * Resets the game state to a fresh copy.
     */
    public static void clear() {
        if (instance == null) {
            return;
        }
        instance = new GameState(cleanInstance);
    }

    /**
     * Destroys the state.
     */
    public static void destroy() {
        instance = null;
    }

    /**
     *  Get the game state.
     *
     * @return The game state.
     *
     * @throws IllegalStateException If the game state has not yet been initialized.
     */
    @JsonIgnore
    public static GameState getInstance() {
        if (instance == null) {
            if (cleanInstance == null) {
                cleanInstance = new GameState((GameCharacter) null);
            }
            instance = new GameState(cleanInstance);
        }
        return instance;
    }

    @JsonIgnore
    public static GameState getCleanInstance() {
        return cleanInstance;
    }

    /**
     * Adds the specified NPC to the state.
     *
     * @param character  The character to be added.
     *
     * @throws IllegalArgumentException If there is already a nonplayer character with the specified name, or the
     * nonplayer character's name is {@link GameState#PC_NAME}.
     */
    public void addNonPlayerCharacter(GameCharacter character) {
        String normalizedName = character.getName().toLowerCase();
        if (normalizedName.equals(PC_NAME)) {
            String msg = String.format("Tried to add a non-player character with illegal name: %s", PC_NAME);
            LOG.log(Level.SEVERE, msg);
            throw new IllegalArgumentException(msg);
        }
        if (characters.containsKey(normalizedName)) {
            String msg = String.format("A character already exists with name %s: %s", character.getName(),
                    characters.get(character.getName()));
            LOG.log(Level.SEVERE, msg);
            throw new IllegalArgumentException(msg);
        }
        characters.put(normalizedName, character);
    }

    /**
     * Overwrites the character with the same name as the passed in character with this character.
     *
     * @param character  The character to overwrite with
     */
    public void overwriteCharacter(GameCharacter character) {
        characters.put(character.getName().toLowerCase(), character);
        if (character.getName().toLowerCase().equals(PC_NAME)) {
            setPlayerCharacter(character);
        }
    }

    /**
     * Adds the specified characters to the state.
     *
     * @param characters The characters to be added to the state
     *
     * @throws IllegalArgumentException If there is already a character with the same name as one of the characters,
     * or at least two of the characters have the same name, or one of them has name {@link GameState#PC_NAME}.
     */
    @SuppressWarnings("unchecked")
    public void addNonPlayerCharacters(GameCharacter... characters) {
        Arrays.stream(characters).forEach(this::addNonPlayerCharacter);
    }

    /**
     * Returns the desired non player character.
     *
     * @param characterName  The name of the desired character
     *
     * @return The desired non-player character
     *
     * @throws IllegalArgumentException If there is no NPC with the specified name
     */
    @JsonIgnore
    public GameCharacter getNonPlayerCharacter(String characterName) {
        GameCharacter character = characters.get(characterName.toLowerCase());
        if (character == null) {
            String msg = String.format("There is no character with name: %s", characterName);
            LOG.log(Level.SEVERE, msg);
            throw new IllegalArgumentException(msg);
        }
        return character;
    }

    public Gender getSpankerGender() {
        return spankerGender;
    }

    public Gender getSpankeeGender() {
        return spankeeGender;
    }

    @JsonIgnore
    @Override
    public GameCharacter getCharacter(String name) {
        return name.toLowerCase().equals(PC_NAME) ? playerCharacter : getNonPlayerCharacter(name.toLowerCase());
    }

    /**
     * Adds the specified character to the state.
     * Note that if a character with the same system name ({@link GameCharacter#getName()}) already exists, that
     * character will be overwritten.
     *
     * @param newCharacter  The new character to put in the state
     */
    public void setCharacter(GameCharacter newCharacter) {
        characters.put(newCharacter.getName(), newCharacter);
    }

    /**
     * Returns the character with the specified name from the set of enemies that the player fought last, with the
     * gender of the spankee.
     * <p>
     *
     * @param name  The system name of the character we are interested in
     *
     * @return The character of interest
     *
     * @throws IllegalArgumentException if there is no character with name {@code name}.
     */
    @JsonIgnore
    public GameCharacter getVillainSpankee(String name) {
        /*
         * This is very much a quick and dirty hack to get the first episode working correctly. It assumes that
         * all the enemies have the same name, and there is guaranteed to be at least one enemy with the spankee gender,
         * so it doesn't matter which one is picked. We need to get much smarter if we want to have enemies of different
         * types in a situation where the player can spank them, and we need a way of saying that we want the spanker
         * instead of the spankee.
         */
        GameCharacter villain = CollectionUtils.getValue(previousVillains, name.toLowerCase());
        if (getSpankeeGender() != Gender.UNKNOWN) {
            villain.setGender(getSpankeeGender());
        }
        return CollectionUtils.getValue(previousVillains, name.toLowerCase());
    }

    /**
     * Removes all non-player characters from this state
     */
    public void clearNonPlayerCharacters() {
        characters.clear();
        characters.put(PC_NAME.toLowerCase(), playerCharacter);
    }

    /**
     * Sets the specified character as the player character
     *
     * @param playerCharacter  The character to set as the player character
     *
     * @throws IllegalArgumentException if the character's name is not PC_NAME.
     */
    public void setPlayerCharacter(GameCharacter playerCharacter) {
        if (!playerCharacter.getName().equals(PC_NAME)) {
            String msg = String.format("Tried to set an NPC as the player character: %s", playerCharacter);
            LOG.log(Level.SEVERE, msg);
            throw new IllegalArgumentException(msg);
        }
        this.playerCharacter = playerCharacter;
        characters.put(PC_NAME, playerCharacter);
    }

    public GameCharacter getPlayerCharacter() {
        return playerCharacter;
    }

    /**
     * Adds the specified character to the party at the specified starting combat range.
     *
     * @param range  The range at which the character should begin combat
     * @param characterName  The name of the character to add
     */
    public void joinParty(CombatRange range, String characterName) {
        if (inParty(characterName)) {
            leaveParty(characterName);
        }
        party.get(range).add(characterName.toLowerCase());
        LOG.info(String.format("New party: %s", party));
    }

    public void leaveParty(String characterName) {
        party.keySet().forEach(range -> party.get(range).remove(characterName));
    }

    /**
     * Determines if the specified character is in the active party.
     *
     * @param characterName  The name of the character who may or may not be in the active party
     *
     * @return  True if the specified character is in the party, false otherwise
     */
    public boolean inParty(String characterName) {
        return party.values().stream().flatMap(List::stream).anyMatch(characterName.toLowerCase()::equals);
    }

    /**
     * Returns an unmodifiable version of the active party
     *
     * @return An unmodifiable copy of the active party
     */
    @JsonIgnore
    public Map<CombatRange, List<GameCharacter>> getParty() {
        return party.entrySet().stream()
                .collect(Collectors.toMap(Map.Entry::getKey, rangeCharacters -> rangeCharacters.getValue().stream()
                        .map(this::getCharacter).collect(Collectors.toList())));
    }

    /**
     * Returns a mapping from combat range to a list of character names, which is then serialized by Jackson to save the
     * current party.
     *
     * @return A mapping from CombatRange to a list of character names in the party at that range
     */
    @JsonProperty("party")
    public Map<CombatRange, List<String>> getSerializedParty() {
        return Collections.unmodifiableMap(getParty().entrySet().stream().collect(Collectors.toMap(
                Map.Entry::getKey,
                entry -> entry.getValue().stream().map(GameCharacter::getName).collect(Collectors.toList()))));
    }

    public void setParty(Map<CombatRange, List<GameCharacter>> party) {
        this.party.values().forEach(List::clear);
        party.keySet()
                .forEach(range -> party.get(range).forEach(character -> joinParty(range, character.getName())));
    }

    public void addKeyword(String keyword) {
        keywords.add(keyword.toLowerCase());
    }

    public void removeKeyword(String keyword) {
        keywords.remove(keyword.toLowerCase());
    }

    public boolean hasKeyword(String keyword) {
        return keywords.contains(keyword.toLowerCase());
    }

    public void addItem(String name, Item item) {
        if (item instanceof Equipment) {
            equipment.put(name.toLowerCase(), (Equipment) item);
        }
        items.put(name.toLowerCase(), item);
    }

    public void removeItem(String name) {
        items.remove(name.toLowerCase());
    }

    @JsonIgnore
    public Item getItem(String name) {
        return CollectionUtils.getValue(items, name);
    }

    @JsonIgnore
    public Equipment getEquipment(String name) {
        return CollectionUtils.getValue(equipment, name.toLowerCase());
    }

    public void addSkill(String name, Skill skill) {
        skills.put(name.toLowerCase(), skill);
    }

    @JsonIgnore
    public Skill getSkill(String name) {
        return CollectionUtils.getValue(skills, name.toLowerCase());
    }

    @JsonIgnore
    public Map<String, Skill> getSkills() {
        return skills;
    }

    @JsonIgnore
    public Event getCurrentEvent() {
        return currentEvent;
    }

    /**
     * Returns the name of the current event.
     * Used by the Jackson deserializer.
     *
     * @return  The name of the current event
     */
    public String getCurrentEventName() {
        return events.entrySet().stream().filter(entry -> entry.getValue().equals(getCurrentEvent()))
                .map(Map.Entry::getKey).findFirst().get();
    }

    public void setCurrentEvent(Event currentEvent) {
        this.currentEvent = currentEvent;
    }

    public void setCurrentEventText(List<String> eventText) {
        this.currentEventText = eventText;
    }

    public List<String> getCurrentEventText() {
        return currentEventText == null ? Collections.singletonList(currentEvent.play(this)) : currentEventText;
    }

    /**
     * Set the current event to the event specified by the passed in name.
     * Used by the Jackson deserializer
     *
     * @param eventName  The name of the event to set as the current event
     */
    public void setCurrentEvent(String eventName) {
        currentEvent = events.get(eventName);
    }

    public void addEvent(String name, Event event) {
        events.put(name.toLowerCase(), event);
    }

    @JsonIgnore
    public Event getEvent(String name) {
        return events.get(name.toLowerCase());
    }

    /**
     * Returns an unmodifiable version of the game's characters.
     *
     * @return  An unmodifiable version of the mapping of characters
     */
    @JsonIgnore
    public Map<String, GameCharacter> getCharacters() {
        return Collections.unmodifiableMap(characters);
    }

    public Map<String, Gender> getCharacterGender() {
        return characters.entrySet().stream()
                .filter(nameCharacter -> nameCharacter.getValue() != getPlayerCharacter()).collect(
                        Collectors.toMap(Map.Entry::getKey, nameCharacter -> nameCharacter.getValue().getGender()));
    }

    public void loadCharacterGender(JsonNode node) {
        Iterator<String> characterNamesGenders = node.fieldNames();
        while (characterNamesGenders.hasNext()) {
            String name = characterNamesGenders.next();
            getCharacter(name).setGender(Gender.valueOf(node.get(name).asText()));
        }
    }

    /**
     * Returns an unmodifiable collection of the keywords accumulated by the player.
     *
     * @return  An unmodifiable collection of the keywords accumulated by the player
     */
    public Set<String> getKeywords() {
        return Collections.unmodifiableSet(keywords);
    }

    public int getNumTimesLost() {
        return numTimesLost;
    }

    public int getEpisodeNumber() {
        return episodeNumber;
    }

    public int getDayNumber() {
        return dayNumber;
    }

    public int getActivityLength() {
        return activityLength;
    }

    public void setActivityLength(int activityLength) {
        this.activityLength = activityLength;
    }

    public Map<String, GameCharacter> getPreviousVillains() {
        return previousVillains;
    }

    public void setPreviousVillains(Map<String, GameCharacter> previousVillains) {
        this.previousVillains = previousVillains;
    }

    /**
     * Returns a JSON string containing the data that needs to be saved across play sessions.
     *
     * @return  A JSON string containing all the data that needs to be saved across play sessions.
     */
    public String save() {
        StringWriter writer = new StringWriter();
        try {
            new ObjectMapper().writeValue(writer, this);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return writer.toString();
    }

    /**
     * Loads the data encoded in the passed in JSON into this state.
     *
     * @param stateToLoad The JSON string to load into this state
     */
    @SuppressWarnings("unchecked")
    public void load(String stateToLoad) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        JsonNode rootNode;
        try {
            rootNode = mapper.readValue(stateToLoad, JsonNode.class);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        loadPlayerCharacter(rootNode.get("playerCharacter"));
        loadKeywords(rootNode.get("keywords"));
        setNumTimesLost(rootNode.get("numTimesLost").asInt());
        setEpisodeNumber(rootNode.get("episodeNumber").asInt());
        setDayNumber(rootNode.get("dayNumber").asInt());
        setActivityLength(rootNode.get("activityLength").asInt());
        setAttritionRate(AttritionRate.valueOf(rootNode.get("attritionRate").asText()));
        setArtificialIntelligenceLevel(
                ArtificialIntelligenceLevel.valueOf(rootNode.get("artificialIntelligenceLevel").asText()));
        setPlayerSpankable(rootNode.get("playerSpankable").asBoolean());
        setSpankerGender(Gender.valueOf(rootNode.get("spankerGender").asText()));
        setSpankeeGender(Gender.valueOf(rootNode.get("spankeeGender").asText()));
        setMusic(rootNode.get("music") == null ? null : rootNode.get("music").asText());

        Options.setGenderForRole(SpankingRole.SPANKER, getSpankerGender());
        Options.setGenderForRole(SpankingRole.SPANKEE, getSpankeeGender());
        loadPreviousVillains(rootNode.get("previousVillains"));
        loadCurrentEvent(rootNode.get("currentEventName"));
        loadParty(rootNode.get("party"));
        loadCharacterGender(rootNode.get("characterGender"));
        try {
            setCurrentEventText((List<String>) mapper.treeToValue(rootNode.get("currentEventText"), List.class));
            setGameTime(mapper.treeToValue(rootNode.get("gameTime"), Calendar.class));
        } catch (JsonProcessingException e) {
            String msg = String.format("Experienced an error while loading node %s: %s",
                    rootNode.get("gameTime").toString(), e);
            LOG.log(Level.SEVERE, msg);
            throw new RuntimeException(msg, e);
        }
    }

    private void loadParty(JsonNode party) {
        Iterator<String> combatRanges = party.fieldNames();
        Map<CombatRange, List<GameCharacter>> partyMap = new LinkedHashMap<>();
        while (combatRanges.hasNext()) {
            CombatRange range = CombatRange.valueOf(combatRanges.next());
            List<GameCharacter> charactersAtRange = new LinkedList<>();
            JsonNode rangeList = party.get(range.name());
            for (int i = 0; i < rangeList.size(); i++) {
                charactersAtRange.add(getCharacter(rangeList.get(i).asText()));
            }
            partyMap.put(range, charactersAtRange);
        }
        setParty(partyMap);
    }

    private void loadPreviousVillains(JsonNode previousVillainsNode) {
        Iterator<String> villainNames = previousVillainsNode.fieldNames();
        while (villainNames.hasNext()) {
            String villainName = villainNames.next();
            previousVillains.put(villainName, characters.get(villainName).copy());
            previousVillains.get(villainName).load(previousVillainsNode.get(villainName));
        }
    }

    private void loadPlayerCharacter(JsonNode playerCharacterNode) {
        CollectionUtils.getValue(characters, PC_NAME).load(playerCharacterNode);
        setPlayerCharacter(characters.get(PC_NAME));
    }

    private void loadKeywords(JsonNode keywords) {
        Iterator<JsonNode> keywordsIterator = keywords.elements();
        this.keywords.clear();
        while (keywordsIterator.hasNext()) {
            this.keywords.add(keywordsIterator.next().asText().trim());
        }
    }

    private void loadCurrentEvent(JsonNode currentEventName) {
        this.currentEvent = events.get(currentEventName.asText());
    }

    public void setDayNumber(int dayNumber) {
        this.dayNumber = dayNumber;
    }

    public void setEpisodeNumber(int episodeNumber) {
        this.episodeNumber = episodeNumber;
    }

    public void setGameTime(Calendar gameTime) {
        this.gameTime = gameTime;
    }

    public Calendar getGameTime() {
        return gameTime;
    }

    public void addHours(int numHours) {
        gameTime.add(Calendar.HOUR_OF_DAY, numHours);
    }

    public void addMinutes(int numMinutes) {
        gameTime.add(Calendar.MINUTE, numMinutes);
    }

    public void setNumTimesLost(int numTimesLost) {
        this.numTimesLost = numTimesLost;
    }

    public void setSpankerGender(Gender gender) {
        this.spankerGender = gender;
    }

    public void setSpankeeGender(Gender gender) {
        this.spankeeGender = gender;
    }

    /**
     * Increments the number of times lost by 1.
     */
    public void incrementNumTimesLost() {
        numTimesLost++;
    }

    public AttritionRate getAttritionRate() {
        return attritionRate;
    }

    public void setAttritionRate(AttritionRate attritionRate) {
        this.attritionRate = attritionRate;
    }

    public ArtificialIntelligenceLevel getArtificialIntelligenceLevel() {
        return artificialIntelligenceLevel;
    }

    public void setArtificialIntelligenceLevel(ArtificialIntelligenceLevel artificialIntelligenceLevel) {
        this.artificialIntelligenceLevel = artificialIntelligenceLevel;
    }

    public boolean isPlayerSpankable() {
        return playerSpankable;
    }

    public void setPlayerSpankable(boolean playerSpankable) {
        this.playerSpankable = playerSpankable;
    }

    public void addPlayerCharacter(GameCharacter playerCharacter) {
        this.playerCharacter = playerCharacter;
        characters.put(PC_NAME, playerCharacter);
    }

    /**
     * Given a list of characters, returns a new list with all the player defined characters filtered out.
     *
     * @param gameCharacters  The characters to be filtered
     * @return  A new list of characters with all player characters filtered out
     */
    private List<GameCharacter> filterPlayerCharacters(List<GameCharacter> gameCharacters) {
        return gameCharacters.stream().filter(character -> !character.equals(playerCharacter))
                .collect(Collectors.toList());
    }

    /**
     * Stop playing the current music.
     * If there is no music playing, this method does nothing
     */
    public void stopMusic() {
        if (MusicMap.mediaPlayer != null) {
            MusicMap.mediaPlayer.stop();
        }
    }

    /**
     * Start playing the music with the specified name.
     * If the music to start playing is the same as the currently played music, this method does nothing.
     *
     * @param music  The name of the music to play
     */
    public void startMusic(String music) {
        startMusic(music, false);
    }

    /**
     * Start playing the music with the specified name.
     *
     * @param music  The name of the music to play
     * @param restart  If true, the music specified will be played from the beginning even if it is already playing,
     * if false then the music will not be restarted if already playing
     */
    public void startMusic(String music, boolean restart) {
        if (musicName != null && !restart && musicName.equals(music)) {
            return;
        }
        stopMusic();
        musicName = music;
        MusicMap musicMap = MusicMap.INSTANCE;
        if (musicMap.get(musicName) == null) {
            LOG.warning(String.format("No music for %s found. MusicMap: %s", music, musicMap));
            return;
        }
        MusicMap.mediaPlayer = new MediaPlayer(MusicMap.INSTANCE.get(music));
        MusicMap.mediaPlayer.setCycleCount(MediaPlayer.INDEFINITE);
        MusicMap.mediaPlayer.play();
    }

    public String getMusic() {
        return musicName;
    }

    public void setMusic(String musicName) {
        this.musicName = musicName;
    }
}