Package net.bytten.metazelda.generators

Source Code of net.bytten.metazelda.generators.DungeonGenerator$KeyLevelRoomMapping

package net.bytten.metazelda.generators;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.Set;

import net.bytten.metazelda.Condition;
import net.bytten.metazelda.Dungeon;
import net.bytten.metazelda.Edge;
import net.bytten.metazelda.IDungeon;
import net.bytten.metazelda.Room;
import net.bytten.metazelda.Symbol;
import net.bytten.metazelda.constraints.IDungeonConstraints;
import net.bytten.metazelda.util.Coords;
import net.bytten.metazelda.util.Direction;
import net.bytten.metazelda.util.GenerationFailureException;
import net.bytten.metazelda.util.ILogger;
import net.bytten.metazelda.util.Pair;
import net.bytten.metazelda.util.RandomUtil;

/**
* The default and reference implementation of an {@link IDungeonGenerator}.
*/
public class DungeonGenerator implements IDungeonGenerator, ILogger {
   
    public static final int MAX_RETRIES = 20;

    protected ILogger logger;
    protected long seed;
    protected Random random;
    protected Dungeon dungeon;
    protected IDungeonConstraints constraints;
   
    protected boolean bossRoomLocked, generateGoal;
   
    /**
     * Creates a DungeonGenerator with a given random seed and places
     * specific constraints on {@link IDungeon}s it generates.
     *
     * @param seed          the random seed to use
     * @param constraints   the constraints to place on generation
     * @see net.bytten.metazelda.constraints.IDungeonConstraints
     */
    public DungeonGenerator(ILogger logger, long seed,
            IDungeonConstraints constraints) {
        this.logger = logger;
        log("Dungeon seed: "+seed);
        this.seed = seed;
        this.random = new Random(seed);
        assert constraints != null;
        this.constraints = constraints;
       
        bossRoomLocked = generateGoal = true;
    }
   
    public DungeonGenerator(long seed, IDungeonConstraints constraints) {
        this(null, seed, constraints);
    }
   
    @Override
    public void log(String msg) {
        if (logger != null) logger.log(msg);
    }
   
    /**
     * Randomly chooses a {@link Room} within the given collection that has at
     * least one adjacent empty space.
     *
     * @param roomCollection    the collection of rooms to choose from
     * @return  the room that was chosen, or null if there are no rooms with
     *          adjacent empty spaces
     */
    protected Room chooseRoomWithFreeEdge(Collection<Room> roomCollection,
            int keyLevel) {
        List<Room> rooms = new ArrayList<Room>(roomCollection);
        Collections.shuffle(rooms, random);
        for (int i = 0; i < rooms.size(); ++i) {
            Room room = rooms.get(i);
            for (Pair<Double,Integer> next:
                    constraints.getAdjacentRooms(room.id, keyLevel)) {
                if (dungeon.get(next.second) == null) {
                    return room;
                }
            }
        }
        return null;
    }
   
    /**
     * Randomly chooses a {@link Direction} in which the given {@link Room} has
     * an adjacent empty space.
     *
     * @param room  the room
     * @return  the Direction of the empty space chosen adjacent to the Room or
     *          null if there are no adjacent empty spaces
     */
    protected int chooseFreeEdge(Room room, int keyLevel) {
        List<Pair<Double,Integer>> neighbors = new ArrayList<Pair<Double,Integer>>(
                constraints.getAdjacentRooms(room.id, keyLevel));
        Collections.shuffle(neighbors, random);
        while (!neighbors.isEmpty()) {
            Pair<Double,Integer> choice = RandomUtil.choice(random, neighbors);
            if (dungeon.get(choice.second) == null)
                return choice.second;
            neighbors.remove(choice);
        }
        assert false;
        throw new GenerationFailureException("Internal error: Room doesn't have a free edge");
    }
   
    /**
     * Maps 'keyLevel' to the set of rooms within that keyLevel.
     * <p>
     * A 'keyLevel' is the count of the number of unique keys are needed for all
     * the locks we've placed. For example, all the rooms in keyLevel 0 are
     * accessible without collecting any keys, while to get to rooms in
     * keyLevel 3, the player must have collected at least 3 keys.
     */
    protected class KeyLevelRoomMapping {
        protected List<List<Room>> map = new ArrayList<List<Room>>(
                constraints.getMaxKeys());
       
