fr.ritaly.dungeonmaster.ai.CreatureManager.java Source code

Java tutorial

Introduction

Here is the source code for fr.ritaly.dungeonmaster.ai.CreatureManager.java

Source

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package fr.ritaly.dungeonmaster.ai;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.Validate;
import org.apache.commons.lang.math.RandomUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import fr.ritaly.dungeonmaster.Direction;
import fr.ritaly.dungeonmaster.Place;
import fr.ritaly.dungeonmaster.Position;
import fr.ritaly.dungeonmaster.Sector;
import fr.ritaly.dungeonmaster.map.Element;

/**
 * An object responsible for managing the positioning of creatures on a given
 * position. Creatures can be different "sizes" that determine the number of
 * sectors occupied.
 *
 * @author <a href="mailto:francois.ritaly@gmail.com">Francois RITALY</a>
 */
public class CreatureManager {

    private final Log log = LogFactory.getLog(this.getClass());

    /**
     * The managed creatures stored as a map. The map content depends on the
     * size of the creatures. If a dragon (whose size is 4) is present, the 4
     * sectors will be occupied by the dragon. For a worm (size of 2), only 2
     * sectors will be occupied per creature.
     */
    private Map<Sector, Creature> creatures;

    /**
     * The element managed by this class.
     */
    private final Element element;

    private final Position position;

    public CreatureManager(Element element) {
        Validate.notNull(element, "The given element is null");

        this.element = element;
        this.position = element.getPosition();
    }

    /**
     * Returns the creatures occupying this position as a set.
     *
     * @return a set of creatures. Never returns null.
     */
    public final Set<Creature> getCreatures() {
        if (creatures == null) {
            return Collections.emptySet();
        }

        // Using a set will remove the double entries (1 creature can occupy
        // several sectors).
        return new HashSet<Creature>(creatures.values());
    }

    /**
     * Returns the creature on the given sector (if any).
     *
     * @param sector
     *            the sector to test. Can't be null.
     * @return the creature occupying the sector or null if there's none.
     */
    public final Creature getCreature(Sector sector) {
        Validate.notNull(sector, "The given sector is null");

        return (creatures != null) ? creatures.get(sector) : null;
    }

    /**
     * Returns the number of creatures present.
     *
     * @return the number of creatures.
     */
    public final int getCreatureCount() {
        return getCreatures().size();
    }

    /**
     * Returns the creatures occupying this position as a map.
     *
     * @return a map of creatures per sector. Never returns null.
     */
    public final Map<Sector, Creature> getCreatureMap() {
        if (creatures == null) {
            return Collections.emptyMap();
        }

        // Defensive recopy
        return Collections.unmodifiableMap(creatures);
    }

    /**
     * Return the unoccupied sectors.
     *
     * @return the set of unoccupied sectors. Never returns null.
     */
    public EnumSet<Sector> getFreeSectors() {
        return (creatures != null) ? EnumSet.complementOf(getOccupiedSectors()) : EnumSet.allOf(Sector.class);
    }

