[
  {
    "path": "LICENSE",
    "content": "Copyright (c) <''2014''> <''Florian Fischer''>\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n   1. The origin of this software must not be misrepresented; you must not\n   claim that you wrote the original software. If you use this software\n   in a product, an acknowledgment in the product documentation would be\n   appreciated but is not required.\n\n   2. Altered source versions must be plainly marked as such, and must not be\n   misrepresented as being the original software.\n\n   3. This notice may not be removed or altered from any source\n   distribution.\n"
  },
  {
    "path": "README.md",
    "content": "# Astray\n\n[![License: Zlib](https://img.shields.io/badge/License-Zlib-brightgreen.svg)](https://opensource.org/licenses/Zlib)\n\nAstray 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.\n\n<p align=\"center\">\n <a href=\"https://raw.githubusercontent.com/SiENcE/astray/master/sample.png\">\n  <img border=\"0\" src=\"https://raw.githubusercontent.com/SiENcE/astray/master/sample.png\">\n </a>\n</p>\n\n## Features\n\n- Procedural maze generation using modified depth-first search\n- Customizable room placement and connection\n- Configurable parameters for dungeon characteristics:\n  - Corridor density and sparseness\n  - Room size and quantity\n  - Dead end frequency\n  - Direction change probability\n- ASCII map output with customizable tiles\n- Support for different cell types (corridors, rooms, doors)\n\n## Installation\n\n1. Copy the Astray folder to your project:\n```bash\ngit clone https://github.com/SiENcE/astray.git\n```\n\n2. Include the library in your Lua code:\n```lua\nlocal astray = require('astray')\n```\n\n## Quick Start\n\n```lua\nlocal astray = require('astray')\n\n-- Initialize generator with desired parameters\n-- Note: Astray generates uneven-sized maps (e.g., 39x39 from 40x40 input)\nlocal height, width = 40, 40\nlocal generator = astray.Astray:new(\n    width/2-1,                -- Map width\n    height/2-1,               -- Map height\n    30,                       -- Change direction modifier (1-30)\n    70,                       -- Sparseness modifier (25-70)\n    50,                       -- Dead end removal modifier (50-99)\n    astray.RoomGenerator:new( -- Room generator configuration\n        4,                    -- Number of rooms\n        2, 4,                -- Min/Max room width\n        2, 4                 -- Min/Max room height\n    )\n)\n\n-- Generate dungeon\nlocal dungeon = generator:Generate()\n\n-- Convert to ASCII tiles\nlocal tiles = generator:CellToTiles(dungeon)\n\n-- Print the dungeon\nfor y = 0, #tiles[1] do\n    local line = ''\n    for x = 0, #tiles do\n        line = line .. tiles[x][y]\n    end\n    print(line)\nend\n```\n\n## Configuration\n\n### Astray Constructor Parameters\n\n```lua\nAstray:new(width, height, changeDirectionMod, sparsenessMod, deadEndRemovalMod, roomGenerator)\n```\n\n| Parameter | Range | Description |\n|-----------|-------|-------------|\n| width | > 0 | Width of the dungeon in even numbers (map will be uneven) |\n| height | > 0 | Height of the dungeon in even numbers (map will be uneven) |\n| changeDirectionMod | 1-30 | Higher values create more winding corridors |\n| sparsenessMod | 25-70 | Higher values create more open layouts |\n| deadEndRemovalMod | 50-99 | Higher values remove more dead ends |\n\n### Room Generator Parameters\n\n```lua\nRoomGenerator:new(rooms, minWidth, maxWidth, minHeight, maxHeight)\n```\n\n| Parameter | Description |\n|-----------|-------------|\n| rooms | Number of rooms to generate |\n| minWidth | Minimum room width |\n| maxWidth | Maximum room width |\n| minHeight | Minimum room height |\n| maxHeight | Maximum room height |\n\n## Customizing Tiles\n\nYou can customize the appearance of generated dungeons by providing a tile mapping:\n\n```lua\nlocal symbols = {\n    Wall = '#',\n    Empty = ' ',\n    DoorN = '|',\n    DoorS = '|',\n    DoorE = '-',\n    DoorW = '-'\n}\n\nlocal tiles = generator:CellToTiles(dungeon, symbols)\n```\n\n## Example Output\n\n```\nMap size=       39      39\n▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n▓▓▓                 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n▓▓▓ ▓▓▓-▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n▓▓▓   |       ▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n▓▓▓▓▓▓▓       ▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n▓▓▓▓▓▓▓       ▓▓▓▓▓     ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓-▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓▓▓▓▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ |   |       ▓   ▓▓▓▓▓▓▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓   ▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓   ▓▓▓▓▓     ▓       ▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓\n▓   ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓\n▓ ▓-▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓\n▓ |     ▓▓▓▓▓             ▓▓▓     ▓   ▓\n▓ ▓     ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓\n▓ ▓     ▓▓▓▓▓▓▓▓▓                 ▓   ▓\n▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓\n▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓\n▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓\n▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓         ▓\n▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓\n▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓         ▓▓▓▓▓▓▓▓▓\n▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓▓\n▓   ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓   ▓\n▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓▓▓▓▓ ▓ ▓\n▓         ▓▓▓▓▓▓▓ ▓▓▓     ▓▓▓ ▓▓▓▓▓   ▓\n▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓-▓▓▓▓▓▓▓ ▓▓▓ ▓▓▓▓▓ ▓▓▓\n▓▓▓▓▓▓▓▓▓       |       | ▓▓▓ ▓▓▓▓▓   ▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓       ▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓       ▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓       ▓ ▓▓▓ ▓▓▓▓▓▓▓ ▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓       ▓ ▓▓▓         ▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓-▓▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓     ▓▓▓▓▓▓▓▓▓▓▓▓▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓\n```\n\n## License\n\nDistributed under the [zlib/libpng License](https://opensource.org/licenses/Zlib). See `LICENSE` for more information.\n\n## Contributing\n\n1. Fork the repository\n2. Create a feature branch (`git checkout -b feature/amazing-feature`)\n3. Commit your changes (`git commit -m 'Add amazing feature'`)\n4. Push to the branch (`git push origin feature/amazing-feature`)\n5. Open a Pull Request\n\n## Acknowledgments\n\nThis work is based on various dungeon generation techniques and algorithms:\n- [Dirkkok's Random Dungeon Generation](http://dirkkok.wordpress.com/2007/11/21/generating-random-dungeons-part-1/)\n- [Myth-Weavers Dungeon Generator](http://www.myth-weavers.com/generate_dungeon.php)\n- [Thomas Bowker's Dungeon Generation](http://thomasbowker.com/2013/08/02/generating-a-dungeon/)\n\n## Support\n\nFor issues, questions, or contributions, please visit the [GitHub repository](https://github.com/SiENcE/astray).\n\n## Author\n\nFlorian Fischer\n"
  },
  {
    "path": "astray/MiddleClass.lua",
    "content": "local middleclass = {\n  _VERSION     = 'middleclass v3.0.0',\n  _DESCRIPTION = 'Object Orientation for Lua',\n  _LICENSE     = [[\n    MIT LICENSE\n\n    Copyright (c) 2011 Enrique García Cota\n\n    Permission is hereby granted, free of charge, to any person obtaining a\n    copy of this software and associated documentation files (the\n    \"Software\"), to deal in the Software without restriction, including\n    without limitation the rights to use, copy, modify, merge, publish,\n    distribute, sublicense, and/or sell copies of the Software, and to\n    permit persons to whom the Software is furnished to do so, subject to\n    the following conditions:\n\n    The above copyright notice and this permission notice shall be included\n    in all copies or substantial portions of the Software.\n\n    THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\n    OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\n    IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\n    CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\n    TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\n    SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n  ]]\n}\n\nlocal function _setClassDictionariesMetatables(aClass)\n  local dict = aClass.__instanceDict\n  dict.__index = dict\n\n  local super = aClass.super\n  if super then\n    local superStatic = super.static\n    setmetatable(dict, super.__instanceDict)\n    setmetatable(aClass.static, { __index = function(_,k) return dict[k] or superStatic[k] end })\n  else\n    setmetatable(aClass.static, { __index = function(_,k) return dict[k] end })\n  end\nend\n\nlocal function _setClassMetatable(aClass)\n  setmetatable(aClass, {\n    __tostring = function() return \"class \" .. aClass.name end,\n    __index    = aClass.static,\n    __newindex = aClass.__instanceDict,\n    __call     = function(self, ...) return self:new(...) end\n  })\nend\n\nlocal function _createClass(name, super)\n  local aClass = { name = name, super = super, static = {}, __mixins = {}, __instanceDict={} }\n  aClass.subclasses = setmetatable({}, {__mode = \"k\"})\n\n  _setClassDictionariesMetatables(aClass)\n  _setClassMetatable(aClass)\n\n  return aClass\nend\n\nlocal function _createLookupMetamethod(aClass, name)\n  return function(...)\n    local method = aClass.super[name]\n    assert( type(method)=='function', tostring(aClass) .. \" doesn't implement metamethod '\" .. name .. \"'\" )\n    return method(...)\n  end\nend\n\nlocal function _setClassMetamethods(aClass)\n  for _,m in ipairs(aClass.__metamethods) do\n    aClass[m]= _createLookupMetamethod(aClass, m)\n  end\nend\n\nlocal function _setDefaultInitializeMethod(aClass, super)\n  aClass.initialize = function(instance, ...)\n    return super.initialize(instance, ...)\n  end\nend\n\nlocal function _includeMixin(aClass, mixin)\n  assert(type(mixin)=='table', \"mixin must be a table\")\n  for name,method in pairs(mixin) do\n    if name ~= \"included\" and name ~= \"static\" then aClass[name] = method end\n  end\n  if mixin.static then\n    for name,method in pairs(mixin.static) do\n      aClass.static[name] = method\n    end\n  end\n  if type(mixin.included)==\"function\" then mixin:included(aClass) end\n  aClass.__mixins[mixin] = true\nend\n\nlocal Object = _createClass(\"Object\", nil)\n\nObject.static.__metamethods = { '__add', '__call', '__concat', '__div', '__ipairs', '__le',\n                                '__len', '__lt', '__mod', '__mul', '__pairs', '__pow', '__sub',\n                                '__tostring', '__unm'}\n\nfunction Object.static:allocate()\n  assert(type(self) == 'table', \"Make sure that you are using 'Class:allocate' instead of 'Class.allocate'\")\n  return setmetatable({ class = self }, self.__instanceDict)\nend\n\nfunction Object.static:new(...)\n  local instance = self:allocate()\n  instance:initialize(...)\n  return instance\nend\n\nfunction Object.static:subclass(name)\n  assert(type(self) == 'table', \"Make sure that you are using 'Class:subclass' instead of 'Class.subclass'\")\n  assert(type(name) == \"string\", \"You must provide a name(string) for your class\")\n\n  local subclass = _createClass(name, self)\n  _setClassMetamethods(subclass)\n  _setDefaultInitializeMethod(subclass, self)\n  self.subclasses[subclass] = true\n  self:subclassed(subclass)\n\n  return subclass\nend\n\nfunction Object.static:subclassed(other) end\n\nfunction Object.static:isSubclassOf(other)\n  return type(other)                   == 'table' and\n         type(self)                    == 'table' and\n         type(self.super)              == 'table' and\n         ( self.super == other or\n           type(self.super.isSubclassOf) == 'function' and\n           self.super:isSubclassOf(other)\n         )\nend\n\nfunction Object.static:include( ... )\n  assert(type(self) == 'table', \"Make sure you that you are using 'Class:include' instead of 'Class.include'\")\n  for _,mixin in ipairs({...}) do _includeMixin(self, mixin) end\n  return self\nend\n\nfunction Object.static:includes(mixin)\n  return type(mixin)          == 'table' and\n         type(self)           == 'table' and\n         type(self.__mixins)  == 'table' and\n         ( self.__mixins[mixin] or\n           type(self.super)           == 'table' and\n           type(self.super.includes)  == 'function' and\n           self.super:includes(mixin)\n         )\nend\n\nfunction Object:initialize() end\n\nfunction Object:__tostring() return \"instance of \" .. tostring(self.class) end\n\nfunction Object:isInstanceOf(aClass)\n  return type(self)                == 'table' and\n         type(self.class)          == 'table' and\n         type(aClass)              == 'table' and\n         ( aClass == self.class or\n           type(aClass.isSubclassOf) == 'function' and\n           self.class:isSubclassOf(aClass)\n         )\nend\n\n\n\nfunction middleclass.class(name, super, ...)\n  super = super or Object\n  return super:subclass(name, ...)\nend\n\nmiddleclass.Object = Object\n\nsetmetatable(middleclass, { __call = function(_, ...) return middleclass.class(...) end })\n\nreturn middleclass\n"
  },
  {
    "path": "astray/astray.lua",
    "content": "--[[\nCopyright (c) <''2024''> <''Florian Fischer''>\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n   1. The origin of this software must not be misrepresented; you must not\n   claim that you wrote the original software. If you use this software\n   in a product, an acknowledgment in the product documentation would be\n   appreciated but is not required.\n\n   2. Altered source versions must be plainly marked as such, and must not be\n   misrepresented as being the original software.\n\n   3. This notice may not be removed or altered from any source\n   distribution.\n]]--\n\n-- Globals\nDirectionType = { North = 0, South = 1, East = 2, West = 3 }\nSideType = { Empty=1, Wall=2, Door=3 }\nMaxValue = 65535\n\nlocal PATH = (...):match(\"(.-)[^%.]+$\")\n\nlocal class = require(PATH .. 'MiddleClass')\nlocal Point = require(PATH .. 'point')\nlocal RoomGenerator = require(PATH .. 'roomgenerator')\nlocal Dungeon = require(PATH .. 'dungeon')\nlocal DirectionPicker = require(PATH .. 'directionpicker')\n\n-- Class\nlocal Astray = class(\"Astray\")\n\nfunction Astray:initialize( width, height, changeDirectionModifier, sparsenessModifier, deadEndRemovalModifier, roomGenerator)\n--\tprint('Astray:initialize')\n\tmath.randomseed(os.time())\n\tmath.random(); math.random(); math.random()\n\t\n\tself.width = width or 25\n\tself.height = height or 25\n\tself.changeDirectionModifier = changeDirectionModifier or 30\n\tself.sparsenessModifier = sparsenessModifier or 70\n\tself.deadEndRemovalModifier = deadEndRemovalModifier or 50\n\tself.roomGenerator = roomGenerator or RoomGenerator:new(10, 1, 5, 1, 5)\nend\n\nfunction Astray:Generate()\n\tlocal dungeon = Dungeon:new(self.width, self.height)\n\tdungeon:FlagAllCellsAsUnvisited()\n\t\n\tself:CreateDenseMaze(dungeon)\n\tself:SparsifyMaze(dungeon)\n\tself:RemoveDeadEnds(dungeon)\n\n\tself.roomGenerator:PlaceRooms(dungeon)\n\tself.roomGenerator:PlaceDoors(dungeon)\n\n\treturn dungeon\nend\n\nfunction Astray:GenerateDungeon()\n\tlocal dungeon = Dungeon:new(self.width, self.height)\n\tdungeon:FlagAllCellsAsUnvisited()\n\tself:CreateDenseMaze(dungeon)\n\treturn dungeon\nend\n\nfunction Astray:GeneratePlaceRooms(dungeon)\n\tself.roomGenerator:PlaceRooms(dungeon)\nend\nfunction Astray:GeneratePlaceDoors(dungeon)\n\tself.roomGenerator:PlaceDoors(dungeon)\nend\n\nfunction Astray:CreateDenseMaze( dungeon )\n\tlocal currentLocation = dungeon:PickRandomCellAndFlagItAsVisited()\n\tlocal previousDirection = DirectionType.North\n\n\twhile (not dungeon:AllCellsAreVisited() ) do\n\t\tlocal directionPicker = DirectionPicker:new(previousDirection, self.changeDirectionModifier)\n\t\tlocal direction = directionPicker:GetNextDirection()\n\t\t\n\t\twhile (not dungeon:HasAdjacentCellInDirection(currentLocation, direction)) or dungeon:AdjacentCellInDirectionIsVisited(currentLocation, direction) do\n\t\t\tif directionPicker:HasNextDirection() then\n\t\t\t\tdirection = directionPicker:GetNextDirection()\n\t\t\telse\n\t\t\t\tcurrentLocation = dungeon:GetRandomVisitedCell(currentLocation) -- Get a new previously visited location\n\t\t\t\tdirectionPicker = DirectionPicker:new(previousDirection, self.changeDirectionModifier) -- Reset the direction picker\n\t\t\t\tdirection = directionPicker:GetNextDirection() -- Get a new direction\n\t\t\tend\n\t\tend\n\n\t\tcurrentLocation = dungeon:CreateCorridor(currentLocation, direction)\n\t\tdungeon:FlagCellAsVisited(currentLocation)\n\t\tpreviousDirection = direction\n\tend\nend\n\nfunction Astray:SparsifyMaze(dungeon)\n    -- Calculate the target number of cells to remove\n    local noOfDeadEndCellsToRemove = math.ceil((self.sparsenessModifier / 100) * (dungeon:getWidth() * dungeon:getHeight()))\n    \n    -- Initialize counters\n    local cellsRemoved = 0\n    local maxIterations = dungeon:getWidth() * dungeon:getHeight() -- Maximum possible iterations\n    local iterations = 0\n    \n    while cellsRemoved < noOfDeadEndCellsToRemove do\n        -- Get current dead end cells\n        local deadEndCellList = dungeon:DeadEndCellLocations()\n        \n        -- Break if no more dead ends or max iterations reached\n        if #deadEndCellList < 2 or iterations >= maxIterations then\n            break\n        end\n        \n        -- Process each dead end\n        for _, point in pairs(deadEndCellList) do\n            local cell = dungeon:getCell(point)\n            local direction = cell:CalculateDeadEndCorridorDirection()\n            \n            -- Remove the dead end\n            dungeon:CreateWall(point, direction)\n            dungeon:getCell(point):setIsCorridor(false)\n            \n            -- Update counter\n            cellsRemoved = cellsRemoved + 1\n            \n            -- Check if we've reached our target\n            if cellsRemoved >= noOfDeadEndCellsToRemove then\n                break\n            end\n        end\n        \n        -- Increment iteration counter\n        iterations = iterations + 1\n    end\n    \n    -- Log completion statistics if needed\n    if iterations >= maxIterations then\n        print(string.format(\"WARNING: SparsifyMaze reached maximum iterations. Removed %d of %d planned cells.\", \n                          cellsRemoved, noOfDeadEndCellsToRemove))\n    end\nend\n\nfunction Astray:RemoveDeadEnds( dungeon )\n\tlocal deadEndCellList = dungeon:DeadEndCellLocations()\n\tfor key,deadEndLocation in pairs( deadEndCellList ) do\n\t\tif self:ShouldRemoveDeadend() then\n\t\t\tlocal currentLocation = deadEndLocation\n\t\t\t\t\n\t\t\trepeat\n\t\t\t\tlocal directionPicker = DirectionPicker:new( dungeon:getCell(currentLocation):CalculateDeadEndCorridorDirection(), 100)\n\t\t\t\tlocal direction = directionPicker:GetNextDirection()\n\t\t\t\t\n\t\t\t\twhile (not dungeon:HasAdjacentCellInDirection(currentLocation, direction)) do\n\t\t\t\t\tif directionPicker:HasNextDirection() then\n\t\t\t\t\t\tdirection = directionPicker:GetNextDirection()\n\t\t\t\t\telse\n\t\t\t\t\t\tprint(\"ERROR: This should not happen\")\n\t\t\t\t\tend\n\t\t\t\tend\n\t\t\t\t-- Create a corridor in the selected direction\n\t\t\t\tcurrentLocation = dungeon:CreateCorridor(currentLocation, direction)\n\t\t\tuntil (not dungeon:getCell(currentLocation):getIsDeadEnd()) -- Stop when you intersect an existing corridor.\n\n\t\tend\n\tend\nend\n\nfunction Astray:ShouldRemoveDeadend()\n\treturn math.random(1, 99) < self.deadEndRemovalModifier\nend\n\nfunction Astray:CellToTiles( dungeon, tiles )\n\tlocal tile = tiles\n\tif not tile then\n\t\ttile = {}\n\t\ttile.Wall = ''\n\t\ttile.Empty = ' '\n\t\ttile.DoorN = '|'\n\t\ttile.DoorS = '|'\n\t\ttile.DoorE = '-'\n\t\ttile.DoorW = '-'\n\tend\n\t\n\tlocal expanded = {}\n    for x = 0, dungeon:getWidth()*2 do\n        expanded[x] = {}\n        for y = 0, dungeon:getHeight()*2  do\n\t\t\texpanded[x][y] = tile.Wall\n\t\tend\n\tend\n\t\n\tlocal minPoint = nil\n\tlocal maxPoint = nil\n\tfor key,room in pairs(dungeon.rooms) do\n\t\t-- Get the room min and max location in tile coordinates\n\t\tminPoint = Point:new(room:getBounds().X * 2 + 1, room:getBounds().Y * 2 + 1)\n\t\tmaxPoint = Point:new( (room:getBounds().X+room:getBounds().Width) * 2, (room:getBounds().Y+room:getBounds().Height) * 2 )\n\n\t\t-- Fill the room in tile space with an empty value\n\t\tfor i = minPoint.X, maxPoint.X-1 do\n\t\t\tfor j = minPoint.Y, maxPoint.Y-1 do\n\t\t\t\texpanded[i][j] = tile.Empty\n\t\t\tend\n\t\tend\n\tend\n\n\tlocal target = nil\n\tfor key,location in pairs( dungeon:CorridorCellLocations() ) do\n\t\ttarget = Point:new(location.X*2+1, location.Y*2+1)\n\t\texpanded[target.X][target.Y] = tile.Empty\n\n\t\tif dungeon:getCell(location):getNorthSide() == SideType.Empty then expanded[target.X][target.Y-1] = tile.Empty end\n\t\tif dungeon:getCell(location):getNorthSide() == SideType.Door then expanded[target.X][target.Y-1] = tile.DoorN end\n\n\t\tif dungeon:getCell(location):getSouthSide() == SideType.Empty then expanded[target.X][target.Y+1] = tile.Empty end\n\t\tif dungeon:getCell(location):getSouthSide() == SideType.Door then expanded[target.X][target.Y+1] = tile.DoorS end\n\n\t\tif dungeon:getCell(location):getEastSide() == SideType.Empty then expanded[target.X+1][target.Y] = tile.Empty end\n\t\tif dungeon:getCell(location):getEastSide() == SideType.Door then expanded[target.X+1][target.Y] = tile.DoorE end\n\n\t\tif dungeon:getCell(location):getWestSide() == SideType.Empty then expanded[target.X-1][target.Y] = tile.Empty end\n\t\tif dungeon:getCell(location):getWestSide() == SideType.Door then expanded[target.X-1][target.Y] = tile.DoorW end\n\tend\n\treturn expanded\nend\n\n------------------------------------------------------\n-- helper functions\n------------------------------------------------------\nfunction Astray:getWidth()\n\treturn self.width\nend\nfunction Astray:setWidth( width )\n\tself.width = width\nend\n\nfunction Astray:getHeight()\n\treturn self.height\nend\nfunction Astray:setHeight( height )\n\tself.height = height\nend\n\nfunction Astray:getChangeDirectionModifier()\n\treturn self.changeDirectionModifier\nend\nfunction Astray:setChangeDirectionModifier( changeDirectionModifier )\n\tself.changeDirectionModifier = changeDirectionModifier\nend\n\nfunction Astray:getSparsenessModifier()\n\treturn self.sparsenessModifier\nend\nfunction Astray:setSparsenessModifier( sparsenessModifier )\n\tself.sparsenessModifier = sparsenessModifier\nend\n\nfunction Astray:getDeadEndRemovalModifier()\n\treturn self.deadEndRemovalModifier\nend\nfunction Astray:setDeadEndRemovalModifier( deadEndRemovalModifier )\n\tself.deadEndRemovalModifier = deadEndRemovalModifier\nend\n\nreturn Astray\n"
  },
  {
    "path": "astray/cell.lua",
    "content": "--[[\nCopyright (c) <''2014''> <''Florian Fischer''>\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n   1. The origin of this software must not be misrepresented; you must not\n   claim that you wrote the original software. If you use this software\n   in a product, an acknowledgment in the product documentation would be\n   appreciated but is not required.\n\n   2. Altered source versions must be plainly marked as such, and must not be\n   misrepresented as being the original software.\n\n   3. This notice may not be removed or altered from any source\n   distribution.\n]]--\n\nlocal PATH = (...):match(\"(.-)[^%.]+$\")\n\nlocal class = require(PATH .. 'MiddleClass')\n\n-- Class\nlocal Cell = class(\"Cell\")\n\nfunction Cell:initialize()\n\tself.eastSide = SideType.Wall\n\tself.northSide = SideType.Wall\n\tself.southSide = SideType.Wall\n\tself.westSide = SideType.Wall\n\tself.visited = false\n\tself.isCorridor = false\nend\n\nfunction Cell:CalculateDeadEndCorridorDirection()\n\tif (not self:getIsDeadEnd()) then print('ERROR: InvalidOperationException (Cell:CalculateDeadEndCorridorDirection): not getIsDeadEnd()') end\n\n\tif (self.northSide == SideType.Empty) then return DirectionType.North end\n\tif (self.southSide == SideType.Empty) then return DirectionType.South end\n\tif (self.westSide == SideType.Empty) then return DirectionType.West end\n\tif (self.eastSide == SideType.Empty) then return DirectionType.East end\n\n\tprint('ERROR: InvalidOperationException (Cell:CalculateDeadEndCorridorDirection)')\nend\n\n------------------------------------------------------\n-- helper functions\n------------------------------------------------------\nfunction Cell:getVisited()\n\treturn self.visited\nend\nfunction Cell:setVisited( visited )\n\tself.visited = visited\nend\n\nfunction Cell:getNorthSide()\n\treturn self.northSide\nend\nfunction Cell:setNorthSide( northSide )\n\tself.northSide = northSide\nend\n\nfunction Cell:getSouthSide()\n\treturn self.southSide\nend\nfunction Cell:setSouthSide( southSide )\n\tself.southSide = southSide\nend\n\nfunction Cell:getEastSide()\n\treturn self.eastSide\nend\nfunction Cell:setEastSide( eastSide )\n\tself.eastSide = eastSide\nend\n\nfunction Cell:getWestSide()\n\treturn self.westSide\nend\nfunction Cell:setWestSide( westSide )\n\tself.westSide = westSide\nend\n\nfunction Cell:getIsDeadEnd()\n\treturn self:getWallCount() == 3\nend\n\nfunction Cell:getIsCorridor()\n\treturn self.isCorridor\nend\nfunction Cell:setIsCorridor( isCorridor )\n\tself.isCorridor = isCorridor\nend\n\nfunction Cell:getWallCount()\n\tlocal wallCount = 0\n\tif (self.northSide == SideType.Wall) then wallCount=wallCount+1 end\n\tif (self.southSide == SideType.Wall) then wallCount=wallCount+1 end\n\tif (self.westSide == SideType.Wall) then wallCount=wallCount+1 end\n\tif (self.eastSide == SideType.Wall) then wallCount=wallCount+1 end\n\treturn wallCount\nend\n\nreturn Cell\n"
  },
  {
    "path": "astray/directionpicker.lua",
    "content": "--[[\nCopyright (c) <''2014''> <''Florian Fischer''>\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n   1. The origin of this software must not be misrepresented; you must not\n   claim that you wrote the original software. If you use this software\n   in a product, an acknowledgment in the product documentation would be\n   appreciated but is not required.\n\n   2. Altered source versions must be plainly marked as such, and must not be\n   misrepresented as being the original software.\n\n   3. This notice may not be removed or altered from any source\n   distribution.\n]]--\n\nlocal PATH = (...):match(\"(.-)[^%.]+$\")\n\nlocal class = require(PATH .. 'MiddleClass')\nlocal Util = require(PATH .. 'util')\n\n-- Class\nlocal DirectionPicker = class(\"DirectionPicker\")\n\nfunction DirectionPicker:initialize(previousDirection, changeDirectionModifier)\n    -- Initialize all available directions\n    self.availableDirections = {\n        DirectionType.North,\n        DirectionType.South,\n        DirectionType.East,\n        DirectionType.West\n    }\n    self.previousDirection = previousDirection\n    self.changeDirectionModifier = changeDirectionModifier\n    self.directionsPicked = {}\nend\n\nfunction DirectionPicker:PickDifferentDirection()\n    -- Filter out previously picked directions and the current direction\n    local validDirections = {}\n    for _, dir in ipairs(self.availableDirections) do\n        if dir ~= self.previousDirection and not Util:tablecontains(self.directionsPicked, dir) then\n            table.insert(validDirections, dir)\n        end\n    end\n    \n    -- If no valid directions, return any unpicked direction\n    if #validDirections == 0 then\n        for _, dir in ipairs(self.availableDirections) do\n            if not Util:tablecontains(self.directionsPicked, dir) then\n                return dir\n            end\n        end\n        -- If all directions are picked, return the previous direction as last resort\n        return self.previousDirection\n    end\n    \n    -- Return random valid direction\n    return validDirections[math.random(1, #validDirections)]\nend\n\nfunction DirectionPicker:GetNextDirection()\n    if not self:HasNextDirection() then\n        return nil\n    end\n\n    local directionPicked\n    if self:MustChangeDirection() then\n        directionPicked = self:PickDifferentDirection()\n    else\n        directionPicked = self.previousDirection\n    end\n\n    table.insert(self.directionsPicked, directionPicked)\n    return directionPicked\nend\n\nfunction DirectionPicker:HasNextDirection()\n    return #self.directionsPicked < 4\nend\n\nfunction DirectionPicker:MustChangeDirection()\n    return (#self.directionsPicked > 0) or (self.changeDirectionModifier > math.random(0, 99))\nend\n\nreturn DirectionPicker"
  },
  {
    "path": "astray/dungeon.lua",
    "content": "--[[\nCopyright (c) <''2014''> <''Florian Fischer''>\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n   1. The origin of this software must not be misrepresented; you must not\n   claim that you wrote the original software. If you use this software\n   in a product, an acknowledgment in the product documentation would be\n   appreciated but is not required.\n\n   2. Altered source versions must be plainly marked as such, and must not be\n   misrepresented as being the original software.\n\n   3. This notice may not be removed or altered from any source\n   distribution.\n]]--\n\nlocal PATH = (...):match(\"(.-)[^%.]+$\")\n\nlocal class = require(PATH .. 'MiddleClass')\nlocal Util = require(PATH .. 'util')\nlocal Point = require(PATH .. 'point')\nlocal Map = require(PATH .. 'map')\n\n-- Class\nlocal Dungeon = class(\"Dungeon\", Map)\n\nfunction Dungeon:initialize( width, height )\n\tMap.initialize(self, width, height) -- invoking the superclass' initializer\n\n\tself.visitedCells = {}\n\tself.rooms = {}\nend\n\nfunction Dungeon:AddRoom( room )\n\ttable.insert(self.rooms, room )\nend\n\nfunction Dungeon:FlagAllCellsAsUnvisited()\n\tfor key,location in pairs( self:getCellLocations() ) do\n\t\tself:getCell(location):setVisited(false)\n\tend\nend\n\n-- return point\nfunction Dungeon:PickRandomCellAndFlagItAsVisited()\n\tlocal randomLocation = Point:new(math.random(0,self:getWidth() - 1), math.random(0,self:getHeight() - 1))\n\tself:FlagCellAsVisited(randomLocation)\n\treturn randomLocation\nend\n\n-- return boolean\nfunction Dungeon:AdjacentCellInDirectionIsVisited( location, direction )\n\tlocal target = self:GetTargetLocation(location, direction)\n\t\n\tif target == nil then\n\t\treturn false\n\tend\n\t\n\tif direction == DirectionType.North then\n\t\treturn self:getCell(target):getVisited()\n\telseif direction == DirectionType.West then\n\t\treturn self:getCell(target):getVisited()\n\telseif direction == DirectionType.South then\n\t\treturn self:getCell(target):getVisited()\n\telseif direction == DirectionType.East then\n\t\treturn self:getCell(target):getVisited()\n\telse\n\t\tprint('ERROR: InvalidOperationException (Dungeon:AdjacentCellInDirectionIsVisited)')\n\t\treturn nil\n\tend\nend\n\n-- return boolean\nfunction Dungeon:AdjacentCellInDirectionIsCorridor( location, direction )\n\tlocal target = self:GetTargetLocation(location, direction)\n\n\tif target == nil then\n\t\treturn false\n\tend\n\n\tif direction == DirectionType.North then\n\t\treturn self:getCell(target):getIsCorridor()\n\telseif direction == DirectionType.West then\n\t\treturn self:getCell(target):getIsCorridor()\n\telseif direction == DirectionType.South then\n\t\treturn self:getCell(target):getIsCorridor()\n\telseif direction == DirectionType.East then\n\t\treturn self:getCell(target):getIsCorridor()\n\telse\n\t\tprint('ERROR: InvalidOperationException (Dungeon:AdjacentCellInDirectionIsCorridor)')\n\t\treturn false\n\tend\nend\n\nfunction Dungeon:FlagCellAsVisited( location )\n\tif not Util:rectbound(location, self:getBounds() ) then\n\t\tprint('ERROR: Location is outside of Dungeon bounds')\n\tend\n\n\tif self:getCell(location):getVisited() then\n\t\tprint('ERROR: Location is already visited')\n\tend\n\n\tself:getCell(location):setVisited(true)\n\ttable.insert( self.visitedCells, location )\nend\n\n-- return point\nfunction Dungeon:GetRandomVisitedCell( location )\n\tif (#self.visitedCells == 0) then\n\t\tprint(\"ERROR: There are no visited cells to return.\")\n\t\treturn nil\n\tend\n\n\tlocal index = math.random(#self.visitedCells-1)\n\n\t-- Loop while the current cell is the visited cell\n\twhile (self.visitedCells[index].X == location.X and self.visitedCells[index].Y == location.Y) do\n\t\tindex = math.random(#self.visitedCells - 1)\n\tend\n\t\n\treturn self.visitedCells[index]\nend\n\n-- return point\nfunction Dungeon:CreateCorridor( location, direction )\n\tlocal targetLocation = self:CreateSide(location, direction, SideType.Empty)\n\t\n\tself:getCell(location):setIsCorridor(true)\t-- Set current location to corridor\n\tself:getCell(targetLocation):setIsCorridor(true) --Set target location to corridor\n\t\n\treturn targetLocation\nend\n\n\n-- return point\nfunction Dungeon:CreateWall( location, direction )\n\treturn self:CreateSide(location, direction, SideType.Wall)\nend\n\n-- return point\nfunction Dungeon:CreateDoor( location, direction )\n\treturn self:CreateSide(location, direction, SideType.Door)\nend\n\n-- return point\nfunction Dungeon:CreateSide( location, direction, sideType )\n\tlocal target = self:GetTargetLocation(location, direction)\n\n\tif (target == nil) then\n\t\tprint('ERROR: ArgumentException(\"There is no adjacent cell in the given direction\", \"location\")')\n\tend\n\n\tif direction == DirectionType.North then\n\t\tself:getCell(location):setNorthSide( sideType )\n\t\tself:getCell(target):setSouthSide( sideType )\n\telseif direction == DirectionType.South then\n\t\tself:getCell(location):setSouthSide( sideType )\n\t\tself:getCell(target):setNorthSide( sideType )\n\telseif direction == DirectionType.West then\n\t\tself:getCell(location):setWestSide( sideType )\n\t\tself:getCell(target):setEastSide( sideType )\n\telseif direction == DirectionType.East then\n\t\tself:getCell(location):setEastSide( sideType )\n\t\tself:getCell(target):setWestSide( sideType )\n\tend\n\n\treturn target\nend\n\n------------------------------------------------------\n-- helper functions\n------------------------------------------------------\n\n-- return boolean\nfunction Dungeon:AllCellsAreVisited()\n\treturn #self.visitedCells == ( self:getWidth() * self:getHeight() )\nend\n\n-- IEnumerable<Point>\nfunction Dungeon:DeadEndCellLocations()\n\tlocal deadEndpointList = {}\n\tfor key,point in pairs( self:getCellLocations() ) do\n\t\tif self:getCell(point):getIsDeadEnd() then\n\t\t\ttable.insert(deadEndpointList, point)\n\t\tend\n\tend\n\n\treturn deadEndpointList\nend\n\nfunction Dungeon:CorridorCellLocations()\n\tlocal corridorPointList = {}\n\tfor key,point in pairs( self:getCellLocations() ) do\n\t\tif self:getCell(point):getIsCorridor() then\n\t\t\ttable.insert(corridorPointList, point)\n\t\tend\n\tend\n\n\treturn corridorPointList\nend\n\nreturn Dungeon\n"
  },
  {
    "path": "astray/init.lua",
    "content": "local BASE = (...) .. '.'\n\nreturn {\n\tAstray     = require(BASE .. 'astray'),\n\tRoomGenerator   = require(BASE .. 'roomgenerator'),\n}\n"
  },
  {
    "path": "astray/map.lua",
    "content": "--[[\nCopyright (c) <''2014''> <''Florian Fischer''>\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n   1. The origin of this software must not be misrepresented; you must not\n   claim that you wrote the original software. If you use this software\n   in a product, an acknowledgment in the product documentation would be\n   appreciated but is not required.\n\n   2. Altered source versions must be plainly marked as such, and must not be\n   misrepresented as being the original software.\n\n   3. This notice may not be removed or altered from any source\n   distribution.\n]]--\n\nlocal PATH = (...):match(\"(.-)[^%.]+$\")\n\nlocal class = require(PATH .. 'MiddleClass')\nlocal Util = require(PATH .. 'util')\nlocal Point = require(PATH .. 'point')\nlocal Cell = require(PATH .. 'cell')\n\n-- Class\nlocal Map = class(\"Map\")\n\nfunction Map:initialize( width, height )\n    self.cells = {}\n    self.bounds = { X=0, Y=0, Width=width, Height=height }\n    self.cellLocations = {} -- Add cache\n\n    -- Initialize cells and cache locations\n    for x = 0, self.bounds.Width-1 do\n        self.cells[x] = {}\n        for y = 0, self.bounds.Height-1 do\n            self.cells[x][y] = Cell:new()\n            table.insert(self.cellLocations, Point:new(x,y))\n        end\n    end\nend\n\nfunction Map:HasAdjacentCellInDirection( location, direction)\n\t-- Check that the location falls within the bounds of the map\n\tif not Util:rectbound(location, self.bounds) then\n\t\tprint('ERROR: Map:HasAdjacentCellInDirection: not rectbound!!')\n\t\treturn false\n\tend\n\n\t-- Check if there is an adjacent cell in the direction\n\tif direction == DirectionType.North then\n\t\treturn location.Y > 0\n\telseif direction == DirectionType.South then\n\t\treturn location.Y < (self:getHeight() - 1)\n\telseif direction == DirectionType.West then\n\t\treturn location.X > 0\n\telseif direction == DirectionType.East then\n\t\treturn location.X < (self:getWidth() - 1)\n\telse\n\t\tprint('ERROR: Map:HasAdjacentCellInDirection')\n\t\treturn false\n\tend\nend\n\n-- return point\nfunction Map:GetTargetLocation( location, direction)\n\tif not self:HasAdjacentCellInDirection(location, direction) then return nil end\n\n\tif direction == DirectionType.North then\n\t\treturn Point:new(location.X, location.Y - 1)\n\telseif direction == DirectionType.West then\n\t\treturn Point:new(location.X - 1, location.Y)\n\telseif direction == DirectionType.South then\n\t\treturn Point:new(location.X, location.Y + 1)\n\telseif direction == DirectionType.East then\n\t\treturn Point:new(location.X + 1, location.Y)\n\telse\n\t\tprint('ERROR: InvalidOperationException (Map:GetTargetLocation)')\n\t\treturn nil\n\tend\nend\n\n------------------------------------------------------\n-- helper functions\n------------------------------------------------------\n\nfunction Map:getBounds()\n\treturn self.bounds\nend\n\nfunction Map:getCell( point )\n\treturn self.cells[point.X][point.Y]\nend\nfunction Map:setCell( point, value )\n\tself.cells[point.X][point.Y] = value\nend\n\nfunction Map:getWidth()\n\treturn self.bounds.Width\nend\nfunction Map:getHeight()\n\treturn self.bounds.Height\nend\n\nfunction Map:getCellLocations()\n    return self.cellLocations\nend\n\nreturn Map\n"
  },
  {
    "path": "astray/point.lua",
    "content": "--[[\nCopyright (c) <''2014''> <''Florian Fischer''>\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n   1. The origin of this software must not be misrepresented; you must not\n   claim that you wrote the original software. If you use this software\n   in a product, an acknowledgment in the product documentation would be\n   appreciated but is not required.\n\n   2. Altered source versions must be plainly marked as such, and must not be\n   misrepresented as being the original software.\n\n   3. This notice may not be removed or altered from any source\n   distribution.\n]]--\n\nlocal PATH = (...):match(\"(.-)[^%.]+$\")\n\nlocal class = require(PATH .. 'MiddleClass')\n\n-- Class\nlocal Point = class(\"Point\")\n\nfunction Point:initialize( x, y )\n\tself.X = x\n\tself.Y = y\nend\n\nreturn Point\n"
  },
  {
    "path": "astray/room.lua",
    "content": "--[[\nCopyright (c) <''2014''> <''Florian Fischer''>\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n   1. The origin of this software must not be misrepresented; you must not\n   claim that you wrote the original software. If you use this software\n   in a product, an acknowledgment in the product documentation would be\n   appreciated but is not required.\n\n   2. Altered source versions must be plainly marked as such, and must not be\n   misrepresented as being the original software.\n\n   3. This notice may not be removed or altered from any source\n   distribution.\n]]--\n\nlocal PATH = (...):match(\"(.-)[^%.]+$\")\n\nlocal class = require(PATH .. 'MiddleClass')\nlocal Cell = require(PATH .. 'cell')\nlocal Map = require(PATH .. 'map')\n\n-- Class\nlocal Room = class(\"Room\", Map)\n\nfunction Room:initialize( width, height )\n\tMap.initialize(self, width, height) -- invoking the superclass' initializer\nend\n\nfunction Room:InitializeRoomCells()\n\tfor key,location in pairs( self:getCellLocations() ) do\n\t\tlocal cell = Cell:new()\n\t\t\n\t\tif (location.X == self.bounds.X) then\n\t\t\tcell.WestSide = SideType.Wall\n\t\telse\n\t\t\tcell.WestSide = SideType.Empty\n\t\tend\n\t\t\n\t\tif (location.X == self.bounds.Width - 1) then\n\t\t\tcell.EastSide = SideType.Wall\n\t\telse\n\t\t\tcell.EastSide = SideType.Empty\n\t\tend\n\t\t\n\t\tif (location.Y == self.bounds.Y) then\n\t\t\tcell.NorthSide = SideType.Wall\n\t\telse\n\t\t\tcell.NorthSide = SideType.Empty\n\t\tend\n\t\t\n\t\tif (location.Y == self.bounds.Height - 1) then\n\t\t\tcell.SouthSide = SideType.Wall\n\t\telse\n\t\t\tcell.SouthSide = SideType.Empty\n\t\tend\n\n\t\tself:setCell( location, cell )\n\tend\nend\n\nfunction Room:SetLocation( location )\n\tself.bounds = { X=location.X, Y=location.Y, Width=self.bounds.Width, Height=self.bounds.Height }\nend\n\nreturn Room\n"
  },
  {
    "path": "astray/roomgenerator.lua",
    "content": "--[[\nCopyright (c) <''2014''> <''Florian Fischer''>\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n   1. The origin of this software must not be misrepresented; you must not\n   claim that you wrote the original software. If you use this software\n   in a product, an acknowledgment in the product documentation would be\n   appreciated but is not required.\n\n   2. Altered source versions must be plainly marked as such, and must not be\n   misrepresented as being the original software.\n\n   3. This notice may not be removed or altered from any source\n   distribution.\n]]--\n\nlocal PATH = (...):match(\"(.-)[^%.]+$\")\n\nlocal class = require(PATH .. 'MiddleClass')\nlocal Util = require(PATH .. 'util')\nlocal Point = require(PATH .. 'point')\nlocal Room = require(PATH .. 'room')\n\n-- Class\nlocal RoomGenerator = class(\"RoomGenerator\")\n\nfunction RoomGenerator:initialize(noOfRoomsToPlace, minRoomWidth, maxRoomWidth, minRoomHeight, maxRoomHeight)\n\tself.noOfRoomsToPlace = noOfRoomsToPlace or 10\n\tself.minRoomWidth = minRoomWidth or 1\n\tself.maxRoomWidth = maxRoomWidth or 6\n\tself.minRoomHeight = minRoomHeight or 1\n\tself.maxRoomHeight = maxRoomHeight or 6\nend\n\nfunction RoomGenerator:CreateRoom()\n\tlocal room = Room:new( math.random(self.minRoomWidth, self.maxRoomWidth), math.random(self.minRoomHeight, self.maxRoomHeight) )\n\n\troom:InitializeRoomCells()\n\treturn room\nend\n\nfunction RoomGenerator:PlaceRooms( dungeon )\n\t-- Loop for the amount of rooms to place\n\tfor roomCounter = 0, self.noOfRoomsToPlace-1 do\n\t\tlocal room = self:CreateRoom()\n\t\t\n\t\tlocal bestRoomPlacementScore = MaxValue\n\t\tlocal bestRoomPlacementLocation = nil\n\n\t\tfor key,currentRoomPlacementLocation in pairs( dungeon:CorridorCellLocations() ) do\n\t\t\tlocal currentRoomPlacementScore = self:CalculateRoomPlacementScore( currentRoomPlacementLocation, room, dungeon )\n\n\t\t\tif (currentRoomPlacementScore < bestRoomPlacementScore) then\n\t\t\t\tbestRoomPlacementScore = currentRoomPlacementScore\n\t\t\t\tbestRoomPlacementLocation = currentRoomPlacementLocation\n\t\t\tend\n\t\tend\n\n\t\t-- Create room at best room placement cell\n\t\tif bestRoomPlacementLocation ~= nil then\n\t\t\tself:PlaceRoom( bestRoomPlacementLocation, room, dungeon )\n\t\tend\n\tend\nend\n\nfunction RoomGenerator:CalculateRoomPlacementScore( location, room, dungeon )\n\t-- Check if the room at the given location will fit inside the bounds of the map\n\tif Util:rectbound( {X=location.X, Y=location.Y, Width=room:getWidth(), Height=room:getHeight()}, dungeon:getBounds() ) then\n\t\tlocal roomPlacementScore = 0\n\n\t\t-- Loop for each cell in the room\n\t\tfor key,roomLocation in pairs( room:getCellLocations() ) do\n\t\t\t--Translate the room cell location to its location in the dungeon\n\t\t\tlocal dungeonLocation = Point:new(location.X + roomLocation.X, location.Y + roomLocation.Y);\n\n\t\t\t-- Add 1 point for each adjacent corridor to the cell\n\t\t\tif dungeon:AdjacentCellInDirectionIsCorridor(dungeonLocation, DirectionType.North) then roomPlacementScore = roomPlacementScore + 1 end\n\t\t\tif dungeon:AdjacentCellInDirectionIsCorridor(dungeonLocation, DirectionType.South) then roomPlacementScore = roomPlacementScore + 1 end\n\t\t\tif dungeon:AdjacentCellInDirectionIsCorridor(dungeonLocation, DirectionType.West) then roomPlacementScore = roomPlacementScore + 1 end\n\t\t\tif dungeon:AdjacentCellInDirectionIsCorridor(dungeonLocation, DirectionType.East) then roomPlacementScore = roomPlacementScore + 1 end\n\n\t\t\t-- Add 3 points if the cell overlaps an existing corridor\n\t\t\tif (dungeon:getCell(dungeonLocation):getIsCorridor()) then roomPlacementScore = roomPlacementScore + 3 end\n\n\t\t\t-- Add 100 points if the cell overlaps any existing room cells\n\t\t\tfor key,dungeonRoom in pairs(dungeon.rooms) do\n\t\t\t\tif Util:rectbound( dungeonLocation, dungeonRoom:getBounds() ) then\n\t\t\t\t\troomPlacementScore = roomPlacementScore + 100\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\t\t\n\t\treturn roomPlacementScore\n\telse\n\t\treturn MaxValue\n\tend\nend\n\nfunction RoomGenerator:PlaceRoom( location, room, dungeon )\n\t-- Offset the room origin to the new location\n\troom:SetLocation(location)\n\n\t-- Loop for each cell in the room\n\tfor key,roomLocation in pairs( room:getCellLocations() ) do\n\t\t-- Translate the room cell location to its location in the dungeon\n\t\tlocal dungeonLocation = Point:new(location.X + roomLocation.X, location.Y + roomLocation.Y)\n\n\t\tdungeon:getCell(dungeonLocation):setNorthSide( room:getCell(roomLocation):getNorthSide() )\n\t\tdungeon:getCell(dungeonLocation):setSouthSide( room:getCell(roomLocation):getSouthSide() )\n\t\tdungeon:getCell(dungeonLocation):setWestSide( room:getCell(roomLocation):getWestSide() )\n\t\tdungeon:getCell(dungeonLocation):setEastSide( room:getCell(roomLocation):getEastSide() )\n\n\t\t-- Create room walls on map (either side of the wall)\n\t\tif (roomLocation.X == 0) and (dungeon:HasAdjacentCellInDirection(dungeonLocation, DirectionType.West)) then\n\t\t\tdungeon:CreateWall( dungeonLocation, DirectionType.West )\n\t\tend\n\t\tif (roomLocation.X == room:getWidth() - 1) and (dungeon:HasAdjacentCellInDirection(dungeonLocation, DirectionType.East)) then\n\t\t\tdungeon:CreateWall( dungeonLocation, DirectionType.East )\n\t\tend\n\t\tif (roomLocation.Y == 0) and (dungeon:HasAdjacentCellInDirection(dungeonLocation, DirectionType.North)) then\n\t\t\tdungeon:CreateWall( dungeonLocation, DirectionType.North )\n\t\tend\n\t\tif (roomLocation.Y == room:getHeight() - 1) and (dungeon:HasAdjacentCellInDirection(dungeonLocation, DirectionType.South)) then\n\t\t\tdungeon:CreateWall( dungeonLocation, DirectionType.South )\n\t\tend\n\tend\n\n\tdungeon:AddRoom(room)\nend\n\n-- TODO: check if Door has adjancent Walls beside!\nfunction RoomGenerator:PlaceDoors( dungeon )\n\tfor key,room in pairs(dungeon.rooms) do\n\t\tlocal hasNorthDoor = false\n\t\tlocal hasSouthDoor = false\n\t\tlocal hasWestDoor = false\n\t\tlocal hasEastDoor = false\n\t\t\n\t\tfor key,cellLocation in pairs( room:getCellLocations() ) do\n\t\t\t-- Translate the room cell location to its location in the dungeon\n\t\t\tlocal dungeonLocation = Point:new(room:getBounds().X + cellLocation.X, room:getBounds().Y + cellLocation.Y)\n\n\t\t\t-- Check if we are on the west boundary of our room\n\t\t\t-- and if there is a corridor to the west\n\t\t\tif (cellLocation.X == 0) and\n\t\t\t\t(dungeon:AdjacentCellInDirectionIsCorridor(dungeonLocation, DirectionType.West)) and\n\t\t\t\t(not hasWestDoor) then\n\t\t\t\t\tdungeon:CreateDoor(dungeonLocation, DirectionType.West)\n\t\t\t\t\thasWestDoor = true\n\t\t\tend\n\n\t\t\t-- Check if we are on the east boundary of our room\n\t\t\t-- and if there is a corridor to the east\n\t\t\tif (cellLocation.X == room:getWidth() - 1) and\n\t\t\t\t(dungeon:AdjacentCellInDirectionIsCorridor(dungeonLocation, DirectionType.East)) and\n\t\t\t\t(not hasEastDoor) then\n\t\t\t\t\tdungeon:CreateDoor(dungeonLocation, DirectionType.East)\n\t\t\t\t\thasEastDoor = true\n\t\t\tend\n\n\t\t\t-- Check if we are on the north boundary of our room \n\t\t\t-- and if there is a corridor to the north\n\t\t\tif (cellLocation.Y == 0) and\n\t\t\t\t(dungeon:AdjacentCellInDirectionIsCorridor(dungeonLocation, DirectionType.North)) and\n\t\t\t\t(not hasNorthDoor) then\n\t\t\t\t\tdungeon:CreateDoor(dungeonLocation, DirectionType.North)\n\t\t\t\t\thasNorthDoor = true\n\t\t\tend\n\n\t\t\t-- Check if we are on the south boundary of our room \n\t\t\t-- and if there is a corridor to the south\n\t\t\tif (cellLocation.Y == room:getHeight() - 1) and\n\t\t\t\t(dungeon:AdjacentCellInDirectionIsCorridor(dungeonLocation, DirectionType.South)) and\n\t\t\t\t(not hasSouthDoor) then\n\t\t\t\t\tdungeon:CreateDoor(dungeonLocation, DirectionType.South)\n\t\t\t\t\thasSouthDoor = true\n\t\t\tend\n\t\t\t\n\t\tend\n\tend\nend\n\n------------------------------------------------------\n-- helper functions\n------------------------------------------------------\nfunction RoomGenerator:getNoOfRoomsToPlace()\n\treturn self.noOfRoomsToPlace\nend\nfunction RoomGenerator:setNoOfRoomsToPlace( noOfRoomsToPlace )\n\tself.noOfRoomsToPlace = noOfRoomsToPlace\nend\n\nfunction RoomGenerator:MinRoomWidth()\n\treturn self.minRoomWidth\nend\nfunction RoomGenerator:MinRoomWidth( minRoomWidth )\n\tself.minRoomWidth = minRoomWidth\nend\n\nfunction RoomGenerator:MaxRoomWidth()\n\treturn self.maxRoomWidth\nend\nfunction RoomGenerator:MaxRoomWidth( maxRoomWidth )\n\tself.maxRoomWidth = maxRoomWidth\nend\n\nfunction RoomGenerator:MinRoomHeight()\n\treturn self.minRoomHeight\nend\nfunction RoomGenerator:MinRoomHeight( minRoomHeight )\n\tself.minRoomHeight = minRoomHeight\nend\n\nfunction RoomGenerator:MaxRoomHeight()\n\treturn self.maxRoomHeight\nend\nfunction RoomGenerator:MaxRoomHeight( maxRoomHeight )\n\tself.maxRoomHeight = maxRoomHeight\nend\n\nreturn RoomGenerator\n"
  },
  {
    "path": "astray/util.lua",
    "content": "--[[\nCopyright (c) <''2014''> <''Florian Fischer''>\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n   1. The origin of this software must not be misrepresented; you must not\n   claim that you wrote the original software. If you use this software\n   in a product, an acknowledgment in the product documentation would be\n   appreciated but is not required.\n\n   2. Altered source versions must be plainly marked as such, and must not be\n   misrepresented as being the original software.\n\n   3. This notice may not be removed or altered from any source\n   distribution.\n]]--\n\nlocal PATH = (...):match(\"(.-)[^%.]+$\")\n\nlocal class = require(PATH .. 'MiddleClass')\n\n-- Class\nlocal Util = class(\"Util\")\n\n-- option1: check if point is in rect bound\n-- option2: check if bound is in rect bound\nfunction Util:rectbound( bounds1, bounds2)\n\tif bounds1.Width and bounds1.Height then\n\t\tlocal inbound = false\n\t\tlocal x1,y1 = bounds1.X, bounds1.Y\n\t\tlocal width1, height1 = bounds1.Width, bounds1.Height\n\t\tlocal x2,y2 = bounds2.X, bounds2.Y\n\t\tlocal width2, height2 = bounds2.Width, bounds2.Height\n\t\t\n\t\tif x1 >= x2 and x1 <= x2+width2 and\n\t\t   x1+width1 <= x2+width2 and\n\t\t   y1 >= y2 and y1 <= y2+height2 and\n\t\t   y1+height1 <= y2+height2 then\n\t\t\tinbound = true\n\t\tend\n\t\treturn inbound\n\telse\n\t\tlocal inbound = false\n\t\tlocal x,y = bounds1.X, bounds1.Y\n\t\tlocal x2 = bounds2.X\n\t\tlocal y2 = bounds2.Y\n\t\tlocal width2, height2 = bounds2.Width, bounds2.Height\n\t\tif x >= x2 and x <= x2+width2 and y >= y2 and y <= y2+height2 then\n\t\t\tinbound = true\n\t\tend\n\t\treturn inbound\n\tend\nend\n\nfunction Util:tablecontains(table, element)\n  for _, value in pairs(table) do\n    if value == element then\n      return true\n    end\n  end\n  return false\nend\n\nreturn Util:new()\n"
  },
  {
    "path": "conf.lua",
    "content": "function love.conf(t)\n    t.title = \"Astray - random dungeon generation library\"\t\t-- The title of the window the game is in (string)\n    t.author = \"Florian Fischer^SiENcE\" -- The author of the game (string)\n    t.identity = \"astray\"\t\t-- The name of the save directory (string)\n--    t.version = 0             -- The LÖVE version this game was made for (number)\n    t.console = true            -- Attach a console (boolean, Windows only)\n\tt.screen = {}\n    t.screen.width = 800\t\t-- The window width (number)\n    t.screen.height = 600     \t-- The window height (number)\n    t.screen.fullscreen = false -- Enable fullscreen (boolean)\n    t.screen.vsync = false      -- Enable vertical sync (boolean)\n    t.screen.fsaa = 0           -- The number of FSAA-buffers (number)\n    t.modules.joystick = false   -- Enable the joystick module (boolean)\n    t.modules.audio = true      -- Enable the audio module (boolean)\n    t.modules.keyboard = true   -- Enable the keyboard module (boolean)\n    t.modules.event = true      -- Enable the event module (boolean)\n    t.modules.image = true      -- Enable the image module (boolean)\n    t.modules.graphics = true   -- Enable the graphics module (boolean)\n    t.modules.timer = true      -- Enable the timer module (boolean)\n    t.modules.mouse = true      -- Enable the mouse module (boolean)\n    t.modules.sound = true      -- Enable the sound module (boolean)\n    t.modules.physics = false    -- Enable the physics module (boolean)\nend\n"
  },
  {
    "path": "main.lua",
    "content": "--[[\nCopyright (c) <''2024''> <''Florian Fischer''>\n]]--\n\nlocal astray = require('astray')\n\nlocal symbols = {Wall='#', Empty=' ', DoorN='|', DoorS='|', DoorE='-', DoorW='-'}\n\nfunction drawdungeon(tiles, startx, starty, width, height)\n\t-- we have to add +1 for the 0 rows\n\tprint(\"Map size=\", #tiles+1-startx, #tiles[1]+1-starty )\n\n    for y = startx, height do\n        local line = ''\n\t\tfor x = starty, width do\n\t\t\tline = line .. tiles[y][x]\n\t\tend\n\t\tprint(line)\n\tend\n\tprint('')\nend\n\nfunction updatewalls(tiles, width, height)\n\tlocal block = false\n    for y = 0, height do\n\t\tfor x = 0, width do\n\t\t\tif tiles[x][y] == '#' then\n\t\t\t\tif block then\n\t\t\t\t\ttiles[x][y] = '#'\n\t\t\t\telse\n\t\t\t\t\ttiles[x][y] = 'O'\n\t\t\t\tend\n\t\t\tend\n\t\t\tblock = not block\n\t\tend\n\tend\n\treturn tiles\nend\n\nfunction fixTiles( tiles, width, height )\n\tlocal fixed_tiles = {}\n\tfor y = 0, height do\n\t\tfor x = 0, width do\n\t\t\tif not fixed_tiles[y+1] then fixed_tiles[y+1] = {} end\n\t\t\tfixed_tiles[y+1][x+1] = tiles[y][x]\n\t\tend\n\tend\n\treturn fixed_tiles\nend\n\nprint('Astay Sample\\n')\n\nprint('Automatic\\n------------------------------------------\\n')\n\n-- NOTE: This maze generator only accepts even number and can only generate uneven maps!\n-- So when you input 40, 40 you get a -> 39,39 map.\nlocal height, width = 40, 40\n\n--\tFunction Header: Astray:new(width/2-1, height/2-1, changeDirectionModifier (1-30), sparsenessModifier (25-70), deadEndRemovalModifier (70-99) ) | RoomGenerator:new(rooms, minWidth, maxWidth, minHeight, maxHeight)\nlocal generator = astray.Astray:new( height/2-1, width/2-1, 30, 70, 50, astray.RoomGenerator:new(4, 2, 4, 2, 4) )\n\n-- original setup\n--local generator = astray.Astray:new( 25, 25, 30, 70, 50, astray.RoomGenerator:new(10,1,5,1,5) )\n\nlocal dungeon = generator:Generate()\n--local tiles = generator:CellToTiles(dungeon, symbols)\nlocal tiles = generator:CellToTiles(dungeon)\n-- to alternate between two wall-types\nupdatewalls(tiles, #tiles, #tiles[1] )\n-- draw on console\ndrawdungeon(tiles, 0, 0, #tiles, #tiles[1] )\n-- fix array index to 1 (instead of 0)\nlocal fixed_tiles = fixTiles( tiles, #tiles, #tiles[1] )\n-- draw on console\ndrawdungeon(fixed_tiles, 1, 1, #fixed_tiles, #fixed_tiles[1] )\n"
  }
]