        List<Room> getRooms(int keyLevel) {
            while (keyLevel >= map.size()) map.add(null);
            if (map.get(keyLevel) == null)
                map.set(keyLevel, new ArrayList<Room>());
            return map.get(keyLevel);
        }
       
        void addRoom(int keyLevel, Room room) {
            getRooms(keyLevel).add(room);
        }
       
        int keyCount() {
            return map.size();
        }
    }
   
    /**
     * Thrown by several IDungeonGenerator methods that can fail.
     * Should be caught and handled in {@link #generate}.
     */
    protected static class RetryException extends Exception {
        private static final long serialVersionUID = 1L;
    }
   
    protected static class OutOfRoomsException extends Exception {
        private static final long serialVersionUID = 1L;
    }
   
    /**
     * Comparator objects for sorting {@link Room}s in a couple of different
     * ways. These are used to determine in which rooms of a given keyLevel it
     * is best to place the next key.
     *
     * @see #placeKeys
     */
    protected static final Comparator<Room>
    EDGE_COUNT_COMPARATOR = new Comparator<Room>() {
        @Override
        public int compare(Room arg0, Room arg1) {
            return arg0.linkCount() - arg1.linkCount();
        }
    },
    INTENSITY_COMPARATOR = new Comparator<Room>() {
        @Override
        public int compare(Room arg0, Room arg1) {
            return arg0.getIntensity() > arg1.getIntensity() ? -1
                    : arg0.getIntensity() < arg1.getIntensity() ? 1
                            : 0;
        }
    };
   
    /**
     * Sets up the dungeon's entrance room.
     *
     * @param levels    the keyLevel -> room-set mapping to update
     * @see KeyLevelRoomMapping
     */
    protected void initEntranceRoom(KeyLevelRoomMapping levels)
            throws RetryException {
        int id;
        List<Integer> possibleEntries = new ArrayList<Integer>(
                constraints.initialRooms());
        assert possibleEntries.size() > 0;
        id = possibleEntries.get(random.nextInt(possibleEntries.size()));
       
        Room entry = new Room(id, constraints.getCoords(id), null,
                new Symbol(Symbol.START), new Condition());
        dungeon.add(entry);
       
        levels.addRoom(0, entry);
    }
   
    /**
     * Fill the dungeon's space with rooms and doors (some locked).
     * Keys are not inserted at this point.
     *
     * @param levels    the keyLevel -> room-set mapping to update
     * @throws RetryException if it fails
     * @see KeyLevelRoomMapping
     */
    protected void placeRooms(KeyLevelRoomMapping levels, int roomsPerLock)
            throws RetryException, OutOfRoomsException {
       
        // keyLevel: the number of keys required to get to the new room
        int keyLevel = 0;
        Symbol latestKey = null;
        // condition that must hold true for the player to reach the new room
        // (the set of keys they must have).
        Condition cond = new Condition();
       
        int usableKeys = constraints.getMaxKeys();
        if (isBossRoomLocked())
            usableKeys -= 1;
       
        // Loop to place rooms and link them
        while (dungeon.roomCount() < constraints.getMaxRooms()) {
           
            boolean doLock = false;
           
            // Decide whether we need to place a new lock
            // (Don't place the last lock, since that's reserved for the boss)
            if (levels.getRooms(keyLevel).size() >= roomsPerLock &&
                    keyLevel < usableKeys) {
                latestKey = new Symbol(keyLevel++);
                cond = cond.and(latestKey);
                doLock = true;
            }
           
            // Find an existing room with a free edge:
            Room parentRoom = null;
            if (!doLock && random.nextInt(10) > 0)
                parentRoom = chooseRoomWithFreeEdge(levels.getRooms(keyLevel),
                        keyLevel);
            if (parentRoom == null) {
                parentRoom = chooseRoomWithFreeEdge(dungeon.getRooms(),
                        keyLevel);
                doLock = true;
            }
           
            if (parentRoom == null)
                throw new OutOfRoomsException();
           
            // Decide which direction to put the new room in relative to the
            // parent
            int nextId = chooseFreeEdge(parentRoom, keyLevel);
            Set<Coords> coords = constraints.getCoords(nextId);
            Room room = new Room(nextId, coords, parentRoom, null, cond);
           
            // Add the room to the dungeon
            assert dungeon.get(room.id) == null;
            synchronized (dungeon) {
                dungeon.add(room);
                parentRoom.addChild(room);
                dungeon.link(parentRoom, room, doLock ? latestKey : null);
            }
            levels.addRoom(keyLevel, room);
        }
    }
   