    private EnumSet<Direction> getFreeDirections() {
        if (creatures != null) {
            final boolean ne = !creatures.containsKey(Sector.NORTH_EAST);
            final boolean nw = !creatures.containsKey(Sector.NORTH_WEST);
            final boolean se = !creatures.containsKey(Sector.SOUTH_EAST);
            final boolean sw = !creatures.containsKey(Sector.SOUTH_WEST);

            int count = 0;

            if (ne) {
                count++;
            }
            if (nw) {
                count++;
            }
            if (se) {
                count++;
            }
            if (sw) {
                count++;
            }

            switch (count) {
            case 0:
            case 1:
                // No free direction
                return EnumSet.noneOf(Direction.class);
            case 2:
                // At most one direction free
                if (ne && nw) {
                    return EnumSet.of(Direction.NORTH);
                } else if (se && sw) {
                    return EnumSet.of(Direction.SOUTH);
                } else if (nw && sw) {
                    return EnumSet.of(Direction.WEST);
                } else if (ne && se) {
                    return EnumSet.of(Direction.EAST);
                } else {
                    // No direction free (the sectors are staggered)
                    return EnumSet.noneOf(Direction.class);
                }
            case 3:
                // One for sure, maybe 2
                final List<Direction> directions = new ArrayList<Direction>(2);

                if (ne && nw) {
                    directions.add(Direction.NORTH);
                }
                if (se && sw) {
                    directions.add(Direction.SOUTH);
                }
                if (nw && sw) {
                    directions.add(Direction.WEST);
                }
                if (ne && se) {
                    directions.add(Direction.EAST);
                }

                if (directions.size() == 1) {
                    return EnumSet.of(directions.iterator().next());
                } else {
                    Collections.shuffle(directions);

                    return EnumSet.of(directions.iterator().next());
                }
            case 4:
                // 2 are free
                // Choose a random pair of directions
                if (RandomUtils.nextBoolean()) {
                    return EnumSet.of(Direction.EAST, Direction.WEST);
                } else {
                    return EnumSet.of(Direction.NORTH, Direction.SOUTH);
                }
            default:
                throw new RuntimeException("Unexpected count <" + count + ">");
            }
        }

        // Choose a random pair of directions
        if (RandomUtils.nextBoolean()) {
            return EnumSet.of(Direction.EAST, Direction.WEST);
        } else {
            return EnumSet.of(Direction.NORTH, Direction.SOUTH);
        }
    }

    /**
     * Returns the sectors occupied by creatures.
     *
     * @return a set of sectors. Never returns null.
     */
    public EnumSet<Sector> getOccupiedSectors() {
        return (creatures != null) ? EnumSet.copyOf(creatures.keySet()) : EnumSet.noneOf(Sector.class);
    }

    public final Sector getSector(Creature creature) {
        Validate.notNull(creature, "The given creature is null");
        if (!Creature.Size.ONE.equals(creature.getSize())) {
            throw new IllegalArgumentException("The given creature <" + creature + "> has an invalid size (actual: "
                    + creature.getSize() + ", expected: " + Creature.Size.ONE + ")");
        }

        if (creatures != null) {
            for (Sector sector : creatures.keySet()) {
                if (creatures.get(sector) == creature) {
                    return sector;
                }
            }
        }

        // Creature not found
        return null;
    }

    public final Direction getDirection(Creature creature) {
        Validate.notNull(creature, "The given creature is null");
        if (!Creature.Size.TWO.equals(creature.getSize())) {
            throw new IllegalArgumentException("The given creature <" + creature + "> has an invalid size (actual: "
                    + creature.getSize() + ", expected: " + Creature.Size.TWO + ")");
        }

        if (creatures == null) {
            return null;
        }

        final boolean ne = (creatures.get(Sector.NORTH_EAST) == creature);
        final boolean nw = (creatures.get(Sector.NORTH_WEST) == creature);
        final boolean se = (creatures.get(Sector.SOUTH_EAST) == creature);
        final boolean sw = (creatures.get(Sector.SOUTH_WEST) == creature);

        if (ne && nw) {
            return Direction.NORTH;
        } else if (se && sw) {
            return Direction.SOUTH;
        } else if (ne && se) {
            return Direction.EAST;
        } else if (nw && sw) {
            return Direction.WEST;
        }

        throw new IllegalStateException("Unable to determine direction for creature <" + creature + "> ne ? " + ne
                + ", nw ? " + nw + ", se ? " + se + ", sw ? " + sw + ")");
    }

    /**
     * Indique si l'lment est occup par au moins une crature.
     *
     * @return si l'lment est occup par au moins une crature.
     */
    public boolean hasCreatures() {
        return (creatures != null) && !creatures.isEmpty();
    }

