Repository: SiENcE/astray Branch: master Commit: ea48e9ac9731 Files: 15 Total size: 54.5 KB Directory structure: gitextract_yev__z04/ ├── LICENSE ├── README.md ├── astray/ │ ├── MiddleClass.lua │ ├── astray.lua │ ├── cell.lua │ ├── directionpicker.lua │ ├── dungeon.lua │ ├── init.lua │ ├── map.lua │ ├── point.lua │ ├── room.lua │ ├── roomgenerator.lua │ └── util.lua ├── conf.lua └── main.lua ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ Copyright (c) <''2014''> <''Florian Fischer''> This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. ================================================ FILE: README.md ================================================ # Astray [](https://opensource.org/licenses/Zlib) Astray is a robust Lua library for procedural generation of mazes, rooms, and dungeons. It provides a flexible system for creating diverse layouts suitable for dungeon crawlers, roguelikes, and other games requiring procedural map generation.
## Features - Procedural maze generation using modified depth-first search - Customizable room placement and connection - Configurable parameters for dungeon characteristics: - Corridor density and sparseness - Room size and quantity - Dead end frequency - Direction change probability - ASCII map output with customizable tiles - Support for different cell types (corridors, rooms, doors) ## Installation 1. Copy the Astray folder to your project: ```bash git clone https://github.com/SiENcE/astray.git ``` 2. Include the library in your Lua code: ```lua local astray = require('astray') ``` ## Quick Start ```lua local astray = require('astray') -- Initialize generator with desired parameters -- Note: Astray generates uneven-sized maps (e.g., 39x39 from 40x40 input) local height, width = 40, 40 local generator = astray.Astray:new( width/2-1, -- Map width height/2-1, -- Map height 30, -- Change direction modifier (1-30) 70, -- Sparseness modifier (25-70) 50, -- Dead end removal modifier (50-99) astray.RoomGenerator:new( -- Room generator configuration 4, -- Number of rooms 2, 4, -- Min/Max room width 2, 4 -- Min/Max room height ) ) -- Generate dungeon local dungeon = generator:Generate() -- Convert to ASCII tiles local tiles = generator:CellToTiles(dungeon) -- Print the dungeon for y = 0, #tiles[1] do local line = '' for x = 0, #tiles do line = line .. tiles[x][y] end print(line) end ``` ## Configuration ### Astray Constructor Parameters ```lua Astray:new(width, height, changeDirectionMod, sparsenessMod, deadEndRemovalMod, roomGenerator) ``` | Parameter | Range | Description | |-----------|-------|-------------| | width | > 0 | Width of the dungeon in even numbers (map will be uneven) | | height | > 0 | Height of the dungeon in even numbers (map will be uneven) | | changeDirectionMod | 1-30 | Higher values create more winding corridors | | sparsenessMod | 25-70 | Higher values create more open layouts | | deadEndRemovalMod | 50-99 | Higher values remove more dead ends | ### Room Generator Parameters ```lua RoomGenerator:new(rooms, minWidth, maxWidth, minHeight, maxHeight) ``` | Parameter | Description | |-----------|-------------| | rooms | Number of rooms to generate | | minWidth | Minimum room width | | maxWidth | Maximum room width | | minHeight | Minimum room height | | maxHeight | Maximum room height | ## Customizing Tiles You can customize the appearance of generated dungeons by providing a tile mapping: ```lua local symbols = { Wall = '#', Empty = ' ', DoorN = '|', DoorS = '|', DoorE = '-', DoorW = '-' } local tiles = generator:CellToTiles(dungeon, symbols) ``` ## Example Output ``` Map size= 39 39 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓-▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ | ▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓-▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ | | ▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓ ▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓ ▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓ ▓ ▓-▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓ ▓ | ▓▓▓▓▓ ▓▓▓ ▓ ▓ ▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓ ▓ ▓ ▓▓▓▓▓▓▓▓▓ ▓ ▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓ ▓ ▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓ ▓ ▓ ▓ ▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓ ▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓-▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓▓▓ | | ▓▓▓ ▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓-▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ``` ## License Distributed under the [zlib/libpng License](https://opensource.org/licenses/Zlib). See `LICENSE` for more information. ## Contributing 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## Acknowledgments This work is based on various dungeon generation techniques and algorithms: - [Dirkkok's Random Dungeon Generation](http://dirkkok.wordpress.com/2007/11/21/generating-random-dungeons-part-1/) - [Myth-Weavers Dungeon Generator](http://www.myth-weavers.com/generate_dungeon.php) - [Thomas Bowker's Dungeon Generation](http://thomasbowker.com/2013/08/02/generating-a-dungeon/) ## Support For issues, questions, or contributions, please visit the [GitHub repository](https://github.com/SiENcE/astray). ## Author Florian Fischer ================================================ FILE: astray/MiddleClass.lua ================================================ local middleclass = { _VERSION = 'middleclass v3.0.0', _DESCRIPTION = 'Object Orientation for Lua', _LICENSE = [[ MIT LICENSE Copyright (c) 2011 Enrique García Cota Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] } local function _setClassDictionariesMetatables(aClass) local dict = aClass.__instanceDict dict.__index = dict local super = aClass.super if super then local superStatic = super.static setmetatable(dict, super.__instanceDict) setmetatable(aClass.static, { __index = function(_,k) return dict[k] or superStatic[k] end }) else setmetatable(aClass.static, { __index = function(_,k) return dict[k] end }) end end local function _setClassMetatable(aClass) setmetatable(aClass, { __tostring = function() return "class " .. aClass.name end, __index = aClass.static, __newindex = aClass.__instanceDict, __call = function(self, ...) return self:new(...) end }) end local function _createClass(name, super) local aClass = { name = name, super = super, static = {}, __mixins = {}, __instanceDict={} } aClass.subclasses = setmetatable({}, {__mode = "k"}) _setClassDictionariesMetatables(aClass) _setClassMetatable(aClass) return aClass end local function _createLookupMetamethod(aClass, name) return function(...) local method = aClass.super[name] assert( type(method)=='function', tostring(aClass) .. " doesn't implement metamethod '" .. name .. "'" ) return method(...) end end local function _setClassMetamethods(aClass) for _,m in ipairs(aClass.__metamethods) do aClass[m]= _createLookupMetamethod(aClass, m) end end local function _setDefaultInitializeMethod(aClass, super) aClass.initialize = function(instance, ...) return super.initialize(instance, ...) end end local function _includeMixin(aClass, mixin) assert(type(mixin)=='table', "mixin must be a table") for name,method in pairs(mixin) do if name ~= "included" and name ~= "static" then aClass[name] = method end end if mixin.static then for name,method in pairs(mixin.static) do aClass.static[name] = method end end if type(mixin.included)=="function" then mixin:included(aClass) end aClass.__mixins[mixin] = true end local Object = _createClass("Object", nil) Object.static.__metamethods = { '__add', '__call', '__concat', '__div', '__ipairs', '__le', '__len', '__lt', '__mod', '__mul', '__pairs', '__pow', '__sub', '__tostring', '__unm'} function Object.static:allocate() assert(type(self) == 'table', "Make sure that you are using 'Class:allocate' instead of 'Class.allocate'") return setmetatable({ class = self }, self.__instanceDict) end function Object.static:new(...) local instance = self:allocate() instance:initialize(...) return instance end function Object.static:subclass(name) assert(type(self) == 'table', "Make sure that you are using 'Class:subclass' instead of 'Class.subclass'") assert(type(name) == "string", "You must provide a name(string) for your class") local subclass = _createClass(name, self) _setClassMetamethods(subclass) _setDefaultInitializeMethod(subclass, self) self.subclasses[subclass] = true self:subclassed(subclass) return subclass end function Object.static:subclassed(other) end function Object.static:isSubclassOf(other) return type(other) == 'table' and type(self) == 'table' and type(self.super) == 'table' and ( self.super == other or type(self.super.isSubclassOf) == 'function' and self.super:isSubclassOf(other) ) end function Object.static:include( ... ) assert(type(self) == 'table', "Make sure you that you are using 'Class:include' instead of 'Class.include'") for _,mixin in ipairs({...}) do _includeMixin(self, mixin) end return self end function Object.static:includes(mixin) return type(mixin) == 'table' and type(self) == 'table' and type(self.__mixins) == 'table' and ( self.__mixins[mixin] or type(self.super) == 'table' and type(self.super.includes) == 'function' and self.super:includes(mixin) ) end function Object:initialize() end function Object:__tostring() return "instance of " .. tostring(self.class) end function Object:isInstanceOf(aClass) return type(self) == 'table' and type(self.class) == 'table' and type(aClass) == 'table' and ( aClass == self.class or type(aClass.isSubclassOf) == 'function' and self.class:isSubclassOf(aClass) ) end function middleclass.class(name, super, ...) super = super or Object return super:subclass(name, ...) end middleclass.Object = Object setmetatable(middleclass, { __call = function(_, ...) return middleclass.class(...) end }) return middleclass ================================================ FILE: astray/astray.lua ================================================ --[[ Copyright (c) <''2024''> <''Florian Fischer''> This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. ]]-- -- Globals DirectionType = { North = 0, South = 1, East = 2, West = 3 } SideType = { Empty=1, Wall=2, Door=3 } MaxValue = 65535 local PATH = (...):match("(.-)[^%.]+$") local class = require(PATH .. 'MiddleClass') local Point = require(PATH .. 'point') local RoomGenerator = require(PATH .. 'roomgenerator') local Dungeon = require(PATH .. 'dungeon') local DirectionPicker = require(PATH .. 'directionpicker') -- Class local Astray = class("Astray") function Astray:initialize( width, height, changeDirectionModifier, sparsenessModifier, deadEndRemovalModifier, roomGenerator) -- print('Astray:initialize') math.randomseed(os.time()) math.random(); math.random(); math.random() self.width = width or 25 self.height = height or 25 self.changeDirectionModifier = changeDirectionModifier or 30 self.sparsenessModifier = sparsenessModifier or 70 self.deadEndRemovalModifier = deadEndRemovalModifier or 50 self.roomGenerator = roomGenerator or RoomGenerator:new(10, 1, 5, 1, 5) end function Astray:Generate() local dungeon = Dungeon:new(self.width, self.height) dungeon:FlagAllCellsAsUnvisited() self:CreateDenseMaze(dungeon) self:SparsifyMaze(dungeon) self:RemoveDeadEnds(dungeon) self.roomGenerator:PlaceRooms(dungeon) self.roomGenerator:PlaceDoors(dungeon) return dungeon end function Astray:GenerateDungeon() local dungeon = Dungeon:new(self.width, self.height) dungeon:FlagAllCellsAsUnvisited() self:CreateDenseMaze(dungeon) return dungeon end function Astray:GeneratePlaceRooms(dungeon) self.roomGenerator:PlaceRooms(dungeon) end function Astray:GeneratePlaceDoors(dungeon) self.roomGenerator:PlaceDoors(dungeon) end function Astray:CreateDenseMaze( dungeon ) local currentLocation = dungeon:PickRandomCellAndFlagItAsVisited() local previousDirection = DirectionType.North while (not dungeon:AllCellsAreVisited() ) do local directionPicker = DirectionPicker:new(previousDirection, self.changeDirectionModifier) local direction = directionPicker:GetNextDirection() while (not dungeon:HasAdjacentCellInDirection(currentLocation, direction)) or dungeon:AdjacentCellInDirectionIsVisited(currentLocation, direction) do if directionPicker:HasNextDirection() then direction = directionPicker:GetNextDirection() else currentLocation = dungeon:GetRandomVisitedCell(currentLocation) -- Get a new previously visited location directionPicker = DirectionPicker:new(previousDirection, self.changeDirectionModifier) -- Reset the direction picker direction = directionPicker:GetNextDirection() -- Get a new direction end end currentLocation = dungeon:CreateCorridor(currentLocation, direction) dungeon:FlagCellAsVisited(currentLocation) previousDirection = direction end end function Astray:SparsifyMaze(dungeon) -- Calculate the target number of cells to remove local noOfDeadEndCellsToRemove = math.ceil((self.sparsenessModifier / 100) * (dungeon:getWidth() * dungeon:getHeight())) -- Initialize counters local cellsRemoved = 0 local maxIterations = dungeon:getWidth() * dungeon:getHeight() -- Maximum possible iterations local iterations = 0 while cellsRemoved < noOfDeadEndCellsToRemove do -- Get current dead end cells local deadEndCellList = dungeon:DeadEndCellLocations() -- Break if no more dead ends or max iterations reached if #deadEndCellList < 2 or iterations >= maxIterations then break end -- Process each dead end for _, point in pairs(deadEndCellList) do local cell = dungeon:getCell(point) local direction = cell:CalculateDeadEndCorridorDirection() -- Remove the dead end dungeon:CreateWall(point, direction) dungeon:getCell(point):setIsCorridor(false) -- Update counter cellsRemoved = cellsRemoved + 1 -- Check if we've reached our target if cellsRemoved >= noOfDeadEndCellsToRemove then break end end -- Increment iteration counter iterations = iterations + 1 end -- Log completion statistics if needed if iterations >= maxIterations then print(string.format("WARNING: SparsifyMaze reached maximum iterations. Removed %d of %d planned cells.", cellsRemoved, noOfDeadEndCellsToRemove)) end end function Astray:RemoveDeadEnds( dungeon ) local deadEndCellList = dungeon:DeadEndCellLocations() for key,deadEndLocation in pairs( deadEndCellList ) do if self:ShouldRemoveDeadend() then local currentLocation = deadEndLocation repeat local directionPicker = DirectionPicker:new( dungeon:getCell(currentLocation):CalculateDeadEndCorridorDirection(), 100) local direction = directionPicker:GetNextDirection() while (not dungeon:HasAdjacentCellInDirection(currentLocation, direction)) do if directionPicker:HasNextDirection() then direction = directionPicker:GetNextDirection() else print("ERROR: This should not happen") end end -- Create a corridor in the selected direction currentLocation = dungeon:CreateCorridor(currentLocation, direction) until (not dungeon:getCell(currentLocation):getIsDeadEnd()) -- Stop when you intersect an existing corridor. end end end function Astray:ShouldRemoveDeadend() return math.random(1, 99) < self.deadEndRemovalModifier end function Astray:CellToTiles( dungeon, tiles ) local tile = tiles if not tile then tile = {} tile.Wall = '' tile.Empty = ' ' tile.DoorN = '|' tile.DoorS = '|' tile.DoorE = '-' tile.DoorW = '-' end local expanded = {} for x = 0, dungeon:getWidth()*2 do expanded[x] = {} for y = 0, dungeon:getHeight()*2 do expanded[x][y] = tile.Wall end end local minPoint = nil local maxPoint = nil for key,room in pairs(dungeon.rooms) do -- Get the room min and max location in tile coordinates minPoint = Point:new(room:getBounds().X * 2 + 1, room:getBounds().Y * 2 + 1) maxPoint = Point:new( (room:getBounds().X+room:getBounds().Width) * 2, (room:getBounds().Y+room:getBounds().Height) * 2 ) -- Fill the room in tile space with an empty value for i = minPoint.X, maxPoint.X-1 do for j = minPoint.Y, maxPoint.Y-1 do expanded[i][j] = tile.Empty end end end local target = nil for key,location in pairs( dungeon:CorridorCellLocations() ) do target = Point:new(location.X*2+1, location.Y*2+1) expanded[target.X][target.Y] = tile.Empty if dungeon:getCell(location):getNorthSide() == SideType.Empty then expanded[target.X][target.Y-1] = tile.Empty end if dungeon:getCell(location):getNorthSide() == SideType.Door then expanded[target.X][target.Y-1] = tile.DoorN end if dungeon:getCell(location):getSouthSide() == SideType.Empty then expanded[target.X][target.Y+1] = tile.Empty end if dungeon:getCell(location):getSouthSide() == SideType.Door then expanded[target.X][target.Y+1] = tile.DoorS end if dungeon:getCell(location):getEastSide() == SideType.Empty then expanded[target.X+1][target.Y] = tile.Empty end if dungeon:getCell(location):getEastSide() == SideType.Door then expanded[target.X+1][target.Y] = tile.DoorE end if dungeon:getCell(location):getWestSide() == SideType.Empty then expanded[target.X-1][target.Y] = tile.Empty end if dungeon:getCell(location):getWestSide() == SideType.Door then expanded[target.X-1][target.Y] = tile.DoorW end end return expanded end ------------------------------------------------------ -- helper functions ------------------------------------------------------ function Astray:getWidth() return self.width end function Astray:setWidth( width ) self.width = width end function Astray:getHeight() return self.height end function Astray:setHeight( height ) self.height = height end function Astray:getChangeDirectionModifier() return self.changeDirectionModifier end function Astray:setChangeDirectionModifier( changeDirectionModifier ) self.changeDirectionModifier = changeDirectionModifier end function Astray:getSparsenessModifier() return self.sparsenessModifier end function Astray:setSparsenessModifier( sparsenessModifier ) self.sparsenessModifier = sparsenessModifier end function Astray:getDeadEndRemovalModifier() return self.deadEndRemovalModifier end function Astray:setDeadEndRemovalModifier( deadEndRemovalModifier ) self.deadEndRemovalModifier = deadEndRemovalModifier end return Astray ================================================ FILE: astray/cell.lua ================================================ --[[ Copyright (c) <''2014''> <''Florian Fischer''> This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. ]]-- local PATH = (...):match("(.-)[^%.]+$") local class = require(PATH .. 'MiddleClass') -- Class local Cell = class("Cell") function Cell:initialize() self.eastSide = SideType.Wall self.northSide = SideType.Wall self.southSide = SideType.Wall self.westSide = SideType.Wall self.visited = false self.isCorridor = false end function Cell:CalculateDeadEndCorridorDirection() if (not self:getIsDeadEnd()) then print('ERROR: InvalidOperationException (Cell:CalculateDeadEndCorridorDirection): not getIsDeadEnd()') end if (self.northSide == SideType.Empty) then return DirectionType.North end if (self.southSide == SideType.Empty) then return DirectionType.South end if (self.westSide == SideType.Empty) then return DirectionType.West end if (self.eastSide == SideType.Empty) then return DirectionType.East end print('ERROR: InvalidOperationException (Cell:CalculateDeadEndCorridorDirection)') end ------------------------------------------------------ -- helper functions ------------------------------------------------------ function Cell:getVisited() return self.visited end function Cell:setVisited( visited ) self.visited = visited end function Cell:getNorthSide() return self.northSide end function Cell:setNorthSide( northSide ) self.northSide = northSide end function Cell:getSouthSide() return self.southSide end function Cell:setSouthSide( southSide ) self.southSide = southSide end function Cell:getEastSide() return self.eastSide end function Cell:setEastSide( eastSide ) self.eastSide = eastSide end function Cell:getWestSide() return self.westSide end function Cell:setWestSide( westSide ) self.westSide = westSide end function Cell:getIsDeadEnd() return self:getWallCount() == 3 end function Cell:getIsCorridor() return self.isCorridor end function Cell:setIsCorridor( isCorridor ) self.isCorridor = isCorridor end function Cell:getWallCount() local wallCount = 0 if (self.northSide == SideType.Wall) then wallCount=wallCount+1 end if (self.southSide == SideType.Wall) then wallCount=wallCount+1 end if (self.westSide == SideType.Wall) then wallCount=wallCount+1 end if (self.eastSide == SideType.Wall) then wallCount=wallCount+1 end return wallCount end return Cell ================================================ FILE: astray/directionpicker.lua ================================================ --[[ Copyright (c) <''2014''> <''Florian Fischer''> This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. ]]-- local PATH = (...):match("(.-)[^%.]+$") local class = require(PATH .. 'MiddleClass') local Util = require(PATH .. 'util') -- Class local DirectionPicker = class("DirectionPicker") function DirectionPicker:initialize(previousDirection, changeDirectionModifier) -- Initialize all available directions self.availableDirections = { DirectionType.North, DirectionType.South, DirectionType.East, DirectionType.West } self.previousDirection = previousDirection self.changeDirectionModifier = changeDirectionModifier self.directionsPicked = {} end function DirectionPicker:PickDifferentDirection() -- Filter out previously picked directions and the current direction local validDirections = {} for _, dir in ipairs(self.availableDirections) do if dir ~= self.previousDirection and not Util:tablecontains(self.directionsPicked, dir) then table.insert(validDirections, dir) end end -- If no valid directions, return any unpicked direction if #validDirections == 0 then for _, dir in ipairs(self.availableDirections) do if not Util:tablecontains(self.directionsPicked, dir) then return dir end end -- If all directions are picked, return the previous direction as last resort return self.previousDirection end -- Return random valid direction return validDirections[math.random(1, #validDirections)] end function DirectionPicker:GetNextDirection() if not self:HasNextDirection() then return nil end local directionPicked if self:MustChangeDirection() then directionPicked = self:PickDifferentDirection() else directionPicked = self.previousDirection end table.insert(self.directionsPicked, directionPicked) return directionPicked end function DirectionPicker:HasNextDirection() return #self.directionsPicked < 4 end function DirectionPicker:MustChangeDirection() return (#self.directionsPicked > 0) or (self.changeDirectionModifier > math.random(0, 99)) end return DirectionPicker ================================================ FILE: astray/dungeon.lua ================================================ --[[ Copyright (c) <''2014''> <''Florian Fischer''> This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. ]]-- local PATH = (...):match("(.-)[^%.]+$") local class = require(PATH .. 'MiddleClass') local Util = require(PATH .. 'util') local Point = require(PATH .. 'point') local Map = require(PATH .. 'map') -- Class local Dungeon = class("Dungeon", Map) function Dungeon:initialize( width, height ) Map.initialize(self, width, height) -- invoking the superclass' initializer self.visitedCells = {} self.rooms = {} end function Dungeon:AddRoom( room ) table.insert(self.rooms, room ) end function Dungeon:FlagAllCellsAsUnvisited() for key,location in pairs( self:getCellLocations() ) do self:getCell(location):setVisited(false) end end -- return point function Dungeon:PickRandomCellAndFlagItAsVisited() local randomLocation = Point:new(math.random(0,self:getWidth() - 1), math.random(0,self:getHeight() - 1)) self:FlagCellAsVisited(randomLocation) return randomLocation end -- return boolean function Dungeon:AdjacentCellInDirectionIsVisited( location, direction ) local target = self:GetTargetLocation(location, direction) if target == nil then return false end if direction == DirectionType.North then return self:getCell(target):getVisited() elseif direction == DirectionType.West then return self:getCell(target):getVisited() elseif direction == DirectionType.South then return self:getCell(target):getVisited() elseif direction == DirectionType.East then return self:getCell(target):getVisited() else print('ERROR: InvalidOperationException (Dungeon:AdjacentCellInDirectionIsVisited)') return nil end end -- return boolean function Dungeon:AdjacentCellInDirectionIsCorridor( location, direction ) local target = self:GetTargetLocation(location, direction) if target == nil then return false end if direction == DirectionType.North then return self:getCell(target):getIsCorridor() elseif direction == DirectionType.West then return self:getCell(target):getIsCorridor() elseif direction == DirectionType.South then return self:getCell(target):getIsCorridor() elseif direction == DirectionType.East then return self:getCell(target):getIsCorridor() else print('ERROR: InvalidOperationException (Dungeon:AdjacentCellInDirectionIsCorridor)') return false end end function Dungeon:FlagCellAsVisited( location ) if not Util:rectbound(location, self:getBounds() ) then print('ERROR: Location is outside of Dungeon bounds') end if self:getCell(location):getVisited() then print('ERROR: Location is already visited') end self:getCell(location):setVisited(true) table.insert( self.visitedCells, location ) end -- return point function Dungeon:GetRandomVisitedCell( location ) if (#self.visitedCells == 0) then print("ERROR: There are no visited cells to return.") return nil end local index = math.random(#self.visitedCells-1) -- Loop while the current cell is the visited cell while (self.visitedCells[index].X == location.X and self.visitedCells[index].Y == location.Y) do index = math.random(#self.visitedCells - 1) end return self.visitedCells[index] end -- return point function Dungeon:CreateCorridor( location, direction ) local targetLocation = self:CreateSide(location, direction, SideType.Empty) self:getCell(location):setIsCorridor(true) -- Set current location to corridor self:getCell(targetLocation):setIsCorridor(true) --Set target location to corridor return targetLocation end -- return point function Dungeon:CreateWall( location, direction ) return self:CreateSide(location, direction, SideType.Wall) end -- return point function Dungeon:CreateDoor( location, direction ) return self:CreateSide(location, direction, SideType.Door) end -- return point function Dungeon:CreateSide( location, direction, sideType ) local target = self:GetTargetLocation(location, direction) if (target == nil) then print('ERROR: ArgumentException("There is no adjacent cell in the given direction", "location")') end if direction == DirectionType.North then self:getCell(location):setNorthSide( sideType ) self:getCell(target):setSouthSide( sideType ) elseif direction == DirectionType.South then self:getCell(location):setSouthSide( sideType ) self:getCell(target):setNorthSide( sideType ) elseif direction == DirectionType.West then self:getCell(location):setWestSide( sideType ) self:getCell(target):setEastSide( sideType ) elseif direction == DirectionType.East then self:getCell(location):setEastSide( sideType ) self:getCell(target):setWestSide( sideType ) end return target end ------------------------------------------------------ -- helper functions ------------------------------------------------------ -- return boolean function Dungeon:AllCellsAreVisited() return #self.visitedCells == ( self:getWidth() * self:getHeight() ) end -- IEnumerable