    /**
     * Places the BOSS and GOAL rooms within the dungeon, in existing rooms.
     * These rooms are moved into the next keyLevel.
     *
     * @param levels    the keyLevel -> room-set mapping to update
     * @throws RetryException if it fails
     * @see KeyLevelRoomMapping
     */
    protected void placeBossGoalRooms(KeyLevelRoomMapping levels)
            throws RetryException {
        List<Room> possibleGoalRooms = new ArrayList<Room>(dungeon.roomCount());
       
        Symbol goalSym = new Symbol(Symbol.GOAL),
               bossSym = new Symbol(Symbol.BOSS);
       
        for (Room room: dungeon.getRooms()) {
            if (room.getChildren().size() > 0 || room.getItem() != null)
                continue;
            Room parent = room.getParent();
            if (parent == null || parent.getChildren().size() != 1 ||
                    room.getItem() != null ||
                    !parent.getPrecond().implies(room.getPrecond()))
                continue;
            if (isGenerateGoal()) {
                if (!constraints.roomCanFitItem(room.id, goalSym) ||
                        !constraints.roomCanFitItem(parent.id, bossSym))
                    continue;
            } else {
                if (!constraints.roomCanFitItem(room.id, bossSym))
                    continue;
            }
            possibleGoalRooms.add(room);
        }
       
        if (possibleGoalRooms.size() == 0) throw new RetryException();
       
        Room goalRoom = possibleGoalRooms.get(random.nextInt(
                possibleGoalRooms.size())),
             bossRoom = goalRoom.getParent();
       
        if (!isGenerateGoal()) {
            bossRoom = goalRoom;
            goalRoom = null;
        }
       
        if (goalRoom != null) goalRoom.setItem(goalSym);
        bossRoom.setItem(bossSym);
       
        if (isBossRoomLocked()) {
            int oldKeyLevel = bossRoom.getPrecond().getKeyLevel(),
                newKeyLevel = Math.min(levels.keyCount(), constraints.getMaxKeys());
           
            List<Room> oklRooms = levels.getRooms(oldKeyLevel);
            if (goalRoom != null) oklRooms.remove(goalRoom);
            oklRooms.remove(bossRoom);
           
            if (goalRoom != null) levels.addRoom(newKeyLevel, goalRoom);
            levels.addRoom(newKeyLevel, bossRoom);
           
            Symbol bossKey = new Symbol(newKeyLevel-1);
            Condition precond = bossRoom.getPrecond().and(bossKey);
            bossRoom.setPrecond(precond);
            if (goalRoom != null) goalRoom.setPrecond(precond);
           
            if (newKeyLevel == 0) {
                dungeon.link(bossRoom.getParent(), bossRoom);
            } else {
                dungeon.link(bossRoom.getParent(), bossRoom, bossKey);
            }
            if (goalRoom != null) dungeon.link(bossRoom, goalRoom);
        }
    }
   
    /**
     * Removes the given {@link Room} and all its descendants from the given
     * list.
     *
     * @param rooms the list of Rooms to remove nodes from
     * @param room  the Room whose descendants to remove from the list
     */
    protected void removeDescendantsFromList(List<Room> rooms, Room room) {
        rooms.remove(room);
        for (Room child: room.getChildren()) {
            removeDescendantsFromList(rooms, child);
        }
    }
   
    /**
     * Adds extra conditions to the given {@link Room}'s preconditions and all
     * of its descendants.
     *
     * @param room  the Room to add extra preconditions to
     * @param cond  the extra preconditions to add
     */
    protected void addPrecond(Room room, Condition cond) {
        room.setPrecond(room.getPrecond().and(cond));
        for (Room child: room.getChildren()) {
            addPrecond(child, cond);
        }
    }
   