    private final void _addCreature(Creature creature, Sector sector) {
        Validate.notNull(creature, "The given creature is null");
        Validate.notNull(sector, "The given sector is null");

        if (!Creature.Size.ONE.equals(creature.getSize())) {
            throw new IllegalArgumentException("The given creature <" + creature + "> has an invalid size (actual: "
                    + creature.getSize() + ", expected: " + Creature.Size.ONE + ")");
        }

        // L'emplacement doit initialement tre vide
        if ((creatures != null) && (creatures.get(sector) != null)) {
            throw new IllegalArgumentException("The cell " + sector + " of element " + position
                    + " is already occupied by a creature (" + creatures.get(sector) + ")");
        }

        // Il doit y avoir la place d'accueillir la crature
        if (!canHost(creature)) {
            throw new IllegalArgumentException("Unable to install creature " + creature + " on cell " + sector
                    + " of element " + position + " because the remaining room is " + getFreeSectors().size());
        }

        if (creatures == null) {
            creatures = new EnumMap<Sector, Creature>(Sector.class);
        }

        creatures.put(sector, creature);

        if (log.isDebugEnabled()) {
            log.debug(creature + " stepped on " + position + " (" + sector + ")");
        }
    }

    private final void _removeCreature(Creature creature, Sector sector) {
        Validate.notNull(creature, "The given creature is null");
        Validate.notNull(sector, "The given sector is null");

        if (!Creature.Size.ONE.equals(creature.getSize())) {
            throw new IllegalArgumentException("The given creature <" + creature + "> has an invalid size (actual: "
                    + creature.getSize() + ", expected: " + Creature.Size.ONE + ")");
        }
        if (creatures == null) {
            throw new IllegalStateException("There is currently no creature on element " + position);
        }

        final Creature removed = creatures.remove(sector);

        if (removed != creature) {
            throw new IllegalArgumentException(
                    "Removed: " + removed + " / Creature: " + creature + " / Sector: " + sector);
        }

        if (creatures.isEmpty()) {
            creatures = null;
        }

        if (log.isDebugEnabled()) {
            log.debug(creature + " stepped off " + position + " (" + sector + ")");
        }
    }

    private final void _addCreature(Creature creature) {
        Validate.notNull(creature, "The given creature is null");

        if (!Creature.Size.FOUR.equals(creature.getSize())) {
            throw new IllegalArgumentException("The given creature <" + creature + "> has an invalid size (actual: "
                    + creature.getSize() + ", expected: " + Creature.Size.FOUR + ")");
        }

        if (hasCreatures()) {
            throw new IllegalArgumentException("The element " + position
                    + " is already occupied by at least one creature (" + getCreatures() + ")");
        }

        if (creatures == null) {
            creatures = new EnumMap<Sector, Creature>(Sector.class);
        }

        creatures.put(Sector.NORTH_EAST, creature);
        creatures.put(Sector.NORTH_WEST, creature);
        creatures.put(Sector.SOUTH_EAST, creature);
        creatures.put(Sector.SOUTH_WEST, creature);

        if (log.isDebugEnabled()) {
            log.debug(creature + " stepped on " + position + " (4 sectors)");
        }
    }

    public boolean hasCreature(Creature creature) {
        Validate.notNull(creature, "The given creature is null");

        return (creatures != null) && creatures.containsValue(creature);
    }

    private final void _removeCreature(Creature creature) {
        Validate.notNull(creature, "The given creature is null");

        if (!Creature.Size.FOUR.equals(creature.getSize())) {
            throw new IllegalArgumentException("The given creature <" + creature + "> has an invalid size (actual: "
                    + creature.getSize() + ", expected: " + Creature.Size.FOUR + ")");
        }
        if (!hasCreature(creature)) {
            throw new IllegalArgumentException(
                    "The given creature " + creature + " isn't currently on element " + this);
        }
        if (creatures == null) {
            throw new IllegalStateException("There is currently no creature on element " + position);
        }

        final boolean ne = (creatures.remove(Sector.NORTH_EAST) == creature);
        final boolean nw = (creatures.remove(Sector.NORTH_WEST) == creature);
        final boolean se = (creatures.remove(Sector.SOUTH_EAST) == creature);
        final boolean sw = (creatures.remove(Sector.SOUTH_WEST) == creature);

        if (!ne || !nw || !se || !sw) {
            throw new IllegalStateException("Unable to remove creature " + creature + " from all sectors (ne ? "
                    + ne + ", nw ? " + nw + ", se ? " + se + ", sw ? " + sw + ")");
        }

        if (!creatures.isEmpty()) {
            throw new IllegalStateException("The creature map isn't empty: " + creatures);
        }

        this.creatures = null;

        if (log.isDebugEnabled()) {
            log.debug(creature + " stepped off " + position + " (4 sectors)");
        }
    }