    /**
     * Randomly locks descendant rooms of the given {@link Room} with
     * {@link Edge}s that require the switch to be in the given state.
     * <p>
     * If the given state is EITHER, the required states will be random.
     *
     * @param room          the room whose child to lock
     * @param givenState    the state to require the switch to be in for the
     *                      child rooms to be accessible
     * @return              true if any locks were added, false if none were
     *                      added (which can happen due to the way the random
     *                      decisions are made)
     * @see Condition.SwitchState
     */
    protected boolean switchLockChildRooms(Room room,
            Condition.SwitchState givenState) {
        boolean anyLocks = false;
        Condition.SwitchState state = givenState != Condition.SwitchState.EITHER
                ? givenState
                : (random.nextInt(2) == 0
                    ? Condition.SwitchState.ON
                    : Condition.SwitchState.OFF);
       
        for (Edge edge: room.getEdges()) {
            int neighborId = edge.getTargetRoomId();
            Room nextRoom = dungeon.get(neighborId);
            if (room.getChildren().contains(nextRoom)) {
                if (room.getEdge(neighborId).getSymbol() == null &&
                        random.nextInt(4) != 0) {
                    dungeon.link(room, nextRoom, state.toSymbol());
                    addPrecond(nextRoom, new Condition(state.toSymbol()));
                    anyLocks = true;
                } else {
                    anyLocks |= switchLockChildRooms(nextRoom, state);
                }
               
                if (givenState == Condition.SwitchState.EITHER) {
                    state = state.invert();
                }
            }
        }
        return anyLocks;
    }
   
    /**
     * Returns a path from the goal to the dungeon entrance, along the 'parent'
     * relations.
     *
     * @return  a list of linked {@link Room}s starting with the goal room and
     *          ending with the start room.
     */
    protected List<Room> getSolutionPath() {
        List<Room> solution = new ArrayList<Room>();
        Room room = dungeon.findGoal();
        while (room != null) {
            solution.add(room);
            room = room.getParent();
        }
        return solution;
    }
   
    /**
     * Makes some {@link Edge}s within the dungeon require the dungeon's switch
     * to be in a particular state, and places the switch in a room in the
     * dungeon.
     *
     * @throws RetryException if it fails
     */
    protected void placeSwitches() throws RetryException {
        // Possible TODO: have multiple switches on separate circuits
        // At the moment, we only have one switch per dungeon.
        if (constraints.getMaxSwitches() <= 0) return;
       
        List<Room> solution = getSolutionPath();
       
        for (int attempt = 0; attempt < 10; ++attempt) {
           
            List<Room> rooms = new ArrayList<Room>(dungeon.getRooms());
            Collections.shuffle(rooms, random);
            Collections.shuffle(solution, random);
           
            // Pick a base room from the solution path so that the player
            // will have to encounter a switch-lock to solve the dungeon.
            Room baseRoom = null;
            for (Room room: solution) {
                if (room.getChildren().size() > 1 && room.getParent() != null) {
                    baseRoom = room;
                    break;
                }
            }
            if (baseRoom == null) throw new RetryException();
            Condition baseRoomCond = baseRoom.getPrecond();
           
            removeDescendantsFromList(rooms, baseRoom);
           
            Symbol switchSym = new Symbol(Symbol.SWITCH);
           
            Room switchRoom = null;
            for (Room room: rooms) {
                if (room.getItem() == null &&
                        baseRoomCond.implies(room.getPrecond()) &&
                        constraints.roomCanFitItem(room.id, switchSym)) {
                    switchRoom = room;
                    break;
                }
            }
            if (switchRoom == null) continue;
           
            if (switchLockChildRooms(baseRoom, Condition.SwitchState.EITHER)) {
                switchRoom.setItem(switchSym);
                return;
            }
        }
        throw new RetryException();
    }
   