    private final void _addCreature(Creature creature, Direction direction) {
        Validate.notNull(creature, "The given creature is null");
        Validate.notNull(direction, "The given direction is null");

        if (!Creature.Size.TWO.equals(creature.getSize())) {
            throw new IllegalArgumentException("The given creature <" + creature + "> has an invalid size (actual: "
                    + creature.getSize() + ", expected: " + Creature.Size.TWO + ")");
        }

        // L'emplacement doit initialement tre vide
        final List<Sector> sectors = Sector.getVisibleSectors(direction);

        for (Sector sector : sectors) {
            if ((creatures != null) && (creatures.get(sector) != null)) {
                throw new IllegalArgumentException("The cell " + sector + " of element " + position
                        + " is already occupied by a creature (" + creatures.get(sector) + ")");
            }
        }

        if (!canHost(creature)) {
            throw new IllegalArgumentException("Unable to install creature " + creature + " on cells " + sectors
                    + " of element " + position + " because the remaining room is " + getFreeSectors().size());
        }

        if (creatures == null) {
            creatures = new EnumMap<Sector, Creature>(Sector.class);
        }

        for (Sector sector : sectors) {
            creatures.put(sector, creature);
        }

        if (log.isDebugEnabled()) {
            log.debug(creature + " stepped on " + position + " (" + direction + ")");
        }
    }

    private final void _removeCreature(Creature creature, Direction direction) {
        Validate.notNull(creature, "The given creature is null");
        Validate.notNull(direction, "The given direction is null");

        if (!Creature.Size.TWO.equals(creature.getSize())) {
            throw new IllegalArgumentException("The given creature <" + creature + "> has an invalid size (actual: "
                    + creature.getSize() + ", expected: " + Creature.Size.TWO + ")");
        }
        if (creatures == null) {
            throw new IllegalStateException("There is currently no creature on element " + position);
        }

        final List<Sector> sectors = Sector.getVisibleSectors(direction);

        for (Sector sector : sectors) {
            if (creatures.get(sector) != creature) {
                throw new IllegalArgumentException("The cell " + sector + " of element " + position
                        + " isn't occupied by creature " + creature + " but by " + creatures.get(sector));
            }
        }

        final boolean c1 = (creatures.remove(sectors.get(0)) == creature);
        final boolean c2 = (creatures.remove(sectors.get(1)) == creature);

        if (!c1 || !c2) {
            throw new IllegalStateException(
                    "Unable to remove creature " + creature + " from sectors (" + sectors + ")");
        }

        if (creatures.isEmpty()) {
            creatures = null;
        }

        if (log.isDebugEnabled()) {
            log.debug(creature + " stepped off " + position + " (" + direction + ")");
        }
    }

    public Creature.Height getTallestCreatureHeight() {
        if (creatures != null) {
            if (creatures.isEmpty()) {
                // Height is undefined
                return null;
            }

            Creature.Height height = null;

            for (Creature creature : creatures.values()) {
                if (height == null) {
                    height = creature.getHeight();
                } else if (creature.getHeight().compareTo(height) > 0) {
                    height = creature.getHeight();
                }
            }

            return height;
        }

        return null;
    }