    /**
     * Randomly links up some adjacent rooms to make the dungeon graph less of
     * a tree.
     *
     * @throws RetryException if it fails
     */
    protected void graphify() throws RetryException {
        for (Room room: dungeon.getRooms()) {
           
            if (room.isGoal() || room.isBoss()) continue;
           
            for (Pair<Double,Integer> next:
                    // Doesn't matter what the keyLevel is; later checks about
                    // preconds ensure linkage doesn't trivialize the puzzle.
                    constraints.getAdjacentRooms(room.id, Integer.MAX_VALUE)) {
                int nextId = next.second;
                if (room.getEdge(nextId) != null) continue;
               
                Room nextRoom = dungeon.get(nextId);
                if (nextRoom == null || nextRoom.isGoal() || nextRoom.isBoss())
                    continue;
               
                boolean forwardImplies = room.getPrecond().implies(nextRoom.getPrecond()),
                        backwardImplies = nextRoom.getPrecond().implies(room.getPrecond());
                if (forwardImplies && backwardImplies) {
                    // both rooms are at the same keyLevel.
                    if (random.nextDouble() >=
                            constraints.edgeGraphifyProbability(room.id, nextRoom.id))
                        continue;
                   
                    dungeon.link(room, nextRoom);
                } else {
                    Symbol difference = room.getPrecond().singleSymbolDifference(
                            nextRoom.getPrecond());
                    if (difference == null || (!difference.isSwitchState() &&
                            random.nextDouble() >=
                                constraints.edgeGraphifyProbability(room.id, nextRoom.id)))
                        continue;
                    dungeon.link(room, nextRoom, difference);
                }
            }
        }
    }
   
    /**
     * Places keys within the dungeon in such a way that the dungeon is
     * guaranteed to be solvable.
     *
     * @param levels    the keyLevel -> room-set mapping to use
     * @throws RetryException if it fails
     * @see KeyLevelRoomMapping
     */
    protected void placeKeys(KeyLevelRoomMapping levels) throws RetryException {
        // Now place the keys. For every key-level but the last one, place a
        // key for the next level in it, preferring rooms with fewest links
        // (dead end rooms).
        for (int key = 0; key < levels.keyCount()-1; ++key) {
            List<Room> rooms = levels.getRooms(key);
           
            Collections.shuffle(rooms, random);
            // Collections.sort is stable: it doesn't reorder "equal" elements,
            // which means the shuffling we just did is still useful.
            Collections.sort(rooms, INTENSITY_COMPARATOR);
            // Alternatively, use the EDGE_COUNT_COMPARATOR to put keys at
            // 'dead end' rooms.
           
            Symbol keySym = new Symbol(key);
           
            boolean placedKey = false;
            for (Room room: rooms) {
                if (room.getItem() == null && constraints.roomCanFitItem(room.id, keySym)) {
                    room.setItem(keySym);
                    placedKey = true;
                    break;
                }
            }
            if (!placedKey)
                // there were no rooms into which the key would fit
                throw new RetryException();
        }
    }
   
    protected static final double
            INTENSITY_GROWTH_JITTER = 0.1,
            INTENSITY_EASE_OFF = 0.2;
   
    /**
     * Recursively applies the given intensity to the given {@link Room}, and
     * higher intensities to each of its descendants that are within the same
     * keyLevel.
     * <p>
     * Intensities set by this method may (will) be outside of the normal range
     * from 0.0 to 1.0. See {@link #normalizeIntensity} to correct this.
     *
     * @param room      the room to set the intensity of
     * @param intensity the value to set intensity to (some randomn variance is
     *                  added)
     * @see Room
     */
    protected double applyIntensity(Room room, double intensity) {
        intensity *= 1.0 - INTENSITY_GROWTH_JITTER/2.0 +
                INTENSITY_GROWTH_JITTER * random.nextDouble();
       
        room.setIntensity(intensity);
       
        double maxIntensity = intensity;
        for (Room child: room.getChildren()) {
            if (room.getPrecond().implies(child.getPrecond())) {
                maxIntensity = Math.max(maxIntensity, applyIntensity(child,
                        intensity + 1.0));
            }
        }
       
        return maxIntensity;
    }

    /**
     * Scales intensities within the dungeon down so that they all fit within
     * the range 0 <= intensity < 1.0.
     *
     * @see Room
     */
    protected void normalizeIntensity() {
        double maxIntensity = 0.0;
        for (Room room: dungeon.getRooms()) {
            maxIntensity = Math.max(maxIntensity, room.getIntensity());
        }
        for (Room room: dungeon.getRooms()) {
            room.setIntensity(room.getIntensity() * 0.99 / maxIntensity);
        }
    }
   
    /**
     * Computes the 'intensity' of each {@link Room}. Rooms generally get more
     * intense the deeper they are into the dungeon.
     *
     * @param levels    the keyLevel -> room-set mapping to update
     * @throws RetryException if it fails
     * @see KeyLevelRoomMapping
     * @see Room
     */
    protected void computeIntensity(KeyLevelRoomMapping levels)
            throws RetryException {
       
        double nextLevelBaseIntensity = 0.0;
        for (int level = 0; level < levels.keyCount(); ++level) {
           
            double intensity = nextLevelBaseIntensity *
                    (1.0 - INTENSITY_EASE_OFF);
           
            for (Room room: levels.getRooms(level)) {
                if (room.getParent() == null ||
                        !room.getParent().getPrecond().
                            implies(room.getPrecond())) {
                    nextLevelBaseIntensity = Math.max(
                            nextLevelBaseIntensity,
                            applyIntensity(room, intensity));
                }
            }
        }
       
        normalizeIntensity();
       
        dungeon.findBoss().setIntensity(1.0);
        Room goalRoom = dungeon.findGoal();
        if (goalRoom != null)
            goalRoom.setIntensity(0.0);
    }
   
    /**
     * Checks with the
     * {@link net.bytten.metazelda.constraints.IDungeonConstraints} that the
     * dungeon is OK to use.
     *
     * @throws RetryException if the IDungeonConstraints decided generation must
     *                        be re-attempted
     * @see net.bytten.metazelda.constraints.IDungeonConstraints
     */
    protected void checkAcceptable() throws RetryException {
        if (!constraints.isAcceptable(dungeon))
            throw new RetryException();
    }
   
    @Override
    public void generate() {
        int attempt = 0;

        while (true) {
            try {
                KeyLevelRoomMapping levels;
                int roomsPerLock;
                if (constraints.getMaxKeys() > 0) {
                    roomsPerLock = constraints.getMaxRooms() /
                        constraints.getMaxKeys();
                } else {
                    roomsPerLock = constraints.getMaxRooms();
                }
                while (true) {
                    dungeon = new Dungeon();
                   
                    // Maps keyLevel -> Rooms that were created when lockCount had that
                    // value
                    levels = new KeyLevelRoomMapping();
                   
                    // Create the entrance to the dungeon:
                    initEntranceRoom(levels);
               
                    try {
                        // Fill the dungeon with rooms:
                        placeRooms(levels, roomsPerLock);
                        break;
                    } catch (OutOfRoomsException e) {
                        // We can run out of rooms where certain links have
                        // predetermined locks. Example: if a river bisects the
                        // map, the keyLevel for rooms in the river > 0 because
                        // crossing water requires a key. If there are not
                        // enough rooms before the river to build up to the
                        log("Ran out of rooms. roomsPerLock was "+roomsPerLock);
                        roomsPerLock = roomsPerLock * constraints.getMaxKeys() /
                                (constraints.getMaxKeys() + 1);
                        log("roomsPerLock is now "+roomsPerLock);
                       
                        if (roomsPerLock == 0) {
                            throw new GenerationFailureException(
                                    "Failed to place rooms. Have you forgotten to disable boss-locking?");
                            // If the boss room is locked, the final key is used
                            // only for the boss room. So if the final key is
                            // also used to cross the river, rooms cannot be
                            // placed.
                        }
                    }
                }
               
                // Place the boss and goal rooms:
                placeBossGoalRooms(levels);
               
                // Place switches and the locks that require it:
                placeSwitches();
       
                // Make the dungeon less tree-like:
                graphify();
               
                computeIntensity(levels);
               
                // Place the keys within the dungeon:
                placeKeys(levels);
               
                if (levels.keyCount()-1 != constraints.getMaxKeys())
                    throw new RetryException();

                checkAcceptable();
               
                return;
           
            } catch (RetryException e) {
                if (++ attempt > MAX_RETRIES) {
                    throw new GenerationFailureException("Dungeon generator failed", e);
                }
                log("Retrying dungeon generation...");
            }
        }
       
    }

    @Override
    public IDungeon getDungeon() {
        return dungeon;
    }

    public boolean isBossRoomLocked() {
        return bossRoomLocked;
    }

    public void setBossRoomLocked(boolean bossRoomLocked) {
        this.bossRoomLocked = bossRoomLocked;
    }

    public boolean isGenerateGoal() {
        return generateGoal;
    }

    public void setGenerateGoal(boolean generateGoal) {
        this.generateGoal = generateGoal;
    }

}
TOP

Related Classes of net.bytten.metazelda.generators.DungeonGenerator$KeyLevelRoomMapping

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.