    public void removeCreature(Creature creature, Place place) {
        // The place can be null
        Validate.notNull(creature, "The given creature is null");

        if (!element.isTraversable(creature)) {
            throw new UnsupportedOperationException("The creature " + creature + " can't step off " + position);
        }

        if (place == null) {
            _removeCreature(creature);
        } else if (place instanceof Sector) {
            _removeCreature(creature, (Sector) place);
        } else if (place instanceof Direction) {
            _removeCreature(creature, (Direction) place);
        } else {
            throw new IllegalArgumentException("Unexpected place <" + place + ">");
        }
    }

    public Place removeCreature(Creature creature) {
        Validate.notNull(creature, "The given creature is null");

        if (!element.isTraversable(creature)) {
            throw new UnsupportedOperationException("The creature " + creature + " can't step off " + position);
        }

        switch (creature.getSize()) {
        case ONE:
            final Sector sector = getSector(creature);

            if (sector == null) {
                throw new IllegalArgumentException(
                        "The given creature " + creature + " isn't on element " + position);
            }

            _removeCreature(creature, sector);

            return sector;
        case TWO:
            final Direction direction = getDirection(creature);

            if (direction == null) {
                throw new IllegalArgumentException(
                        "The given creature " + creature + " isn't on element " + position);
            }

            _removeCreature(creature, direction);

            return direction;
        case FOUR:
            _removeCreature(creature);

            return null;
        default:
            throw new UnsupportedOperationException("Unsupported creature size <" + creature.getSize() + ">");
        }
    }

    public void addCreature(Creature creature, Place place) {
        // The place can be null
        Validate.notNull(creature, "The given creature is null");

        if (!element.isTraversable(creature)) {
            throw new UnsupportedOperationException("The creature " + creature + " can't step on " + position);
        }

        if (place == null) {
            _addCreature(creature);
        } else if (place instanceof Sector) {
            _addCreature(creature, (Sector) place);
        } else if (place instanceof Direction) {
            _addCreature(creature, (Direction) place);
        } else {
            throw new IllegalArgumentException("Unexpected place <" + place + ">");
        }
    }

    public Place addCreature(Creature creature) {
        Validate.notNull(creature, "The given creature is null");

        if (!element.isTraversable(creature)) {
            throw new UnsupportedOperationException("The creature " + creature + " can't step on " + position);
        }

        switch (creature.getSize()) {
        case ONE:
            final List<Sector> sectors = new ArrayList<Sector>(getFreeSectors());

            if (sectors.size() > 1) {
                Collections.shuffle(sectors);
            }

            final Sector sector = sectors.iterator().next();

            addCreature(creature, sector);

            return sector;
        case TWO:
            final List<Direction> directions = new ArrayList<Direction>(getFreeDirections());

            if (directions.size() > 1) {
                Collections.shuffle(directions);
            }

            final Direction direction = directions.iterator().next();

            addCreature(creature, direction);

            return direction;
        case FOUR:
            addCreature(creature, null);

            return null;
        default:
            throw new UnsupportedOperationException("Unsupported creature size <" + creature.getSize() + ">");
        }
    }

    /**
     * Indique si cet {@link Element} peut accueillir la {@link Creature} donne
     * compte tenu de sa taille et de la place restante.
     *
     * @param creature
     *            une {@link Creature}.
     * @return si cet {@link Element} peut accueillir la {@link Creature} donne
     *         compte tenu de sa taille et de la place restante.
     */
    public boolean canHost(Creature creature) {
        Validate.notNull(creature);

        final int room = getFreeSectors().size();
        final int creatureSize = creature.getSize().value();

        if (creatureSize > room) {
            // Plus assez de place pour accueillir la crature
            return false;
        }

        // Dans le cas o la place restante est de 2 et la taille de la crature
        // galement de 2, il faut s'assurer qu'il s'agit de Sector voisines
        // qui permettent rellement d'accueillir la crature !
        if ((room == 2) && (creatureSize == 2)) {
            final Iterator<Sector> iterator = getFreeSectors().iterator();

            final Sector cell1 = iterator.next();
            final Sector cell2 = iterator.next();

            if (!cell1.isNeighbourOf(cell2)) {
                return false;
            }
        }

        return true;
    }
}