diff --git a/build/te4core.lua b/build/te4core.lua index 2ea0f26fbf5133dbbf06403729fd7af3593d881f..3a8348ec0d3ed46778c1b5e4910af95e6eb34c0e 100644 --- a/build/te4core.lua +++ b/build/te4core.lua @@ -37,7 +37,7 @@ project "TEngine" if _OPTIONS.steam then files { "../steamworks/luasteam.c", } end - links { "physfs", "lua".._OPTIONS.lua, "fov", "luasocket", "luaprofiler", "lpeg", "tcodimport", "lxp", "expatstatic", "luamd5", "luazlib", "luabitop", "te4-bzip" } + links { "physfs", "lua".._OPTIONS.lua, "fov", "luasocket", "luaprofiler", "lpeg", "tcodimport", "lxp", "expatstatic", "luamd5", "luazlib", "luabitop", "te4-bzip", "te4-wfc" } if _OPTIONS.discord then defines { "DISCORD_TE4" } end defines { "_DEFAULT_VIDEOMODE_FLAGS_='SDL_HWSURFACE|SDL_DOUBLEBUF'" } defines { [[TENGINE_HOME_PATH='".t-engine"']], "TE4CORE_VERSION="..TE4CORE_VERSION } @@ -50,6 +50,7 @@ project "TEngine" if _OPTIONS.relpath == "64" then defines{"TE4_RELPATH64"} end links { "m" } + cppconfig() if _OPTIONS.no_rwops_size then defines{"NO_RWOPS_SIZE"} end @@ -171,13 +172,14 @@ project "physfs" files { "../src/physfs/platform/windows.c", } configuration "macosx" files { "../src/physfs/platform/macosx.c", "../src/physfs/platform/posix.c", } - includedirs { "/Library/Frameworks/SDL.framework/Headers" } + includedirs { "/Library/Frameworks/SDL2.framework/Headers" } if _OPTIONS.lua == "default" then project "luadefault" kind "StaticLib" language "C" targetname "lua" + buildoptions{ "-O2" } files { "../src/lua/*.c", } elseif _OPTIONS.lua == "jit2" then @@ -359,6 +361,7 @@ elseif _OPTIONS.lua == "jit2" then language "C" targetname "lua" links { "buildvm" } + buildoptions { "-O2", "-fomit-frame-pointer" } files { "../src/luajit2/src/*.c", "../src/luajit2/src/*.s", "../src/luajit2/src/lj_vm.s", "../src/luajit2/src/lj_bcdef.h", "../src/luajit2/src/lj_ffdef.h", "../src/luajit2/src/lj_ffdef.h", "../src/luajit2/src/lj_libdef.h", "../src/luajit2/src/lj_recdef.h", "../src/luajit2/src/lj_folddef.h" } excludes { "../src/luajit2/src/buildvm*.c", "../src/luajit2/src/luajit.c", "../src/luajit2/src/ljamalg.c" } @@ -423,6 +426,7 @@ project "luasocket" kind "StaticLib" language "C" targetname "luasocket" + buildoptions { "-O2" } configuration "not windows" files { @@ -461,6 +465,7 @@ project "fov" kind "StaticLib" language "C" targetname "fov" + buildoptions { "-O2" } files { "../src/fov/*.c", } @@ -468,6 +473,7 @@ project "lpeg" kind "StaticLib" language "C" targetname "lpeg" + buildoptions { "-O2" } files { "../src/lpeg/*.c", } @@ -475,6 +481,7 @@ project "luaprofiler" kind "StaticLib" language "C" targetname "luaprofiler" + buildoptions { "-O2" } files { "../src/luaprofiler/*.c", } @@ -482,6 +489,7 @@ project "tcodimport" kind "StaticLib" language "C" targetname "tcodimport" + buildoptions { "-O2" } files { "../src/libtcod_import/*.c", } @@ -490,6 +498,7 @@ project "expatstatic" language "C" targetname "expatstatic" defines{ "HAVE_MEMMOVE" } + buildoptions { "-O2" } files { "../src/expat/*.c", } @@ -497,6 +506,7 @@ project "lxp" kind "StaticLib" language "C" targetname "lxp" + buildoptions { "-O2" } files { "../src/lxp/*.c", } @@ -504,6 +514,7 @@ project "luamd5" kind "StaticLib" language "C" targetname "luamd5" + buildoptions { "-O2" } files { "../src/luamd5/*.c", } @@ -511,6 +522,7 @@ project "luazlib" kind "StaticLib" language "C" targetname "luazlib" + buildoptions { "-O2" } files { "../src/lzlib/*.c", } @@ -518,6 +530,7 @@ project "luabitop" kind "StaticLib" language "C" targetname "luabitop" + buildoptions { "-O2" } files { "../src/luabitop/*.c", } @@ -525,9 +538,20 @@ project "te4-bzip" kind "StaticLib" language "C" targetname "te4-bzip" + buildoptions { "-O2" } files { "../src/bzip2/*.c", } +project "te4-wfc" + kind "StaticLib" + language "C++" + targetname "te4-wfc" + buildoptions { "-O3" } + buildoptions { "-std=c++11" } + cppconfig() + + files { "../src/wfc/*.cpp", } + if _OPTIONS['web-awesomium'] and not _OPTIONS.wincross then project "te4-web" kind "SharedLib" @@ -547,6 +571,9 @@ project "te4-web" language "C++" targetname "te4-web" + buildoptions { "-O3", "-std=c++11" } + cppconfig("web") + if _OPTIONS.relpath=="32" then linkoptions{"-Wl,-rpath -Wl,\\\$\$ORIGIN "} end if _OPTIONS.relpath=="64" then linkoptions{"-Wl,-rpath -Wl,\\\$\$ORIGIN "} end @@ -554,8 +581,8 @@ project "te4-web" configuration "macosx" defines { 'SELFEXE_MACOSX' } - libdirs {"/users/tomedev/downloads/cef_binary_3.1547.1597_macosx64/xcodebuild/Release/", "/users/tomedev/downloads/cef_binary_3.1547.1597_macosx64/Release/"} - includedirs {"/users/tomedev/downloads/cef_binary_3.1547.1597_macosx64/include/", "/users/tomedev/downloads/cef_binary_3.1547.1597_macosx64/"} + libdirs {"/Users/darkmac/libs/CEF/cef_binary_3.1547.1597_macosx64/xcodebuild/Release/", "/Users/darkmac/libs/CEF/cef_binary_3.1547.1597_macosx64/Release/"} + includedirs {"/Users/darkmac/libs/CEF/cef_binary_3.1547.1597_macosx64/include/", "/Users/darkmac/libs/CEF/cef_binary_3.1547.1597_macosx64/"} links { "cef", "cef_dll_wrapper" } configuration "windows" @@ -574,6 +601,10 @@ project "cef3spawn" language "C++" targetname "cef3spawn" + buildoptions { "-O3" } + buildoptions { "-std=c++11" } + cppconfig("web") + includedirs {"../src/web-cef3/", } files { "../src/web-cef3/spawn.cpp", @@ -581,8 +612,8 @@ project "cef3spawn" configuration "macosx" defines { 'SELFEXE_MACOSX' } - libdirs {"/users/tomedev/downloads/cef_binary_3.1547.1597_macosx64/xcodebuild/Release/", "/users/tomedev/downloads/cef_binary_3.1547.1597_macosx64/Release/"} - includedirs {"/users/tomedev/downloads/cef_binary_3.1547.1597_macosx64/include/", "/users/tomedev/downloads/cef_binary_3.1547.1597_macosx64/"} + libdirs {"/Users/darkmac/libs/CEF/cef_binary_3.1547.1597_macosx64/xcodebuild/Release/", "/Users/darkmac/libs/CEF/cef_binary_3.1547.1597_macosx64/Release/"} + includedirs {"/Users/darkmac/libs/CEF/cef_binary_3.1547.1597_macosx64/include/", "/Users/darkmac/libs/CEF/cef_binary_3.1547.1597_macosx64/"} links { "cef", "cef_dll_wrapper" } configuration "linux" diff --git a/game/engines/default/engine/Zone.lua b/game/engines/default/engine/Zone.lua index 01c8981f62be8bfd6a60977d01be41dd621c16ac..5cd522de62dbd1afa661b159728040c54dfba5b0 100644 --- a/game/engines/default/engine/Zone.lua +++ b/game/engines/default/engine/Zone.lua @@ -1176,5 +1176,7 @@ function _M:runPostGeneration(level) end end - level.map.room_map = nil -- delete the room map + if not config.settings.cheat then + level.map.room_map = nil -- delete the room map, but keep it for debugging + end end diff --git a/game/engines/default/engine/BSP.lua b/game/engines/default/engine/algorithms/BSP.lua similarity index 98% rename from game/engines/default/engine/BSP.lua rename to game/engines/default/engine/algorithms/BSP.lua index 20fda92040918e5138333e002999f877fee35438..21091e93c47e0059e613e3a7044cbe2d63766c3f 100644 --- a/game/engines/default/engine/BSP.lua +++ b/game/engines/default/engine/algorithms/BSP.lua @@ -21,7 +21,7 @@ require "engine.class" --- Abstract binary space partitioning -- Can be used to generator levels and so on --- @classmod engine.BSP +-- @classmod engine.algorithms.BSP module(..., package.seeall, class.make) --- Init diff --git a/game/engines/default/engine/algorithms/MST.lua b/game/engines/default/engine/algorithms/MST.lua new file mode 100644 index 0000000000000000000000000000000000000000..8eab83d453131e7071678315cc42fa75ebf4b127 --- /dev/null +++ b/game/engines/default/engine/algorithms/MST.lua @@ -0,0 +1,85 @@ +-- TE4 - T-Engine 4 +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +local unionfind = require "algorithms.unionfind" + +--- Find a MST (Minimum Spanning Tree), aka the smallest fully connected graph, from a graph presented as a list of weighted edges +-- @classmod engine.algorithms.MST +module(..., package.seeall, class.make) + +----------------------------------------------------------- +-- Small Edge class +----------------------------------------------------------- +local Edge_t +Edge_t = { __index = { + hash = function(e) + local s1, s2 = tostring(e.from), tostring(e.to) + if s2 < s1 then s2, s1 = s1, s2 end + return s1..":"..s2 + end, +} } + +function Edge(r1, r2, cost, data) + return setmetatable({from=r1, to=r2, cost=cost, data=data}, Edge_t) +end +----------------------------------------------------------- + +function _M:init() + self.mst = {} + self.edges = {} + self.sorted_edges = {} +end + +function _M:edge(r1, r2, cost, data) + local e + if getmetatable(r1) == Edge_t then e = r1 + else e = Edge(r1, r2, cost, data) end + self.edges[e:hash()] = e +end + +function _M:run() + self.sorted_edges = table.values(self.edges) + table.sort(self.sorted_edges, "cost") + + -- Find the MST graph + local uf = unionfind.create() + for _, edge in ipairs(self.sorted_edges) do + -- Skip this edge to avoid creating a cycle in MST + if not uf:connected(edge.from, edge.to) then + -- Include this edge + uf:union(edge.from, edge.to) + + -- Add it to the final graph + self.mst[edge:hash()] = edge + + -- Remove it from the remaining edges + self.edges[edge:hash()] = nil + end + end + return self.mst +end + +function _M:fattenRandom(nb_adds) + while nb_adds > 0 and next(self.edges) do + local edge = rng.table(table.values(self.edges)) + self.edges[edge:hash()] = nil + self.mst[edge:hash()] = edge + nb_adds = nb_adds - 1 + end +end diff --git a/game/engines/default/engine/generator/actor/OnSpots.lua b/game/engines/default/engine/generator/actor/OnSpots.lua index 45146e327ac78b5469d6041575d0c2b1ceee9ba5..78552ca2a6e6c6fc984ad4a8614f0bc7fba71afb 100644 --- a/game/engines/default/engine/generator/actor/OnSpots.lua +++ b/game/engines/default/engine/generator/actor/OnSpots.lua @@ -30,10 +30,27 @@ function _M:init(zone, map, level, spots) self.on_spot_chance = data.on_spot_chance or 70 self.spot_radius = data.spot_radius or 5 self.spot_filters = data.spot_filters or {} + self.nb_spots = data.nb_spots or 2 + + if self.nb_spots == true then + self.nb_spots = 0 + for _, spot in ipairs(level.spots) do + if #self.spot_filters == 0 then + self.nb_spots = self.nb_spots + 1 + else + for _, filter in ipairs(self.spot_filters) do + if zone:checkFilter(spot, filter) then + self.nb_spots = self.nb_spots + 1 + break + end + end + end + end + end -- Pick a number of spots self.genspots = {} - for i = 1, self.nb_spots or 2 do + for i = 1, self.nb_spots do local spot = self.level:pickSpot(rng.table(self.spot_filters)) if spot then self.genspots[#self.genspots+1] = spot end end @@ -41,13 +58,15 @@ end function _M:getSpawnSpot(m) -- Spawn near a spot - if rng.percent(self.on_spot_chance) then + local done = true + + while rng.percent(self.on_spot_chance) do -- NOT A WHILE, this is used as a breakable if -- Cycle through spots, looking for one with empty spaces local tries = 0 local spot = rng.table(self.genspots) if not spot then print("No spots for spawning") - return + break end local _, _, gs = util.findFreeGrid(spot.x, spot.y, self.spot_radius, "block_move", {[Map.ACTOR]=true}) while not gs and tries < 10 do @@ -57,7 +76,7 @@ function _M:getSpawnSpot(m) end if not gs then print("No more free space for spawning") - return + break end -- Cycle through available spaces tries = 0 @@ -75,17 +94,18 @@ function _M:getSpawnSpot(m) return x, y end - -- Spawn randomly - else - local x, y = rng.range(self.area.x1, self.area.x2), rng.range(self.area.y1, self.area.y2) - local tries = 0 - while (not m:canMove(x, y) or (self.map.room_map[x][y] and self.map.room_map[x][y].special)) and tries < 100 do - x, y = rng.range(self.area.x1, self.area.x2), rng.range(self.area.y1, self.area.y2) - tries = tries + 1 - end - if tries < 100 then - return x, y - end + break + end + + -- Otherwise, spawn randomly + local x, y = rng.range(self.area.x1, self.area.x2), rng.range(self.area.y1, self.area.y2) + local tries = 0 + while (not m:canMove(x, y) or (self.map.room_map[x][y] and self.map.room_map[x][y].special)) and tries < 100 do + x, y = rng.range(self.area.x1, self.area.x2), rng.range(self.area.y1, self.area.y2) + tries = tries + 1 + end + if tries < 100 then + return x, y end end diff --git a/game/engines/default/engine/generator/map/Building.lua b/game/engines/default/engine/generator/map/Building.lua index aedbf51eda3d84b8e22e31dec491dd6c890e64bc..19f47f8ee080e0b330d7e02ca6be08fd07f7fe1f 100644 --- a/game/engines/default/engine/generator/map/Building.lua +++ b/game/engines/default/engine/generator/map/Building.lua @@ -19,7 +19,7 @@ require "engine.class" local Map = require "engine.Map" -local BSP = require "engine.BSP" +local BSP = require "engine.algorithms.BSP" require "engine.Generator" local RoomsLoader = require "engine.generator.map.RoomsLoader" diff --git a/game/engines/default/engine/generator/map/MapScript.lua b/game/engines/default/engine/generator/map/MapScript.lua new file mode 100644 index 0000000000000000000000000000000000000000..b8e66993dd015317c74c847b58507cc739f6dbc6 --- /dev/null +++ b/game/engines/default/engine/generator/map/MapScript.lua @@ -0,0 +1,262 @@ +-- TE4 - T-Engine 4 +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +require "engine.class" +local Map = require "engine.Map" +local BaseGenerator = require "engine.Generator" +local RoomsLoader = require "engine.generator.map.RoomsLoader" +require "engine.Generator" + +--- @classmod engine.generator.map.MapScript +module(..., package.seeall, class.inherit(engine.Generator, RoomsLoader)) + +function _M:init(zone, map, level, data) + engine.Generator.init(self, zone, map, level) + self.data = data + self.grid_list = zone.grid_list + self.spots = {} + self.mapsize = {self.map.w, self.map.h, w=self.map.w, h=self.map.h} + self.post_gen = {} + self.maps_positions = {} + self.maps_registers = {} + + RoomsLoader.init(self, data) +end + + +--- Resolve a filename, from /data/ /data-addon/ or subfodler of zonedir +function _M:getFile(file, folder) + folder = folder or "maps" + if file:prefix("/") then return file end + -- Found in the zone itself ? + if file:find("^!") then return self.zone:getBaseName().."/"..folder.."/"..file:sub(2) end + + local _, _, addon, rfile = file:find("^([^+]+)%+(.+)$") + if addon and rfile then + return "/data-"..addon.."/"..folder.."/"..rfile + end + return "/data/"..folder.."/"..file +end + +function _M:regenerate() + self.force_regen = true +end + +function _M:loadFile(mapscript, lev, old_lev, args) + local file = self:getFile(mapscript..".lua", "mapscripts") + local f, err = loadfile(file) + if not f and err then error(err) end + local nenv = { + args = args, + self = self, + zone = self.zone, + level = self.level, + mapdata = self.data, + lev = lev, + old_lev = old_lev, + loadMapScript = function(name, args) return self:loadFile(name, lev, old_lev, args) end, + } + for f in fs.iterate("/engine/tilemaps/", function(f) return f:find("%.lua$") end) do + local n = f:sub(1, -5) + local nf = "engine.tilemaps."..n + nenv[n] = require(nf) + end + setfenv(f, setmetatable(nenv, {__index=_G})) + return f() +end + +function _M:custom(lev, old_lev) + for f in fs.iterate("/engine/tilemaps/", function(f) return f:find("%.lua$") end) do + local n = f:sub(1, -5) + local nf = "engine.tilemaps."..n + package.loaded[nf] = nil + end + + + local ret = nil + if self.data.mapscript then + local mapscript = self.data.mapscript + if type(mapscript) == "table" then mapscript = rng.table(mapscript) end + ret = self:loadFile(mapscript, lev, old_lev) + elseif self.data.custom then + ret = self.data.custom(self, lev, old_lev) + end + + if ret then + return ret + elseif self.force_regen then + return nil + else + error("Generator MapScript called without mapscript or custom fields set!") + end +end + +function _M:generate(lev, old_lev) + print("Generating MapScript") + self.lev, self.old_lev = lev, old_lev + self.force_regen = false + local data = self:custom(lev, old_lev) + + for id, map in pairs(self.maps_registers) do + local pos = self.maps_positions[id] + self.map:import(map, pos.x - 1, pos.y - 1) + end + + -- We do it AFTER importing submaps to ensure entities on them are correctly released + if self.force_regen then self.level.force_recreate = true return false end + + if not self.entrance_pos then self.entrance_pos = data:locateTile('<') end + if not self.exit_pos then self.exit_pos = data:locateTile('>') end + if not self.entrance_pos then self.entrance_pos = data:point(1, 1) end + if not self.exit_pos then self.exit_pos = data:point(1, 1) end + + data = data:getResult(true) + for i = 0, self.map.w - 1 do + for j = 0, self.map.h - 1 do + if data[j+1][i+1] ~= "â“" and data[j+1][i+1] ~= "⎕" then + self.map(i, j, Map.TERRAIN, self:resolve(data[j+1][i+1] or '#')) + end + end + end + + for _, post in pairs(self.post_gen) do + post(self, lev, old_lev) + end + + return self.entrance_pos.x - 1, self.entrance_pos.y - 1, self.exit_pos.x - 1, self.exit_pos.y - 1 +end + +function _M:setEntrance(pos) + self.entrance_pos = pos +end +function _M:setExit(pos) + self.exit_pos = pos +end + +function _M:addSpot(x, y, type, subtype, data) + data = data or {} + -- Tilemap uses 1 based indexes + data.x = math.floor(x) - 1 + data.y = math.floor(y) - 1 + data.type = type + data.subtype = subtype + self.spots[#self.spots+1] = data +end + +function _M:postGen(fct) + self.post_gen[#self.post_gen+1] = fct +end + +function _M:makeTemporaryMap(map_w, map_h, fct) + local old_map = self.level.map + local old_game_level = game.level + game.level = self.level + + local tmp_map = Map.new(map_w, map_h) + self.level.map = tmp_map + self.map = tmp_map + local new_data = table.clone(self.data, true) + + -- Fake a generator call to init tmp_map.room_map + local ngen = BaseGenerator.new({}, tmp_map, {}, {}) + + fct(tmp_map, new_data) + + game.level = old_game_level + self.map = old_map + self.level.map = old_map + + return tmp_map +end + +--- Create the stairs inside the level +function _M:makeStairsInside(lev, old_lev, spots) + -- Put down stairs + local dx, dy + if lev < self.zone.max_level or self.data.force_last_stair then + while true do + dx, dy = rng.range(1, self.map.w - 1), rng.range(1, self.map.h - 1) + if not self.map:checkEntity(dx, dy, Map.TERRAIN, "block_move") and not self.map.room_map[dx][dy].special then + self.map(dx, dy, Map.TERRAIN, self:resolve(">")) + self.map.room_map[dx][dy].special = "exit" + break + end + end + end + + -- Put up stairs + local ux, uy + while true do + ux, uy = rng.range(1, self.map.w - 1), rng.range(1, self.map.h - 1) + if not self.map:checkEntity(ux, uy, Map.TERRAIN, "block_move") and not self.map.room_map[ux][uy].special then + self.map(ux, uy, Map.TERRAIN, self:resolve("<")) + self.map.room_map[ux][uy].special = "exit" + break + end + end + + return ux, uy, dx, dy, spots +end + +--- Create the stairs on the sides +function _M:makeStairsSides(lev, old_lev, sides, spots) + -- Put down stairs + local dx, dy + if self.forced_down then + dx, dy = self.forced_down.x, self.forced_down.y + else + if lev < self.zone.max_level or self.data.force_last_stair then + while true do + if sides[2] == 4 then dx, dy = 0, rng.range(0, self.map.h - 1) + elseif sides[2] == 6 then dx, dy = self.map.w - 1, rng.range(0, self.map.h - 1) + elseif sides[2] == 8 then dx, dy = rng.range(0, self.map.w - 1), 0 + elseif sides[2] == 2 then dx, dy = rng.range(0, self.map.w - 1), self.map.h - 1 + end + + if not self.map.room_map[dx][dy].special then + self.map(dx, dy, Map.TERRAIN, self:resolve("down")) + self.map.room_map[dx][dy].special = "exit" + break + end + end + end + end + + -- Put up stairs + local ux, uy + if self.forced_up then + ux, uy = self.forced_up.x, self.forced_up.y + else + while true do + if sides[1] == 4 then ux, uy = 0, rng.range(0, self.map.h - 1) + elseif sides[1] == 6 then ux, uy = self.map.w - 1, rng.range(0, self.map.h - 1) + elseif sides[1] == 8 then ux, uy = rng.range(0, self.map.w - 1), 0 + elseif sides[1] == 2 then ux, uy = rng.range(0, self.map.w - 1), self.map.h - 1 + end + + if not self.map.room_map[ux][uy].special then + self.map(ux, uy, Map.TERRAIN, self:resolve("up")) + self.map.room_map[ux][uy].special = "exit" + break + end + end + end + + return ux, uy, dx, dy, spots +end diff --git a/game/engines/default/engine/generator/map/Town.lua b/game/engines/default/engine/generator/map/Town.lua index 99447f6dfc263e81a5fe2b46e8ca8ac5f16bbdde..a71bb0a39a9299c1636043104d0bdba0d49ffc9d 100644 --- a/game/engines/default/engine/generator/map/Town.lua +++ b/game/engines/default/engine/generator/map/Town.lua @@ -19,7 +19,7 @@ require "engine.class" local Map = require "engine.Map" -local BSP = require "engine.BSP" +local BSP = require "engine.algorithms.BSP" require "engine.Generator" local RoomsLoader = require "engine.generator.map.RoomsLoader" diff --git a/game/engines/default/engine/tilemaps/BSP.lua b/game/engines/default/engine/tilemaps/BSP.lua new file mode 100644 index 0000000000000000000000000000000000000000..0d79786e2cae4acaaefa0ab95fdd5835abcea2c3 --- /dev/null +++ b/game/engines/default/engine/tilemaps/BSP.lua @@ -0,0 +1,82 @@ +-- TE4 - T-Engine 4 +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +require "engine.class" +local Tilemap = require "engine.tilemaps.Tilemap" +local Proxy = require "engine.tilemaps.Proxy" +local AlgoBSP = require "engine.algorithms.BSP" + +--- Generate partitioned "building" +-- @classmod engine.tilemaps.BSP +module(..., package.seeall, class.inherit(Tilemap)) + +function _M:init(min_w, min_h, max_depth, inner_walls) + Tilemap.init(self) + self.a_min_w, self.a_min_h, self.a_max_depth = min_w, min_h, max_depth + self.inner_walls = inner_walls +end + +function _M:make(w, h, floor, wall) + local bsp = AlgoBSP.new(w, h, self.a_min_w, self.a_min_h, self.a_max_depth) + bsp:partition() + + self:setSize(w, h, ' ') + + self.rooms = {} + + for _, leaf in ipairs(bsp.leafs) do + local from = self:point(leaf.rx + 1, leaf.ry + 1) + local to = self:point(leaf.rx + 1 + leaf.w - 2, leaf.ry + 1 + leaf.h - 2) + local center = from + (to - from) / 2 + + if floor then + self:carveArea(floor, from, to) + end + if wall then + if self.inner_walls then + for i = from.x, to.x do + self:put(self:point(i, from.y), wall) + self:put(self:point(i, to.y), wall) + end + for j = from.y, to.y do + self:put(self:point(from.x, j), wall) + self:put(self:point(to.x, j), wall) + end + else + for i = from.x - 1, to.x + 1 do + self:put(self:point(i, from.y - 1), wall) + self:put(self:point(i, to.y + 1), wall) + end + for j = from.y - 1, to.y + 1 do + self:put(self:point(from.x - 1, j), wall) + self:put(self:point(to.x + 1, j), wall) + end + end + end + + self.rooms[#self.rooms+1] = { + from = from, + to = to, + center = center, + map = Proxy.new(self, from, leaf.w - 1, leaf.h - 1), + } + end + + return self +end diff --git a/game/engines/default/engine/tilemaps/Heightmap.lua b/game/engines/default/engine/tilemaps/Heightmap.lua new file mode 100644 index 0000000000000000000000000000000000000000..6faf58cde01dc094e755ea98cbef3b41aa3fd41f --- /dev/null +++ b/game/engines/default/engine/tilemaps/Heightmap.lua @@ -0,0 +1,63 @@ +-- TE4 - T-Engine 4 +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +require "engine.class" +local Tilemap = require "engine.tilemaps.Tilemap" +local HMap = require "engine.Heightmap" + +--- Generate map-like data from a heightmap fractal +-- @classmod engine.tilemaps.Heightmap +module(..., package.seeall, class.inherit(Tilemap)) + +function _M:init(roughness, start) + Tilemap.init(self) + self.roughness = roughness or 1.2 + self.start = start or {} + for k, e in pairs(start) do start[k] = e * HMap.max end +end + +function _M:make(w, h, chars, normalize) + if normalize == nil then normalize = true end + + self:setSize(w, h, ' ') + + local hmap = HMap.new(w, h, self.roughness, self.start) + hmap:generate() + + local tmp = {} + local min, max = 1, 0 + for j = 1, h do for i = 1, w do + local nv = hmap.hmap[i][j] / hmap.max + + tmp[j] = tmp[j] or {} + tmp[j][i] = nv + if nv < min then min = nv end + if nv > max then max = nv end + end end + + for j = 1, h do for i = 1, w do + local nv = tmp[j][i] + if normalize then nv = (nv - min) / (max - min) end + + local c = util.bound(math.floor(nv * (#chars - 1)) + 1, 1, #chars) + self.data[j][i] = chars[c] + end end + + return self +end diff --git a/game/engines/default/engine/tilemaps/Maze.lua b/game/engines/default/engine/tilemaps/Maze.lua new file mode 100644 index 0000000000000000000000000000000000000000..20a03a3f76221871a0220232f2a14a272ec11fdf --- /dev/null +++ b/game/engines/default/engine/tilemaps/Maze.lua @@ -0,0 +1,110 @@ +-- TE4 - T-Engine 4 +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +require "engine.class" +local Tilemap = require "engine.tilemaps.Tilemap" +local HMap = require "engine.Heightmap" + +--- Generate map-like data from a heightmap fractal +-- @classmod engine.tilemaps.Heightmap +module(..., package.seeall, class.inherit(Tilemap)) + +function _M:init() + Tilemap.init(self) +end + +function _M:makeSimple(w, h, floors, walls, enclosed) + if type(floors) == "string" then floors = {floors} end + if type(walls) == "string" then walls = {walls} end + + self:setSize(w, h, ' ') + + local lastx, lasty = 0, 0 + + local mazemap = self:makeData(w, h, false) + + local do_tile = function(i, j, wall) + self.data[j][i] = wall and rng.table(walls) or rng.table(floors) + mazemap[j][i] = wall + if not wall then + lastx = math.max(lastx, i) + lasty = math.max(lasty, j) + end + end + + for i = 1, w do for j = 1, h do + do_tile(i, j, true) + end end + + local mw, Mw = 1, w + local mh, Mh = 1, h + if not enclosed then + mw, Mw = 0, w + 1 + mh, Mh = 0, h + 1 + end + + local xpos, ypos = mw+1, mh+1 + local moves = {{xpos,ypos}} + local pickp = rng.range(1,4) + while #moves > 0 do + local pickn = #moves - math.floor((rng.range(1,100000)/100001)^pickp * #moves) + local pick = moves[pickn] + xpos = pick[1] + ypos = pick[2] + local dir = {} + if xpos+2>mw and xpos+2<Mw and mazemap[ypos][xpos+2] then + dir[#dir+1] = 6 + end + if xpos-2>mw and xpos-2<Mw and mazemap[ypos][xpos-2] then + dir[#dir+1] = 4 + end + if ypos-2>mh and ypos-2<Mh and mazemap[ypos-2][xpos] then + dir[#dir+1] = 8 + end + if ypos+2>mh and ypos+2<Mh and mazemap[ypos+2][xpos] then + dir[#dir+1] = 2 + end + + if #dir > 0 then + local d = dir[rng.range(1, #dir)] + if d == 4 then + do_tile(xpos-2, ypos, false) + do_tile(xpos-1, ypos, false) + xpos = xpos - 2 + elseif d == 6 then + do_tile(xpos+2, ypos, false) + do_tile(xpos+1, ypos, false) + xpos = xpos + 2 + elseif d == 8 then + do_tile(xpos, ypos-2, false) + do_tile(xpos, ypos-1, false) + ypos = ypos - 2 + elseif d == 2 then + do_tile(xpos, ypos+2, false) + do_tile(xpos, ypos+1, false) + ypos = ypos + 2 + end + table.insert(moves, {xpos, ypos}) + else + local back = table.remove(moves,pickn) + end + end + + return self +end diff --git a/game/engines/default/engine/tilemaps/Noise.lua b/game/engines/default/engine/tilemaps/Noise.lua new file mode 100644 index 0000000000000000000000000000000000000000..5d2ec6734885887c8d11a7d70934cdfd702ad16b --- /dev/null +++ b/game/engines/default/engine/tilemaps/Noise.lua @@ -0,0 +1,65 @@ +-- TE4 - T-Engine 4 +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +require "engine.class" +local Tilemap = require "engine.tilemaps.Tilemap" + +--- Generate map-like data from a noise generator +-- @classmod engine.tilemaps.Noise +module(..., package.seeall, class.inherit(Tilemap)) + +function _M:init(noise_kind, hurst, lacunarity, zoom, octave) + Tilemap.init(self) + + self.noise_kind = noise_kind or "fbm_perlin" + hurst = hurst or 0.5 + lacunarity = lacunarity or 2 + self.zoom = zoom or 1 + self.octave = octave or 6 + + self.noise = core.noise.new(2, self.hurst, self.lacunarity) + + -- self.data_h = #self.data + -- self.data_w = self.data[1] and #self.data[1] or 0 +end + +function _M:make(w, h, chars, normalize) + if normalize == nil then normalize = true end + + self:setSize(w, h, ' ') + + local tmp = {} + local min, max = 1, 0 + for i = 1, w do for j = 1, h do + local nv = self.noise[self.noise_kind](self.noise, self.zoom * (i-1) / w, self.zoom * (j-1) / h, self.octave) / 2 + 0.5 + tmp[j] = tmp[j] or {} + tmp[j][i] = nv + if nv < min then min = nv end + if nv > max then max = nv end + end end + + for i = 1, w do for j = 1, h do + local nv = tmp[j][i] + if normalize then nv = (nv - min) / (max - min) end + local v = math.floor(nv * (#chars - 1)) + if v > #chars then os.crash() end + self.data[j][i] = chars[v+1] + end end + return self +end diff --git a/game/engines/default/engine/tilemaps/Proxy.lua b/game/engines/default/engine/tilemaps/Proxy.lua new file mode 100644 index 0000000000000000000000000000000000000000..3124f3c86ec2bccaab09da7b557681fb806eb95a --- /dev/null +++ b/game/engines/default/engine/tilemaps/Proxy.lua @@ -0,0 +1,65 @@ +-- TE4 - T-Engine 4 +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +require "engine.class" +local Tilemap = require "engine.tilemaps.Tilemap" +local HMap = require "engine.Heightmap" + +--- Generate map-like data from a heightmap fractal +-- @classmod engine.tilemaps.Heightmap +module(..., package.seeall, class.inherit(Tilemap)) + +function _M:init(source_map, offset, w, h) + Tilemap.init(self) + self.unmergable = true + self.source_map = source_map + self.merged_pos = offset + self:setSize(w, h) + + -- Make data an actual proxy + self.data = {} + + local proxy_t = { + __newindex = function(ys, x, v) + if self.mask and self.mask[ys.__y] and not self.mask[ys.__y][x] then return end -- Woops, not allowed! + local pos = self.merged_pos + self:point(x, ys.__y) - 1 + self.source_map.merged_pos - 1 + self.source_map:put(pos, v) + end, + __index = function(ys, x) + local pos = self.merged_pos + self:point(x, ys.__y) - 1 + self.source_map.merged_pos - 1 + return self.source_map.data[pos.y][pos.x] + end, + } + + for j = 1, h do + self.data[j] = setmetatable({__y=j}, proxy_t) + end +end + +function _M:maskOtherPoints(list, source_coords) + if not self.mask then self.mask = self:makeData(self.data_w, self.data_h, false) end + for _, p in ipairs(list) do + if source_coords then + local np = p - self.merged_pos + 1 + self.mask[np.y][np.x] = true + else + self.mask[p.y][p.x] = true + end + end +end diff --git a/game/engines/default/engine/tilemaps/Rooms.lua b/game/engines/default/engine/tilemaps/Rooms.lua new file mode 100644 index 0000000000000000000000000000000000000000..e02efe9fb47fd3c7da111e24cc58fe26870cf454 --- /dev/null +++ b/game/engines/default/engine/tilemaps/Rooms.lua @@ -0,0 +1,160 @@ +-- TE4 - T-Engine 4 +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +require "engine.class" +local Tilemap = require "engine.tilemaps.Tilemap" +local RoomsLoader = require "engine.generator.map.RoomsLoader" + +--- Generate map-like data from a heightmap fractal +-- @classmod engine.tilemaps.Heightmap +module(..., package.seeall, class.inherit(Tilemap)) + +local RoomInstance = class.inherit(Tilemap){} + +function _M:init(mapscript, rooms_list) + self.mapscript = mapscript + if type(rooms_list) == "string" then rooms_list = {rooms_list} end + + self.rooms = {} + for _, file in ipairs(rooms_list) do + table.insert(self.rooms, mapscript:loadRoom(file)) + end + + self.room_next_id = 1 +end + +function _M:generateRoom(temp_symbol, account_for_border) + local mapscript = self.mapscript + local roomdef = rng.table(self.rooms) + local id = tostring(self)..":"..self.room_next_id + local room = mapscript:roomGen(roomdef, id, mapscript.lev, mapscript.old_lev) + if not room then + print("Tilemap.Rooms failed to generate room") + return nil + end + + if account_for_border == nil then account_for_border = false end + + temp_symbol = temp_symbol or {wall='â“', floor='⎕'} + + local tm = RoomInstance.new({room.w - (account_for_border and 2 or 0), room.h - (account_for_border and 2 or 0)}, temp_symbol and temp_symbol.wall) + room.temp_symbol = temp_symbol + tm.mapscript = mapscript + tm.room_data = room + tm.room_id = id + + self.room_next_id = self.room_next_id + 1 + return tm +end + +function RoomInstance:build() + local mapscript = self.mapscript + local room = self.room_data + local id = self.room_id + + if account_for_border == nil then account_for_border = false end + + local map = mapscript:makeTemporaryMap(room.w, room.h, function(map) + mapscript:roomPlace(room, id, 0, 0) + end) + mapscript.maps_registers[id] = map + mapscript.maps_positions[id] = account_for_border and self:point(0, 0) or self:point(1, 1) + + local exits = { openables={}, doors={} } + + local function checkexits(i, j, dir) + if map.room_map[i][j].can_open then + exits.openables[#exits.openables+1] = self:point(i+1, j+1) + end + if map(i, j, map.TERRAIN) and map(i, j, map.TERRAIN).door_opened then + -- Doors "position" is the actual tile right beside it, not the door itself as we dont want to delete the door + local dx, dy = util.coordAddDir(i+1, j+1, dir) + exits.doors[#exits.doors+1] = self:point(dx, dy) + end + end + self.static_map = map + self.exits = exits + + for i = 0, map.w - 1 do + checkexits(i, 0, 8) + checkexits(i, map.h - 1, 2) + end + for j = 0, map.h - 1 do + checkexits(0, j, 4) + checkexits(map.w - 1, j, 6) + end + + -- Mark floor & walls + for i = 0, map.w - 1 do for j = 0, map.h - 1 do + if not map:checkEntity(i, j, map.TERRAIN, 'block_move') then + self.data[j+1][i+1] = room.temp_symbol.floor + end + end end + + return self +end + +--- Not used, so we remove all entities +function RoomInstance:discard() + for i = 0, self.static_map.w - 1 do for j = 0, self.static_map.h - 1 do + local z = i + j * self.static_map.w + if self.static_map.map[z] then + for _, e in pairs(self.static_map.map[z]) do + e:removed() + end + end + end end +end + +function RoomInstance:mergedAt(x, y) + Tilemap.mergedAt(self, x, y) + + local d = self:point(x - 1, y - 1) + + self.mapscript.maps_positions[self.room_id] = self.mapscript.maps_positions[self.room_id] + d + for _, open in pairs(self.exits.openables) do open.x, open.y = open.x + d.x, open.y + d.y end + for _, door in pairs(self.exits.doors) do door.x, door.y = door.x + d.x, door.y + d.y end +end + +function RoomInstance:findExits(pos, kind) + local list = {} + if not kind or kind == "openable" then + for _, open in pairs(self.exits.openables) do + local dist = core.fov.distance(pos.x, pos.y, open.x, open.y) + table.insert(list, {dist = dist, pos = open, kind = "open"}) + end + end + if not kind or kind == "door" then + for _, door in pairs(self.exits.doors) do + local dist = core.fov.distance(pos.x, pos.y, door.x, door.y) + table.insert(list, {dist = dist, pos = door, kind = "door"}) + end + end + table.sort(list, "dist") + return list +end + + + +function _M:flip(axis) error("Cannot use :flip() on a Room tilemap") end +function _M:rotate(angle) error("Cannot use :rotate() on a Room tilemap") end +function _M:scale(sx, sy) error("Cannot use :scale() on a Room tilemap") end +function RoomInstance:flip(axis) error("Cannot use :flip() on a Room tilemap") end +function RoomInstance:rotate(angle) error("Cannot use :rotate() on a Room tilemap") end +function RoomInstance:scale(sx, sy) error("Cannot use :scale() on a Room tilemap") end diff --git a/game/engines/default/engine/tilemaps/Static.lua b/game/engines/default/engine/tilemaps/Static.lua new file mode 100644 index 0000000000000000000000000000000000000000..063590872f394c50397acd4620c1a3be75373116 --- /dev/null +++ b/game/engines/default/engine/tilemaps/Static.lua @@ -0,0 +1,34 @@ +-- TE4 - T-Engine 4 +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +require "engine.class" +local Tilemap = require "engine.tilemaps.Tilemap" + +--- Generate map-like data from a tmx file +-- @classmod engine.tilemaps.Static +module(..., package.seeall, class.inherit(Tilemap)) + +function _M:init(file) + Tilemap.init(self) + + self.data = self:tmxLoad(file) + self.data_h = #self.data + self.data_w = self.data[1] and #self.data[1] or 0 + self.data_size = self:point(self.data_w, self.data_h) +end diff --git a/game/engines/default/engine/tilemaps/Tilemap.lua b/game/engines/default/engine/tilemaps/Tilemap.lua new file mode 100644 index 0000000000000000000000000000000000000000..abc9c295ead143bd64870d7a32ab7b0c6d793380 --- /dev/null +++ b/game/engines/default/engine/tilemaps/Tilemap.lua @@ -0,0 +1,1206 @@ +-- TE4 - T-Engine 4 +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +require "engine.class" +local lom = require "lxp.lom" +local mime = require "mime" + +--- Base class to generate map-like +-- @classmod engine.tilemaps.Tilemap +module(..., package.seeall, class.make) + +function _M:init(size, fill_with) + if size then + self:setSize(size[1], size[2], fill_with) + end + self.merged_pos = self:point(1, 1) +end + +function _M:getSize() + return self.data_w, self.data_h +end + +function _M:setSize(w, h, fill_with) + self.data_w = math.floor(w) + self.data_h = math.floor(h) + self.data_size = self:point(self.data_w, self.data_h) + if self.data_w and self.data_h then + self.data = self:makeData(self.data_w, self.data_h, fill_with or ' ') + end +end + +function _M:makeData(w, h, fill_with) + local data = {} + for y = 1, h do + data[y] = {} + for x = 1, w do + data[y][x] = fill_with + end + end + return data +end + +function _M:makeCharsTable(v, default) + if not v then v = default end + if type(v) == "string" then v = {v} end + return table.reverse(v) +end + +local point_meta = { + __add = function(a, b) + if type(b) == "number" then return _M:point(a.x + b, a.y + b) + else return _M:point(a.x + b.x, a.y + b.y) end + end, + __sub = function(a, b) + if type(b) == "number" then return _M:point(a.x - b, a.y - b) + else return _M:point(a.x - b.x, a.y - b.y) end + end, + __mul = function(a, b) + if type(b) == "number" then return _M:point(a.x * b, a.y * b) + else return _M:point(a.x * b.x, a.y * b.y) end + end, + __div = function(a, b) + if type(b) == "number" then return _M:point(a.x / b, a.y / b) + else return _M:point(a.x / b.x, a.y / b.y) end + end, + __eq = function(a, b) + return a.x == b.x and a.y == b.y + end, + __tostring = function(p) + return ("Point(%d x %d)"):format(p.x, p.y) + end, + __index = { + hashCoord = function(p, tilemap) + return p.y * tilemap.data_w + p.x + end, + area = function(p) + return p.y * p.x + end, + }, +} +--- Make a point data, can be added +function _M:point(x, y) + if type(x) == "table" then + local p = {x=math.floor(x.x), y=math.floor(x.y)} + setmetatable(p, point_meta) + return p + else + local p = {x=math.floor(x), y=math.floor(y)} + setmetatable(p, point_meta) + return p + end +end + + +--- Returns a point at the center of the map, accounting for merged_pos +function _M:centerPoint() + return self:point(self.data_w / 2, self.data_h / 2) + self.merged_pos - 1 +end + +--- Sorts a list of tilemaps by their centerPoint +function _M:sortListCenter(list) + local newlist = {} + + local first = table.remove(list, 1) + while #list > 0 do + local fp = first:centerPoint() + table.sort(list, function(a, b) a = a:centerPoint() b = b:centerPoint() return core.fov.distance(fp.x, fp.y, a.x, a.y) < core.fov.distance(fp.x, fp.y, b.x, b.y) end) + + newlist[#newlist+1] = first + first = table.remove(list, 1) + if #list == 0 then newlist[#newlist+1] = first end + end + + table.replaceWith(list, newlist) + return list +end + +--- Sorts a list of tilemaps by their centerPoint +function _M:findEachClosestCenter(list) + local newlist = {} + + for _, map in ipairs(list) do + local closests = {} + local fp = map:centerPoint() + + for _, cmap in ipairs(list) do if cmap ~= map then closests[#closests+1] = cmap end end + table.sort(closests, function(a, b) a = a:centerPoint() b = b:centerPoint() return core.fov.distance(fp.x, fp.y, a.x, a.y) < core.fov.distance(fp.x, fp.y, b.x, b.y) end) + + newlist[map] = closests + end + + return newlist +end + +--- Find all empty spaces (defaults to ' ') and fill them with a give char +function _M:fillAll(fill_with, empty_char) + if not self.data then return end + empty_char = empty_char or ' ' + fill_with = fill_with or '#' + for y = 1, self.data_h do + for x = 1, self.data_w do + if self.data[y][x] == empty_char then + self.data[y][x] = fill_with + end + end + end +end + +--- Check if the given coords are on the map +function _M:isBound(pos, y) + if y then pos = self:point(pos, y) end + if pos.x >= 1 and pos.x <= self.data_w and pos.y >=1 and pos.y <= self.data_h then + return true + else + return false + end +end + +--- Draw a single tile +function _M:put(pos, char) + if self:isBound(pos) then + if type(char) == "function" then char = char(pos) + elseif type(char) == "table" then char = rng.table(char) end + self.data[pos.y][pos.x] = char + end +end + +--- Flip the map +function _M:flip(axis) + local ndata = self:makeData(self.data_w, self.data_h, '') + for y = 1, self.data_h do + for x = 1, self.data_w do + local nx, ny = x, y + if axis == "x" or axis == true then + x = self.data_w - (x - 1) + elseif axis == "y" or axis == false then + y = self.data_h - (y - 1) + end + ndata[ny][nx] = self.data[y][x] + end + end + self.data = ndata + return self +end + +--- Rotate the map +function _M:rotate(angle) + local function rotate_coords(i, j) + local ii, jj = i, j + if angle == 90 then ii, jj = j, self.data_w - i + 1 + elseif angle == 180 then ii, jj = self.data_w - i + 1, self.data_h - j + 1 + elseif angle == 270 then ii, jj = self.data_h - j + 1, i + end + return ii, jj + end + + local ndata_w, ndata_h = self.data_w, self.data_h + if angle == 90 or angle == 270 then ndata_w, ndata_h = self.data_h, self.data_w end + local ndata = self:makeData(ndata_w, ndata_h, '') + for y = 1, self.data_h do + for x = 1, self.data_w do + local nx, ny = rotate_coords(x, y) + ndata[ny][nx] = self.data[y][x] + end + end + self.data = ndata + self.data_w = ndata_w + self.data_h = ndata_h + return self +end + +--- Widen the map +function _M:scale(sx, sy) + local ndata = self:makeData(self.data_w * sx, self.data_h * sy, '') + for y = 0, self.data_h - 1 do + for x = 0, self.data_w - 1 do + for nx = x * sx, x * sx + sx - 1 do + for ny = y * sy, y * sy + sy - 1 do + ndata[ny + 1][nx + 1] = self.data[y + 1][x + 1] + end + end + end + end + self.data = ndata + self.data_w, self.data_h = self.data_w * sx, self.data_h * sy + return self +end + +--- Used internally to load a tilemap from a tmx file +function _M:tmxLoad(file) + local f = fs.open(file, "r") local data = f:read(10485760) f:close() + local map = lom.parse(data) + local mapprops = {} + if map:findOne("properties") then mapprops = map:findOne("properties"):findAllAttrs("property", "name", "value") end + self.props = mapprops + + local w, h = tonumber(map.attr.width), tonumber(map.attr.height) + if mapprops.map_data then + local params = self:loadLuaInEnv(g, nil, "return "..mapprops.map_data) + table.merge(self.data, params, true) + end + + local gids = {} + + for _, tileset in ipairs(map:findAll("tileset")) do + local firstgid = tonumber(tileset.attr.firstgid) + local has_tile = tileset:findOne("tile") + for _, tile in ipairs(tileset:findAll("tile")) do + local tileprops = {} + if tile:findOne("properties") then tileprops = tile:findOne("properties"):findAllAttrs("property", "name", "value") end + local gid = tonumber(tile.attr.id) + gids[firstgid + gid] = {tid=tileprops.id or ' '} + end + end + + local data = {} + for y = 1, h do + data[y] = {} + for x = 1, w do data[y][x] = ' ' end + end + + local function populate(x, y, gid) + if not gids[gid] then return end + local g = gids[gid] + data[y][x] = g.tid + end + + for _, layer in ipairs(map:findAll("layer")) do + local mapdata = layer:findOne("data") + if mapdata.attr.encoding == "base64" then + local b64 = mime.unb64(mapdata[1]:trim()) + local data + if mapdata.attr.compression == "zlib" then data = zlib.decompress(b64) + elseif not mapdata.attr.compression then data = b64 + else error("tmx map compression unsupported: "..mapdata.attr.compression) + end + local gid, i = nil, 1 + local x, y = 1, 1 + while i <= #data do + gid, i = struct.unpack("<I4", data, i) + populate(x, y, gid) + x = x + 1 + if x > w then x = 1 y = y + 1 end + end + elseif mapdata.attr.encoding == "csv" then + local data = mapdata[1]:gsub("[^,0-9]", ""):split(",") + local x, y = 1, 1 + for i, gid in ipairs(data) do + gid = tonumber(gid) + populate(x, y, gid) + x = x + 1 + if x > w then x = 1 y = y + 1 end + end + elseif not mapdata.attr.encoding then + local data = mapdata:findAll("tile") + local x, y = 1, 1 + for i, tile in ipairs(data) do + local gid = tonumber(tile.attr.gid) + populate(x, y, gid) + x = x + 1 + if x > w then x = 1 y = y + 1 end + end + end + end + return data, w, h +end + +function _M:collapseToLineFormat(data) + if not data then return nil end + local ndata = {} + for y = 1, #data do ndata[y] = table.concat(data[y]) end + return ndata +end + +--- Do we have results, or did we fail? +function _M:hasResult() + return self.data and true or false +end + +--- Locate a specific tile +function _M:locateTile(char, erase) + local res = {} + for i = 1, self.data_w do + for j = 1, self.data_h do + if self.data[j][i] == char then + res[#res+1] = self:point(i, j) + if erase then self.data[j][i] = erase end + end + end + end + if #res == 0 then return nil end + return rng.table(res), res +end + +--- Finds a random location with enough area available +function _M:findRandomArea(from, to, w, h, made_of, margin, tries) + if not tries then tries = 500 end + if not margin then margin = 0 end + if not from then from = self:point(1, 1) end + if not to then to = self:point(self.data_w, self.data_h) end + if not made_of then made_of = {'#'} end + if type(made_of) == "string" then made_of = {made_of} end + made_of = table.reverse(made_of) + + while tries > 0 do + local pos = self:point(rng.range(from.x, to.x - w - margin * 2), rng.range(from.y, to.y - h - margin * 2)) + local ok = true + for i = pos.x - margin, pos.x + w + margin do + for j = pos.y - margin, pos.y + h + margin do + if not self:isBound(i, j) or not made_of[self.data[j][i]] then + ok = false + break + end + end + if not ok then break end + end + if ok then return pos end + + tries = tries - 1 + end + return nil +end + +--- Returns the bounding rectangle of a list of points +function _M:pointsBoundingRectangle(list) + local to = self:point(1, 1) + local from = self:point(self.data_w, self.data_h) + for _, p in ipairs(list) do + if p.x < from.x then from.x = p.x end + if p.x > to.x then to.x = p.x end + if p.y < from.y then from.y = p.y end + if p.y > to.y then to.y = p.y end + end + return from, to +end + +--- Return a list of groups of tiles that matches the given cond function +function _M:findGroups(cond) + if not self.data then return {} end + local Proxy = require "engine.tilemaps.Proxy" + + local fills = {} + local opens = {} + local list = {} + for i = 1, self.data_w do + opens[i] = {} + for j = 1, self.data_h do + if cond(self.data[j][i]) then + opens[i][j] = #list+1 + list[#list+1] = {x=i, y=j} + end + end + end + + local nbg = 0 + local function floodFill(x, y) + nbg=nbg+1 + local q = {self:point{x=x,y=y}} + local closed = {} + while #q > 0 do + local n = table.remove(q, 1) + if opens[n.x] and opens[n.x][n.y] then + -- self.data[n.y][n.x] = string.char(string.byte('0') + nbg) -- Debug to visualize floodfill groups + closed[#closed+1] = n + list[opens[n.x][n.y]] = nil + opens[n.x][n.y] = nil + q[#q+1] = self:point{x=n.x-1, y=n.y} + q[#q+1] = self:point{x=n.x, y=n.y+1} + q[#q+1] = self:point{x=n.x+1, y=n.y} + q[#q+1] = self:point{x=n.x, y=n.y-1} + + q[#q+1] = self:point{x=n.x+1, y=n.y-1} + q[#q+1] = self:point{x=n.x+1, y=n.y+1} + q[#q+1] = self:point{x=n.x-1, y=n.y-1} + q[#q+1] = self:point{x=n.x-1, y=n.y+1} + end + end + return closed + end + + -- Process all open spaces + local groups = {} + while next(list) do + local i, l = next(list) + local closed = floodFill(l.x, l.y) + + local from, to = self:pointsBoundingRectangle(closed) + to = to - from + 1 + local map = Proxy.new(self, from, to.x, to.y) + map:maskOtherPoints(closed, true) + + groups[#groups+1] = {list=closed, map=map} + print("[Tilemap] Floodfill group", i, #closed) + end + + return groups +end + +--- Return a list of groups of tiles representing each of the connected areas +function _M:findGroupsNotOf(wall) + wall = table.reverse(wall) + return self:findGroups(function(c) return not wall[c] end) +end + +--- Return a list of groups of tiles representing each of the connected areas +function _M:findGroupsOf(floor) + floor = table.reverse(floor) + return self:findGroups(function(c) return floor[c] end) +end + +--- Apply a custom method over the given groups, sorting them from bigger to smaller +-- It gives the groups in order of bigger to smaller +function _M:applyOnGroups(groups, fct) + if not self.data then return end + table.sort(groups, function(a,b) return #a.list > #b.list end) + for id, group in ipairs(groups) do + fct(group, id) + end +end + +--- Given a list of groups, eliminate them all +function _M:eliminateGroups(wall, groups) + if not self.data then return end + print("[Tilemap] Eleminating groups", #groups) + for i = 1, #groups do + print("[Tilemap] Eleminating group "..i.." of", #groups[i].list) + for j = 1, #groups[i].list do + local jn = groups[i].list[j] + self.data[jn.y][jn.x] = wall + end + end +end + +--- Simply destroy all connected groups except the biggest one +function _M:eliminateByFloodfill(walls) + if not self.data then return 0 end + local groups = self:findGroupsNotOf(walls) + + -- If nothing exists, regen + if #groups == 0 then + print("[Tilemap] Floodfill found nothing") + return 0 + end + + -- Sort to find the biggest group + table.sort(groups, function(a,b) return #a.list < #b.list end) + local g = table.remove(groups) + if g and #g.list > 0 then + print("[Tilemap] Ok floodfill with main group size", #g.list) + self:eliminateGroups(walls[1], groups) + return #g.list + else + print("[Tilemap] Floodfill left nothing") + return 0 + end +end + +function _M:fillGroup(group, char) + -- print("[Tilemap] Filling group of", #group.list, "with", char) + for j = 1, #group.list do + local jn = group.list[j] + self.data[jn.y][jn.x] = char + end +end + +function _M:isInGroup(group, x, y) + if not group.reverse then + group.reverse = {} + for j = 1, #group.list do + local jn = group.list[j] + group.reverse[jn.x] = group.reverse[jn.x] or {} + group.reverse[jn.x][jn.y] = true + end + end + return group.reverse[x] and group.reverse[x][y] +end +--[=[ +--- Find the biggest rectangle that can fit fully in the given group +function _M:groupInnerRectangle(group) + if #group.list == 0 then return nil end + + -- Make a matrix to work on + local outrect = self:groupOuterRectangle(group) + local m = self:makeData(outrect.w, outrect.h, 0) + local matrix = self:makeData(outrect.w, outrect.h, false) + for j = 1, #group.list do + local jn = group.list[j] + matrix[jn.y - outrect.y1 + 1][jn.x - outrect.x1 + 1] = true + end + + for i = 1, outrect.w do + for j = 1, outrect.h do + m[j][i] = matrix[j][i] and (1 + m[j+1][i]) or 0 + end + end + m[i][j]=matrix[i][j]=='1'?1+m[i][j+1]:0; +end + +public int maximalRectangle(char[][] matrix) { + int m = matrix.length; + int n = m == 0 ? 0 : matrix[0].length; + int[][] height = new int[m][n + 1]; + + int maxArea = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (matrix[i][j] == '0') { + height[i][j] = 0; + } else { + height[i][j] = i == 0 ? 1 : height[i - 1][j] + 1; + } + } + } + + for (int i = 0; i < m; i++) { + int area = maxAreaInHist(height[i]); + if (area > maxArea) { + maxArea = area; + } + } + + return maxArea; +} + +private int maxAreaInHist(int[] height) { + Stack<Integer> stack = new Stack<Integer>(); + + int i = 0; + int max = 0; + + while (i < height.length) { + if (stack.isEmpty() || height[stack.peek()] <= height[i]) { + stack.push(i++); + } else { + int t = stack.pop(); + max = Math.max(max, height[t] + * (stack.isEmpty() ? i : i - stack.peek() - 1)); + } + } + + return max; +} + +-- int maximalRectangle(vector<vector<char> > &matrix) { +-- if(matrix.size()==0 || matrix[0].size()==0)return 0; +-- vector<vector<int>>m(matrix.size()+1,vector<int>(matrix[0].size()+1,0)); +-- for(int i=0;i<matrix.size();i++) +-- for(int j=matrix[0].size()-1;j>=0;j--) +-- m[i][j]=matrix[i][j]=='1'?1+m[i][j+1]:0; +-- int max=0; +-- for(int i=0;i<matrix[0].size();i++){ +-- int p=0; +-- vector<int>s; +-- while(p!=m.size()){ +-- if(s.empty() || m[p][i]>=m[s.back()][i]) +-- s.push_back(p++); +-- else{ +-- int t=s.back(); +-- s.pop_back(); +-- max=std::max(max,m[t][i]*(s.empty()?p:p-s.back()-1)); +-- } +-- } +-- } +-- return max; +-- } +--]=] + +--- Find the smallest rectangle that can fit around in the given group +function _M:groupOuterRectangle(group) + local n = group.list[1] + if not n then return end -- wtf? + local x1, x2 = n.x, n.x + local y1, y2 = n.y, n.y + + for j = 1, #group.list do + local jn = group.list[j] + if jn.x < x1 then x1 = jn.x end + if jn.x > x2 then x2 = jn.x end + if jn.y < y1 then y1 = jn.y end + if jn.y > y2 then y2 = jn.y end + end + + -- Debug + -- for i = x1, x2 do for j = y1, y2 do + -- if not self:isInGroup(group, i, j) then + -- if self.data[j][i] == '#' then + -- self.data[j][i] = 'T' + -- end + -- end + -- end end + + return self:point(x1, y1), self:point(x2, y2), x2 - x1 + 1, y2 - y1 + 1 +end + +--- Carve out a simple linear path from coords until a tile is reached +function _M:carveLinearPath(char, from, dir, stop_at, dig_only_into) + local x, y = math.floor(from.x), math.floor(from.y) + local dx, dy = util.dirToCoord(dir) + if type(dig_only_into) == "table" then dig_only_into = table.reverse(dig_only_into) end + while x >= 1 and x <= self.data_w and y >= 1 and y <= self.data_h and self.data[y][x] ~= stop_at do + if not dig_only_into or (type(dig_only_into) == "table" and dig_only_into[self.data[y][x]]) or (type(dig_only_into) == "function" and dig_only_into(x, y, self.data[y][x])) then + self.data[y][x] = char + end + x, y = x + dx, y + dy + end +end + +--- Carve out a simple rectangle +function _M:carveArea(char, from, to, dig_only_into) + if type(dig_only_into) == "table" then dig_only_into = table.reverse(dig_only_into) end + for x = from.x, to.x do for y = from.y, to.y do + if x >= 1 and x <= self.data_w and y >= 1 and y <= self.data_h then + if not dig_only_into or (type(dig_only_into) == "table" and dig_only_into[self.data[y][x]]) or (type(dig_only_into) == "function" and dig_only_into(x, y, self.data[y][x])) then + self.data[y][x] = char + end + end + end end +end + +--- Apply a function over an area +function _M:applyArea(from, to, fct) + for x = from.x, to.x do for y = from.y, to.y do + if x >= 1 and x <= self.data_w and y >= 1 and y <= self.data_h then + local ret = fct(x, y, self.data[y][x]) + if ret ~= nil then + self.data[y][x] = ret + end + end + end end +end + +--- Get the results +-- @param is_array if true returns a table[][] of characters, if false a table[] of string lines +function _M:getResult(is_array) + if not self.data then return nil end + if is_array then return self.data end + local data = {} + for y = 1, self.data_h do data[y] = table.concat(self.data[y]) end + return data +end + +--- Debug function to print the result to the log +function _M:printResult() + if not self.data then + print("-------------") + print("------------- Tilemap result") + return + end + print("------------- Tilemap result --[[") + for _, line in ipairs(self:getResult()) do + print(line) + end + print("]]-----------") +end + +--- Merge an other Tilemap's data +function _M:merge(x, y, tm, char_order, empty_char) + if tm.unmergable then error("Trying to merge an unmergable tilemap, likely to be a Proxy") end + + if type(x) == "table" then -- If passed a point, shift the parameters + x, y, tm, char_order, empty_char = x.x, x.y, y, tm, char_order + end + + if not self.data or not tm.data then return end + -- if x is a table it's a point data so we shift parameters + if type(x) == "table" then + x, y, tm, char_order, empty_char = x.x, x.y, y, tm, char_order + end + + x = math.floor(x) + y = math.floor(y) + + char_order = table.reverse(char_order or {}) + + empty_char = empty_char or {' '} + if type(empty_char) == "string" then empty_char = {empty_char} end + empty_char = table.reverse(empty_char) + + if not tm.data then return end + + for i = 1, tm.data_w do + for j = 1, tm.data_h do + local si, sj = i + x - 1, j + y - 1 + if si >= 1 and si <= self.data_w and sj >= 1 and sj <= self.data_h then + local c = tm.data[j][i] + if not empty_char[c] then + local sc = self.data[sj][si] + local sc_o = char_order[sc] or 0 + local c_o = char_order[c] or 0 + + if c_o >= sc_o then + self.data[sj][si] = tm.data[j][i] + end + end + end + end + end + tm:mergedAt(x, y) +end + +function _M:mergedAt(x, y) + self.merged_pos = self.merged_pos + self:point(x, y) - 1 +end + +function _M:findExits(pos, kind, exitable_chars) + local list = {} + + if type(exitable_chars) == "string" then exitable_chars = {exitable_chars} end + exitable_chars = exitable_chars or {'.', ';'} + exitable_chars = table.reverse(exitable_chars) + + local function checkexits(i, j, dir) + local c = self.data[j][i] + if exitable_chars[c] then + local dist = core.fov.distance(pos.x, pos.y, i, j) + table.insert(list, {dist = dist, pos = self:point(i, j) + self.merged_pos - 1, kind = "open"}) + end + end + + local possibles = {} + + -- Find all possible exits on each sides, but "dig up" inside if the outer layer is just ' ' + local p, j + for i = 1, self.data_w do + for j = 1, self.data_h / 2 do + p = self:point(i, j); p.dir = 8 + if self.data[j][i] ~= ' ' then possibles[p:hashCoord(self)] = p break end + end + + for j = self.data_h, self.data_h / 2, -1 do + p = self:point(i, j); p.dir = 2 + if self.data[j][i] ~= ' ' then possibles[p:hashCoord(self)] = p break end + end + end + for j = 1, self.data_h do + for i = 1, self.data_w / 2 do + p = self:point(i, j); p.dir = 4 + if self.data[j][i] ~= ' ' then possibles[p:hashCoord(self)] = p break end + end + + for i = self.data_w, self.data_w / 2, -1 do + p = self:point(i, j); p.dir = 6 + if self.data[j][i] ~= ' ' then possibles[p:hashCoord(self)] = p break end + end + end + + for _, p in pairs(possibles) do checkexits(p.x, p.y, p.dir) end + + table.sort(list, "dist") + return list +end + +function _M:findClosestExit(pos, kind, exitable_chars) + local list = self:findExits(pos, kind, exitable_chars) + if #list == 0 then return nil end + local c = list[1] + return c.pos, c.kind, c.dist +end + +function _M:findFurthestExit(pos, kind, exitable_chars) + local list = self:findExits(pos, kind, exitable_chars) + if #list == 0 then return nil end + local c = list[#list] + return c.pos, c.kind, c.dist +end + +function _M:findRandomClosestExit(nb, pos, kind, exitable_chars) + local list = self:findExits(pos, kind, exitable_chars) + if #list == 0 then return nil end + local c = list[rng.range(1, math.min(#list, nb))] + return c.pos, c.kind, c.dist +end + +function _M:findRandomFurthestExit(nb, pos, kind, exitable_chars) + local list = self:findExits(pos, kind, exitable_chars) + if #list == 0 then return nil end + local c = list[rng.range(math.max(1, #list-nb+1), #list)] + return c.pos, c.kind, c.dist +end + +function _M:findRandomExit(pos, kind, exitable_chars) + local list = self:findExits(pos, kind, exitable_chars) + if #list == 0 then return nil end + local c = rng.table(list) + return c.pos, c.kind, c.dist +end + +function _M:smartDoor(pos, chance, door_char, walls) + if pos.x <= 1 or pos.y <= 1 or pos.x >= self.data_w or pos.y >= self.data_h then return false end + + walls = self:makeCharsTable(walls, {'#','â“'}) + + local c8 = self.data[pos.y-1][pos.x] + local c2 = self.data[pos.y+1][pos.x] + local c4 = self.data[pos.y][pos.x-1] + local c6 = self.data[pos.y][pos.x+1] + + if (walls[c8] and walls[c2]) and not walls[c4] and not walls[c6] and rng.percent(chance) then + if door_char then self.data[pos.y][pos.x] = door_char end + return true + elseif (walls[c4] and walls[c6]) and not walls[c2] and not walls[c8] and rng.percent(chance) then + if door_char then self.data[pos.y][pos.x] = door_char end + return true + else + return false + end +end + +------------------------------------------------------------------------------ +-- Simple tunneling +------------------------------------------------------------------------------ + +function _M:initTunneling() + if not self.tunnels_map then + self.tunnels_map = self:makeData(self.data_w, self.data_h, false) + self.tunnels_next_id = 'a' + end + local id = self.tunnels_next_id + self.tunnels_next_id = string.char(string.byte(self.tunnels_next_id) + 1) + return id +end + +--- Random tunnel dir (no diagonals) +function _M:tunnelRandDir(sx, sy) + local dirs = util.primaryDirs() --{4,6,8,2} + return util.dirToCoord(dirs[rng.range(1, #dirs)], sx, sy) +end + +--- Find the direction in which to tunnel (no diagonals) +function _M:tunnelDir(x1, y1, x2, y2) + -- HEX TODO ? + local xdir = (x1 == x2) and 0 or ((x1 < x2) and 1 or -1) + local ydir = (y1 == y2) and 0 or ((y1 < y2) and 1 or -1) + if xdir ~= 0 and ydir ~= 0 then + if rng.percent(50) then xdir = 0 + else ydir = 0 + end + end + return xdir, ydir +end + +--- Marks a tunnel as a tunnel and the space behind it +function _M:tunnelMark(x, y, xdir, ydir, id) + x, y = x - xdir, y - ydir + local dir = util.coordToDir(xdir, ydir, x, y) + local sides = util.dirSides(dir, x, y) + local mark_dirs = {dir, sides.left, sides.right} + for i, d in ipairs(mark_dirs) do + local xd, yd = util.dirToCoord(d, x, y) + if self:isBound(x+xd, y+yd) and not self.tunnels_map[y+yd][x+xd] then + self.tunnels_map[y+yd][x+xd] = id + -- print("mark tunnel", x+xd, y+yd , id) + end + end + if not self.tunnels_map[y][x] then + self.tunnels_map[y][x] = id + -- print("mark tunnel", x, y , id) + end +end + +--- Tunnel between two points +-- @param x1, y1 starting coordinates +-- @param x2, y2 ending coordinates +-- @param id tunnel id +-- @param virtual set true to mark the tunnel without changing terrain +function _M:tunnel(from, to, char, tunnel_through, tunnel_avoid, config, virtual) + local x1, y1, x2, y2 = from.x, from.y, to.x, to.y + config = config or {} + config.tunnel_change = config.tunnel_change or 60 + config.tunnel_random = config.tunnel_random or 7 + + char = char or '.' + tunnel_through = table.reverse(tunnel_through or {'ALL'}) + tunnel_avoid = table.reverse(tunnel_avoid or {'â“'}) + + local id = self:initTunneling() + + if x1 == x2 and y1 == y2 then return end + -- Disable the many prints of tunnelling + -- local print = function()end + + local xdir, ydir = self:tunnelDir(x1, y1, x2, y2) + -- print("tunneling from",x1, y1, "to", x2, y2, "initial dir", xdir, ydir) + + local startx, starty = x1, y1 + local tun = {} + + local tries = 2000 + local no_move_tries = 0 + while tries > 0 do + if rng.percent(config.tunnel_change) then + if rng.percent(config.tunnel_random) then xdir, ydir = self:tunnelRandDir(x1, x2) + else xdir, ydir = self:tunnelDir(x1, y1, x2, y2) + end + end + + local nx, ny = x1 + xdir, y1 + ydir + while true do + if self:isBound(nx, ny) then break end + + if rng.percent(config.tunnel_random) then xdir, ydir = self:tunnelRandDir(nx, ny) + else xdir, ydir = self:tunnelDir(x1, y1, x2, y2) + end + nx, ny = x1 + xdir, y1 + ydir + end + -- print(feat, "try pos", nx, ny, "dir", util.coordToDir(xdir, ydir, nx, ny)) + local nc = self.data[ny][nx] + + if tunnel_avoid[nc] then + if nx == from.x and ny == from.y then + tun[#tun+1] = {nx,ny} + x1, y1 = nx, ny + -- print(feat, "accept avoid (start)", nc) + elseif nx == to.x and ny == to.y then + tun[#tun+1] = {nx,ny} + x1, y1 = nx, ny + -- print(feat, "accept avoid (end)", nc) + else + -- print(feat, "reject avoid", nc) + if nx == x2 and ny == y2 then -- stop if next to special target + x1, y1 = nx, ny + -- print(feat, "end adjacent to special target") + end + end + -- elseif nc.can_open ~= nil then + -- if nc.can_open then + -- print(feat, "tunnel crossing can_open", nx,ny) + -- for _, coord in pairs(util.adjacentCoords(nx, ny)) do + -- if self:isBound(coord[1], coord[2]) then + -- self.map.room_map[coord[1]][coord[2]].can_open = false + -- print(feat, "forbidding crossing at ", coord[1], coord[2]) + -- end + -- end + -- tun[#tun+1] = {nx,ny,true} + -- x1, y1 = nx, ny + -- print(feat, "accept can_open") + -- else + -- print(feat, "reject can_open") + -- end + elseif self.tunnels_map[ny][nx] then + if no_move_tries >= 15 then + tun[#tun+1] = {nx,ny} + x1, y1 = nx, ny + -- print(feat, "accept tunnel", nc, id) + else + -- print(feat, "reject tunnel", nc, id) + end + elseif tunnel_through[nc] or tunnel_through.ALL then + tun[#tun+1] = {nx,ny} + x1, y1 = nx, ny + -- print(feat, "accept normal", nc) + else + tun[#tun+1] = {nx,ny,"ignore"} + x1, y1 = nx, ny + -- print(feat, "reject normal", nc) + end + + if x1 == nx and y1 == ny then + self:tunnelMark(x1, y1, xdir, ydir, id) + no_move_tries = 0 + else + no_move_tries = no_move_tries + 1 + end + + if x1 == x2 and y1 == y2 then break end + + tries = tries - 1 + end + + local doors = {} + self.possible_doors = self.possible_doors or {} + for _, t in ipairs(tun) do + local nx, ny = t[1], t[2] + -- if t[3] and self.data.door then self.possible_doors[#self.possible_doors+1] = t end + if t[3] ~= "ignore" and not virtual then + -- print("=======TUNN", nx, ny) + -- self.map(nx, ny, Map.TERRAIN, self:resolve('=') or self:resolve('.') or self:resolve('floor')) + self:put(self:point(nx, ny), char) + end + end +end + +------------------------------------------------------------------------------ +-- A* tunneling +------------------------------------------------------------------------------ + +--- The default heuristic for A*, tries to come close to the straight path +-- @int sx +-- @int sy +-- @int cx +-- @int cy +-- @int tx +-- @int ty +local function heuristicCloserPath(sx, sy, cx, cy, tx, ty, config) + local h + -- Chebyshev distance + h = math.max(math.abs(tx - cx), math.abs(ty - cy)) + + -- tie-breaker rule for straighter paths + local dx1 = cx - tx + local dy1 = cy - ty + local dx2 = sx - tx + local dy2 = sy - ty + return h + 0.01*math.abs(dx1*dy2 - dx2*dy1) + rng.float(-config.erraticness, config.erraticness) +end + +--- A simple heuristic for A*, using distance +-- @int sx +-- @int sy +-- @int cx +-- @int cy +-- @int tx +-- @int ty +local function heuristicDistance(sx, sy, cx, cy, tx, ty) + return core.fov.distance(cx, cy, tx, ty) + rng.float(-config.erraticness, config.erraticness) +end + +--- Converts x & y into a single value +-- @see astarToDouble +-- @int x +-- @int y +function _M:astarToSingle(x, y) + return x + y * self.data_w +end + +--- Converts a single value back into x & y +-- @see astarToSingle +-- @int c +function _M:astarToDouble(c) + local y = math.floor(c / self.data_w) + return c - y * self.data_w, y +end + +--- Create Path +-- @param came_from +-- @param cur +function _M:astarCreatePath(came_from, from, to, cur, id, char, config, tunnel_through) + if not came_from[cur] then return end + local rpath, path = {}, {} + while came_from[cur] do + local x, y = self:astarToDouble(cur) + rpath[#rpath+1] = self:point(x, y) + if not config.virtual and (tunnel_through.ALL or tunnel_through[self.data[y][x]]) then self:put(self:point(x, y), char) end + self.tunnels_map[y][x] = id + cur = came_from[cur] + end + + -- First one + rpath[#rpath+1] = from + if not config.virtual then self:put(from, char) end + self.tunnels_map[from.y][from.x] = id + + -- Last one + rpath[#rpath+1] = to + if not config.virtual then self:put(to, char) end + self.tunnels_map[to.y][to.x] = id + + for i = #rpath, 1, -1 do path[#path+1] = rpath[i] end + return path +end + +--- Compute path from sx/sy to tx/ty +function _M:tunnelAStar(from, to, char, tunnel_through, tunnel_avoid, config) + print("ASTAR Tunnel from ", from, "to", to) + local sx, sy, tx, ty = from.x, from.y, to.x, to.y + config = config or {} + + char = char or '.' + tunnel_through = table.reverse(tunnel_through or {'ALL'}) + tunnel_avoid = table.reverse(tunnel_avoid or {'â“'}) + + local id = self:initTunneling() + + if sx == tx and sy == ty then return end + + config.weights = config.weights or {} + config.weight_erraticness = config.weight_erraticness or 0 + config.erraticness = config.erraticness or 9 + config.weight_tunnel = config.weight_tunnel or 20 + if type(config.forbid_diagonals) == "nil" then config.forbid_diagonals = true end + + local heur = config.heuristic or heuristicCloserPath + local w, h = self.data_w, self.data_h + local start = self:astarToSingle(sx, sy) + local stop = self:astarToSingle(tx, ty) + local open = {[start]=true} + local closed = {} + local g_score = {[start] = 0} + local h_score = {[start] = heur(sx, sy, sx, sy, tx, ty, config)} + local f_score = {[start] = heur(sx, sy, sx, sy, tx, ty, config)} + local came_from = {} + + if not self:isBound(sx, sy) or not self:isBound(tx, ty) then + print("Astar fail: source/destination unreachable") + return nil + end + local checkPos = function(node, nx, ny) + local npos = self:point(nx, ny) + local nnode = self:astarToSingle(nx, ny) + if not closed[nnode] and self:isBound(nx, ny) and ( + ( + npos == from or npos == to -- Always allow on start & stop + ) + or + ( + (not tunnel_avoid[self.data[ny][nx]]) and -- Avoid tunneling in + (not config.add_check or config.add_check(nx, ny)) -- Extra checks + ) + ) then + local nc = self.data[ny][nx] + local score = 1 + if config.weight_erraticness > 0 then score = score + rng.float(0, config.weight_erraticness) end + if config.weights[nc] then score = score + config.weights[nc] end + if self.tunnels_map[ny][nx] then score = score + config.weight_tunnel end + local tent_g_score = g_score[node] + score -- we can adjust here for difficult passable terrain + local tent_is_better = false + if not open[nnode] then open[nnode] = true; tent_is_better = true + elseif tent_g_score < g_score[nnode] then tent_is_better = true + end + + if tent_is_better then + came_from[nnode] = node + g_score[nnode] = tent_g_score + h_score[nnode] = heur(sx, sy, tx, ty, nx, ny, config) + f_score[nnode] = g_score[nnode] + h_score[nnode] + end + end + end + + while next(open) do + -- Find lowest of f_score + local node, lowest = nil, 999999999999999 + local n, _ = next(open) + while n do + if f_score[n] < lowest then node = n; lowest = f_score[n] end + n, _ = next(open, n) + end + + if node == stop then return self:astarCreatePath(came_from, from, to, stop, id, char, config, tunnel_through) end + + open[node] = nil + closed[node] = true + local x, y = self:astarToDouble(node) + + -- Check sides + for _, coord in pairs(util.adjacentCoords(x, y, config.forbid_diagonals)) do + checkPos(node, coord[1], coord[2]) + end + end +end diff --git a/game/engines/default/engine/tilemaps/WaveFunctionCollapse.lua b/game/engines/default/engine/tilemaps/WaveFunctionCollapse.lua new file mode 100644 index 0000000000000000000000000000000000000000..09445b89c075c528dc34b8e4387a2a645860122c --- /dev/null +++ b/game/engines/default/engine/tilemaps/WaveFunctionCollapse.lua @@ -0,0 +1,95 @@ +-- TE4 - T-Engine 4 +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +require "engine.class" +local Tilemap = require "engine.tilemaps.Tilemap" + +--- Generate map-like data from samples using the WaveFunctionCollapse algorithm (in C++) +-- @classmod engine.tilemaps.WaveFunctionCollapse +module(..., package.seeall, class.inherit(Tilemap)) + +--- Run the algorithm +-- It will produce internal results which this class can then manipulate +-- If async is true in the parameters the generator will run in an asynchronous thread and you must call :waitCompute() +-- before using the results, this allows you to run multiple WFCs for later merging wihtout taking more time (if the user have enoguh CPUs obviously) +function _M:init(t) + assert(t.mode == "overlapping", "bad WaveFunctionCollapse mode") + assert(t.size, "WaveFunctionCollapse has no size") + + Tilemap.init(self) + + self:setSize(t.size[1], t.size[2], ' ') + + if t.mode == "overlapping" then + if type(t.sample) == "string" then + t.sample = self:collapseToLineFormat(self:tmxLoad(t.sample)) + end + self:run(t) + end +end + +--- Used internally to parse the results +function _M:parseResult(data) + print("[WaveFunctionCollapse] compute done", data) + if not data then return end + self.data = {} + for y = 1, #data do + local x = 1 + self.data[y] = {} + for c in data[y]:gmatch('.') do + self.data[y][x] = c + x = x + 1 + end + end +end + +--- Called by the constructor to actaully start doing stuff +function _M:run(t) + print("[WaveFunctionCollapse] running with parameter table:") + table.print(t) + if not t.async then + local data = core.generator.wfc.overlapping(t.sample, t.size[1], t.size[2], t.n, t.symmetry, t.periodic_out, t.periodic_in, t.has_foundation) + self:parseResult(data) + return false + else + self.async_data = core.generator.wfc.asyncOverlapping(t.sample, t.size[1], t.size[2], t.n, t.symmetry, t.periodic_out, t.periodic_in, t.has_foundation) + return true + end +end + +--- Wait for computation to finish if in async mode, if not it just returns immediately +function _M:waitCompute() + if not self.async_data then return end + local data = self.async_data:wait() + self.async_data = nil + + self:parseResult(data) + return self:hasResult() +end + +--- Wait for multiple WaveFunctionCollapse at once +-- Static +function _M:waitAll(...) + local all_have_data = true + for _, wfcasync in ipairs{...} do + wfcasync:waitCompute() + if not wfcasync:hasResult() then all_have_data = false end -- We cant break, we need to wait all the threads to not leave them dangling in the wind + end + return all_have_data +end diff --git a/game/engines/default/engine/utils.lua b/game/engines/default/engine/utils.lua index b4739d43c30a604550583b8c0a3439e15c0e594f..50be20969d4493aadbe2479af18240d2454d16aa 100644 --- a/game/engines/default/engine/utils.lua +++ b/game/engines/default/engine/utils.lua @@ -89,6 +89,15 @@ function ripairs(t) end end +function table.empty(t) + while next(t) do t[next(t)] = nil end +end + +function table.replaceWith(t, nt) + table.empty(t) + for k, e in pairs(nt) do t[k] = e end +end + function table.count(t) local i = 0 for k, v in pairs(t) do diff --git a/game/modules/tome/class/Game.lua b/game/modules/tome/class/Game.lua index 65ef082ba9a2495aaf7df1975e9d30c0e3456407..c95912a50410070d91dff77a73b5fd37fe05da7d 100644 --- a/game/modules/tome/class/Game.lua +++ b/game/modules/tome/class/Game.lua @@ -1996,7 +1996,11 @@ function _M:setupCommands() print("===============") end end, [{"_g","ctrl"}] = function() if config.settings.cheat then - self:changeLevel(1, "cults+godfeaster") + if self.zone.short_name ~= "test" then + self:changeLevel(1, "test") + else + self:changeLevel(game.level.level + 1) + end do return end local m = game.zone:makeEntity(game.level, "actor", {name="elven mage"}, nil, true) local x, y = util.findFreeGrid(game.player.x, game.player.y, 20, true, {[Map.ACTOR]=true}) diff --git a/game/modules/tome/data/mapscripts/lib/connect_rooms_linear.lua b/game/modules/tome/data/mapscripts/lib/connect_rooms_linear.lua new file mode 100644 index 0000000000000000000000000000000000000000..936dd98166b330b2cc87f8d0e6bb5f9e2af056f5 --- /dev/null +++ b/game/modules/tome/data/mapscripts/lib/connect_rooms_linear.lua @@ -0,0 +1,41 @@ +-- TE4 - T-Engine 4 +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +local map = args.map +local rooms = map:sortListCenter(args.rooms) + +local full = true +for i, room in ipairs(rooms) do + if i > 1 then + local proom = rooms[i-1] + local pos1, kind1 = proom:findRandomClosestExit(7, room:centerPoint(), nil, args.exitable_chars or {'.', ';', '='}) + local pos2, kind2 = room:findRandomClosestExit(7, proom:centerPoint(), nil, args.exitable_chars or {'.', ';', '='}) + if pos1 and pos2 then + map:tunnelAStar(pos1, pos2, args.tunnel_char or '.', args.tunnel_through or {'#'}, args.tunnel_avoid or nil, {erraticness=args.erraticness or 5}) + if kind1 == 'open' then map:smartDoor(pos1, args.door_chance or 40, '+') end + if kind2 == 'open' then map:smartDoor(pos2, args.door_chance or 40, '+') end + else + full = false + end + + end + -- map:carveArea(string.char(string.byte('0')+i-1), room.merged_pos, room.merged_pos + room.data_size - 1) +end + +return full diff --git a/game/modules/tome/data/mapscripts/lib/connect_rooms_multi.lua b/game/modules/tome/data/mapscripts/lib/connect_rooms_multi.lua new file mode 100644 index 0000000000000000000000000000000000000000..b414170d0ce28fe87f36e96163064845306bb9f3 --- /dev/null +++ b/game/modules/tome/data/mapscripts/lib/connect_rooms_multi.lua @@ -0,0 +1,69 @@ +-- TE4 - T-Engine 4 +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + + +-- This file uses Kruskals algorithm to find a MST(minimum spanning tree) in a graph of rooms + +local MST = require "engine.algorithms.MST" + +local max_links = args.max_links or 3 +local map = args.map +local rooms = args.rooms + +if #rooms <= 1 then return true end -- Easy ! + +local mstrun = MST.new() + +-- Generate all possible edges +for i, room in ipairs(rooms) do + local c = room:centerPoint() + for j, proom in ipairs(rooms) do if proom ~= room then + local c1, c2 = room:centerPoint(), proom:centerPoint() + mstrun:edge(room, proom, core.fov.distance(c1.x, c1.y, c2.x, c2.y)) + end end +end + +-- Compute! +mstrun:run() + +-- Add some more randomly selected edges +mstrun:fattenRandom(args.edges_surplus or 0) + +-- Draw the paths +local full = true +for _, edge in pairs(mstrun.mst) do + local pos1, kind1 + local pos2, kind2 + if args.from_center then + pos1, kind1 = edge.to:centerPoint(), 'open' + pos2, kind2 = edge.from:centerPoint(), 'open' + else + pos1, kind1 = edge.from:findRandomClosestExit(7, edge.to:centerPoint(), nil, args.exitable_chars or {'.', ';', '='}) + pos2, kind2 = edge.to:findRandomClosestExit(7, edge.from:centerPoint(), nil, args.exitable_chars or {'.', ';', '='}) + end + if pos1 and pos2 then + map:tunnelAStar(pos1, pos2, args.tunnel_char or '.', args.tunnel_through or {'#'}, args.tunnel_avoid or nil, {erraticness=args.erraticness or 5}) + if kind1 == 'open' then map:smartDoor(pos1, args.door_chance or 40, '+') end + if kind2 == 'open' then map:smartDoor(pos2, args.door_chance or 40, '+') end + else + full = false + end +end + +return full diff --git a/game/modules/tome/data/rooms/pod.lua b/game/modules/tome/data/rooms/pod.lua index 0dfb6ac8b97c6cdf4c868f5c63099cb20b38d891..0a2fc0bdfe501e9ec94c97acbba04b70cb4c9c6c 100644 --- a/game/modules/tome/data/rooms/pod.lua +++ b/game/modules/tome/data/rooms/pod.lua @@ -19,7 +19,7 @@ return function(gen, id) local w = rng.table{4,6,8,10} - local h = h + local h = w local function make_pod(self, x, y, is_lit) gen:makePod(x + w / 2, y + h / 2, w, id) end diff --git a/game/modules/tome/data/zones/test/grids.lua b/game/modules/tome/data/zones/test/grids.lua index 53c2b6457ab2d3847bc0e177acce411fd87a6098..c1838f78d71b1c60e54420b0de2d5baf2880879f 100644 --- a/game/modules/tome/data/zones/test/grids.lua +++ b/game/modules/tome/data/zones/test/grids.lua @@ -19,3 +19,5 @@ load("/data/general/grids/basic.lua") load("/data/general/grids/forest.lua") +load("/data/general/grids/water.lua") +load("/data/general/grids/jungle_hut.lua") diff --git a/game/modules/tome/data/zones/test/mapscripts/bsp_islands.lua b/game/modules/tome/data/zones/test/mapscripts/bsp_islands.lua new file mode 100644 index 0000000000000000000000000000000000000000..b85796e4080080a917627abd3c2fc859a95a9698 --- /dev/null +++ b/game/modules/tome/data/zones/test/mapscripts/bsp_islands.lua @@ -0,0 +1,43 @@ +-- ToME - Tales of Maj'Eyal +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +local BSP = require "engine.tilemaps.BSP" + +-- rng.seed(2) + +local tm = Tilemap.new(self.mapsize, '=', 1) + +local bsp = BSP.new(10, 10, 2):make(50, 50, nil, '=') + +for _, room in ipairs(bsp.rooms) do + local pond = Heightmap.new(1.6, {up_left=0, down_left=0, up_right=0, down_right=0, middle=1}):make(room.map.data_w, room.map.data_h, {' ', ' ', ';', ';', 'T', ';', ';', ';'}) + -- Ensure exit from the lake to exterrior + local pond_exit = pond:findRandomExit(pond:centerPoint(), nil, {';'}) + pond:tunnelAStar(pond:centerPoint(), pond_exit, ';', {'T'}, {}, {erraticness=9}) + -- If lake is big enough and we find a spot, place it + if pond:eliminateByFloodfill{'T', ' '} < 8 then return self:regenerate() end + + room.map:merge(1, 1, pond) +end + +tm:merge(1, 1, bsp) + +-- if tm:eliminateByFloodfill{'T','#'} < 800 then return self:regenerate() end + +return tm diff --git a/game/modules/tome/data/zones/test/mapscripts/cavernous_forest.lua b/game/modules/tome/data/zones/test/mapscripts/cavernous_forest.lua new file mode 100644 index 0000000000000000000000000000000000000000..4dfdaf227228e3fa51e3e07c8cc20be59a798b0a --- /dev/null +++ b/game/modules/tome/data/zones/test/mapscripts/cavernous_forest.lua @@ -0,0 +1,38 @@ +-- ToME - Tales of Maj'Eyal +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +-- rng.seed(2) + +local tm = Tilemap.new(self.mapsize, '#') + +local noise = Noise.new("simplex", 0.2, 4, 12, 1):make(50, 50, {'T', 'T', ';', ';', '=', '='}) + +-- Eliminate water ponds that are too small +noise:applyOnGroups(noise:findGroupsOf{'='}, function(room, idx) + if #room.list < 18 then noise:fillGroup(room, ';') end + -- room.map:carveArea('#', room.map:point(1, 1), room.map.data_size) +end) + +-- MAKE MINIMUM SPANNING TREE A GENERIC ALGORITHM IN UTIL + +tm:merge(1, 1, noise) + +-- if tm:eliminateByFloodfill{'T','#'} < 800 then return self:regenerate() end + +return tm diff --git a/game/modules/tome/data/zones/test/mapscripts/inner_outer.lua b/game/modules/tome/data/zones/test/mapscripts/inner_outer.lua new file mode 100644 index 0000000000000000000000000000000000000000..9c1478f3c81b588aaa0dba8211115d3fa6a21b07 --- /dev/null +++ b/game/modules/tome/data/zones/test/mapscripts/inner_outer.lua @@ -0,0 +1,112 @@ +-- ToME - Tales of Maj'Eyal +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +local merge_order = {'.', '_', 'r', '+', '#', 'O', ';', '=', 'T'} + +-- Water & trees layer +local wfcwater = WaveFunctionCollapse.new{ + mode="overlapping", async=true, + sample=self:getFile("!wfctest2.tmx", "samples"), + size={self.mapsize.w/3, self.mapsize.h}, + n=3, symmetry=8, periodic_out=true, periodic_in=true, has_foundation=false +} +-- Outer buildings +local wfcouter = WaveFunctionCollapse.new{ + mode="overlapping", async=true, + sample=self:getFile("!wfctest.tmx", "samples"), + size={self.mapsize.w/3, self.mapsize.h}, + n=3, symmetry=8, periodic_out=true, periodic_in=true, has_foundation=false +} +-- Inner buildings +local wfcinner = WaveFunctionCollapse.new{ + mode="overlapping", async=true, + sample=self:getFile("!wfctest5.tmx", "samples"), + size={self.mapsize.w/2, self.mapsize.h-2}, + n=3, symmetry=8, periodic_out=false, periodic_in=false, has_foundation=false +} + +-- Load predrawn stuff +local doorway = Static.new(self:getFile("!door.tmx", "samples")) +local doorwaytunnel = doorway:locateTile('E', '.') + +-- Wait for all generators to finish +if not WaveFunctionCollapse:waitAll(wfcinner, wfcwater, wfcouter) then print("[inner_outer] a WFC failed") return self:regenerate() end + +-- Merge them all +local tm = Tilemap.new(self.mapsize) +wfcouter:merge(1, 1, wfcwater, merge_order) +if wfcouter:eliminateByFloodfill{'#', 'T'} < 400 then print("[inner_outer] outer is too small") return self:regenerate() end +if wfcinner:eliminateByFloodfill{'#', 'T'} < 400 then print("[inner_outer] inner is too small") return self:regenerate() end +tm:merge(1, 1, wfcouter, merge_order) +tm:merge(self.mapsize.w - wfcinner.data_w, 2, wfcinner, merge_order) + +-- Place the doorway and tunnel from it to the dungeon part on the right +local doorwaypos = tm:point(self.mapsize.w / 3, (self.mapsize.h - doorway.data_h) / 2) +tm:merge(doorwaypos.x, doorwaypos.y, doorway) +tm:carveLinearPath('.', doorwaytunnel + doorwaypos, 6, '.') + +-- Find rooms +local rooms = tm:findGroupsOf{'r'} +local noroomforest = true +tm:applyOnGroups(rooms, function(room, idx) + local p1, p2, rw, rh = tm:groupOuterRectangle(room) + print("ROOM", idx, "::", rw, rh, "=>", rw * rh) + tm:fillGroup(room, '.') + if noroomforest and rw >= 8 and rh >= 8 then + local pond = Heightmap.new(1.6, {up_left=0, down_left=0, up_right=0, down_right=0, middle=1}):make(rw-2, rh-2, {' ', ';', ';', 'T', '=', '='}) + pond:printResult() + -- tm:fillGroup(room, ';') + tm:merge(p1.x+1, p1.y+1, pond) + noroomforest = false + end + + self:addSpot(p1.x + rw/2, p1.y + rh/2, "spawn-spot", "spawn-spot") + + -- table.print() + for j = 1, #room.list do + local jn = room.list[j] + -- data[jn.y][jn.x] = tostring(idx) + end +end) +if noroomforest then return self:regenerate() end + +-- Complete the map by putting wall in all the remaining blank spaces +tm:fillAll() + +-- Elimitate the rest +if tm:eliminateByFloodfill{'#', 'T'} < 400 then return self:regenerate() end + +tm:printResult() + +-- print('---==============---') +-- local noise = Noise.new(nil, 0.5, 2, 3, 6):make(80, 50, {'T', 'T', '=', '=', '=', ';', ';'}) +-- noise:printResult() +-- print('---==============---') +-- print('---==============---') +-- local pond = Heightmap.new(1.9, {up_left=0, down_left=0, up_right=0, down_right=0, middle=1}):make(30, 30, {';', 'T', '=', '=', ';'}) +-- pond:printResult() +-- print('---==============---') +-- print('---==============---') +-- local maze = Maze.new():makeSimple(31, 31, '.', {'#','T'}, true) +-- maze:printResult() +-- print('---==============---') + +-- DGDGDGDG: make at least Tilemap handlers for BSP, roomer (single room), roomers and correctly handle up/down stairs + +return tm diff --git a/game/modules/tome/data/zones/test/mapscripts/rooms_test.lua b/game/modules/tome/data/zones/test/mapscripts/rooms_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..94a0f8485727d3a94c6a5c4d7f538b8b3571c651 --- /dev/null +++ b/game/modules/tome/data/zones/test/mapscripts/rooms_test.lua @@ -0,0 +1,84 @@ +-- ToME - Tales of Maj'Eyal +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +-- rng.seed(2) + +local tm = Tilemap.new(self.mapsize, '#') + +-- self.data.greater_vaults_list = {"32-chambers"} +local room_factory = Rooms.new(self, "random_room") + +local rooms = {} +for i = 1, 20 do + local proom = room_factory:generateRoom() + local pos = tm:findRandomArea(nil, tm.data_size, proom.data_w, proom.data_h, '#', 1) + if pos then + tm:merge(pos, proom:build()) + rooms[#rooms+1] = proom + end +end + +local up_stairs = true +for i = 1, 20 do + -- Make a little lake + local r = rng.range(7, 15) + local pond = Heightmap.new(1.6, {up_left=0, down_left=0, up_right=0, down_right=0, middle=1}):make(r, r, {' ', ' ', ';', ';', 'T', '=', '=', up_stairs and '<' or '='}) + -- Ensure exit from the lake to exterrior + local pond_exit = pond:findRandomExit(pond:centerPoint(), nil, {';'}) + pond:tunnelAStar(pond:centerPoint(), pond_exit, ';', {'T'}, {}, {erraticness=9}) + -- If lake is big enough and we find a spot, place it + if pond:eliminateByFloodfill{'T', ' '} > 8 then + local pos = tm:findRandomArea(nil, tm.data_size, pond.data_w, pond.data_h, '#', 1) + if pos then + tm:merge(pos, pond) + rooms[#rooms+1] = pond + up_stairs = false + end + end +end + +if not loadMapScript("lib/connect_rooms_multi", {map=tm, rooms=rooms, edges_surplus=0}) then return self:regenerate() end +-- loadMapScript("lib/connect_rooms_multi", {map=tm, rooms=rooms}) + + +self:setEntrance(tm:locateTile('<')) +self:setExit(rooms[#rooms]:centerPoint()) tm:put(rooms[#rooms]:centerPoint(), '>') + +-- Elimitate the rest +-- if tm:eliminateByFloodfill{'#', 'T'} < 600 then return self:regenerate() end + +tm:printResult() + + +-- print('---==============---') +-- local noise = Noise.new(nil, 0.5, 2, 3, 6):make(80, 50, {'T', 'T', '=', '=', '=', ';', ';'}) +-- noise:printResult() +-- print('---==============---') +-- print('---==============---') +-- local pond = Heightmap.new(1.9, {up_left=0, down_left=0, up_right=0, down_right=0, middle=1}):make(30, 30, {';', 'T', '=', '=', ';'}) +-- pond:printResult() +-- print('---==============---') +-- print('---==============---') +-- local maze = Maze.new():makeSimple(31, 31, '.', {'#','T'}, true) +-- maze:printResult() +-- print('---==============---') + +-- DGDGDGDG: make at least Tilemap handlers for BSP, roomer (single room), roomers and correctly handle up/down stairs + +return tm diff --git a/game/modules/tome/data/zones/test/mapscripts/testroom.lua b/game/modules/tome/data/zones/test/mapscripts/testroom.lua new file mode 100644 index 0000000000000000000000000000000000000000..ce258495d5bfa0d37adf7efc2d309bb2bbe31367 --- /dev/null +++ b/game/modules/tome/data/zones/test/mapscripts/testroom.lua @@ -0,0 +1,49 @@ +-- ToME - Tales of Maj'Eyal +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +-- Merge them all +local tm = Tilemap.new(self.mapsize, '#') +tm:carveArea(';', tm:point(1, 1), tm:point(4, 10)) +tm:carveArea('T', tm:point(15, 3), tm:point(15, 16)) +tm:carveArea(';', tm:point(30, 1), tm:point(35, 10)) + +-- tm:tunnel(tm:point(1, 10), tm:point(36, 10), ';', nil, {'T'}, {tunnel_change=60, tunnel_random=5}) +-- tm:tunnelAStar(tm:point(1, 10), tm:point(36, 1), '=', nil, {'T'}, {}) +tm:tunnelAStar(tm:point(1, 1), tm:point(36, 10), '.', nil, {'T'}, {}) +-- tm:tunnelAStar(tm:point(1, 30), tm:point(36, 30), '.', nil, {}, {}) + +tm:printResult() + + +-- print('---==============---') +-- local noise = Noise.new(nil, 0.5, 2, 3, 6):make(80, 50, {'T', 'T', '=', '=', '=', ';', ';'}) +-- noise:printResult() +-- print('---==============---') +-- print('---==============---') +-- local pond = Heightmap.new(1.9, {up_left=0, down_left=0, up_right=0, down_right=0, middle=1}):make(30, 30, {';', 'T', '=', '=', ';'}) +-- pond:printResult() +-- print('---==============---') +-- print('---==============---') +-- local maze = Maze.new():makeSimple(31, 31, '.', {'#','T'}, true) +-- maze:printResult() +-- print('---==============---') + +-- DGDGDGDG: make at least Tilemap handlers for BSP, roomer (single room), roomers and correctly handle up/down stairs + +return tm diff --git a/game/modules/tome/data/zones/test/mapscripts/testroom2.lua b/game/modules/tome/data/zones/test/mapscripts/testroom2.lua new file mode 100644 index 0000000000000000000000000000000000000000..5365ae0dcecac9be7b7c25c47de9baf76772fea0 --- /dev/null +++ b/game/modules/tome/data/zones/test/mapscripts/testroom2.lua @@ -0,0 +1,75 @@ +-- ToME - Tales of Maj'Eyal +-- Copyright (C) 2009 - 2018 Nicolas Casalini +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see <http://www.gnu.org/licenses/>. +-- +-- Nicolas Casalini "DarkGod" +-- darkgod@te4.org + +local BSP = require "engine.tilemaps.BSP" +local MST = require "engine.algorithms.MST" + +-- rng.seed(1) + +local tm = Tilemap.new(self.mapsize, '=', 1) + +local bsp = BSP.new(4, 4, 10):make(50, 50, '.', '#') + +-- Remove a few rooms +for i = 1, #bsp.rooms / 4 do + local room = rng.tableRemove(bsp.rooms) + room.map:carveArea('#', room.map:point(1, 1), room.map.data_size) +end + +local mstrun = MST.new() + +-- Generate all possible edges +for i, room1 in ipairs(bsp.rooms) do + local c = room1.map:centerPoint() + for j, room2 in ipairs(bsp.rooms) do if room1 ~= room2 then + local c1, c2 = room1.map:centerPoint(), room2.map:centerPoint() + mstrun:edge(room1, room2, core.fov.distance(c1.x, c1.y, c2.x, c2.y)) + end end +end + +-- Compute! +mstrun:run() + +for _, edge in pairs(mstrun.mst) do + if edge.from.from.x - 1 == edge.to.to.x + 1 then + local min_y, max_y = math.max(edge.from.from.y, edge.to.from.y), math.min(edge.from.to.y, edge.to.to.y) + bsp:put(bsp:point(edge.from.from.x - 1, rng.range(min_y, max_y)), rng.percent(40) and '+' or '.') + elseif edge.from.to.x + 1 == edge.to.from.x - 1 then + local min_y, max_y = math.max(edge.from.from.y, edge.to.from.y), math.min(edge.from.to.y, edge.to.to.y) + bsp:put(bsp:point(edge.from.to.x + 1, rng.range(min_y, max_y)), rng.percent(40) and '+' or '.') + elseif edge.from.from.y - 1 == edge.to.to.y + 1 then + local min_x, max_x = math.max(edge.from.from.x, edge.to.from.x), math.min(edge.from.to.x, edge.to.to.x) + bsp:put(bsp:point(rng.range(min_x, max_x), edge.from.from.y - 1), rng.percent(40) and '+' or '.') + elseif edge.from.to.y + 1 == edge.to.from.y - 1 then + local min_x, max_x = math.max(edge.from.from.x, edge.to.from.x), math.min(edge.from.to.x, edge.to.to.x) + bsp:put(bsp:point(rng.range(min_x, max_x), edge.from.to.y + 1), rng.percent(40) and '+' or '.') + end +end + +bsp:applyOnGroups(bsp:findGroupsOf{'.', '+'}, function(room, idx) + if room.map.data_size:area() < 10 then + room.map:carveArea('#', room.map:point(1, 1), room.map.data_size) + end +end) +-- if bsp:eliminateByFloodfill{'#'} < 10 then return self:regenerate() end + +tm:merge(1, 1, bsp) + + +return tm diff --git a/game/modules/tome/data/zones/test/npcs.lua b/game/modules/tome/data/zones/test/npcs.lua index 69f526b28719907139074bd6ffd497b2bad1bfc0..96628dd6cd0ed39164fe60462fc5399783127a9e 100644 --- a/game/modules/tome/data/zones/test/npcs.lua +++ b/game/modules/tome/data/zones/test/npcs.lua @@ -17,5 +17,4 @@ -- Nicolas Casalini "DarkGod" -- darkgod@te4.org ---load("/data/zones/town-angolwen/npcs.lua", rarity(0)) ---load("/data/zones/golem-graveyard/npcs.lua", rarity(0)) +load("/data/general/npcs/all.lua") diff --git a/game/modules/tome/data/zones/test/samples/door.tmx b/game/modules/tome/data/zones/test/samples/door.tmx new file mode 100644 index 0000000000000000000000000000000000000000..510b3fcb6102d78b2bb6fe78efe520f094526762 --- /dev/null +++ b/game/modules/tome/data/zones/test/samples/door.tmx @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8"?> +<map version="1.0" tiledversion="1.0.3" orientation="orthogonal" renderorder="right-down" width="6" height="11" tilewidth="32" tileheight="32" nextobjectid="31"> + <properties> + <property name="status_all" value="{no_teleport=true}"/> + </properties> + <tileset firstgid="1" name="dg_dungeon32" tilewidth="32" tileheight="32" tilecount="90" columns="9"> + <image source="../../../../../../../tiled-maps/gfx/dg_dungeon32.gif" width="288" height="320"/> + <tile id="0"> + <properties> + <property name="id" value="#"/> + </properties> + </tile> + <tile id="3"> + <properties> + <property name="id" value="+"/> + </properties> + </tile> + <tile id="51"> + <properties> + <property name="id" value="."/> + </properties> + </tile> + <tile id="54"> + <properties> + <property name="id" value="_"/> + </properties> + </tile> + <tile id="58"> + <properties> + <property name="id" value="r"/> + </properties> + </tile> + </tileset> + <tileset firstgid="91" name="tome-terrains" tilewidth="32" tileheight="32" tilecount="144" columns="8"> + <image source="../../../../../../../tiled-maps/gfx/tome-terrains.png" width="256" height="576"/> + <tile id="24"> + <properties> + <property name="id" value=";"/> + </properties> + </tile> + <tile id="85"> + <properties> + <property name="id" value="E"/> + </properties> + </tile> + <tile id="102"> + <properties> + <property name="id" value="T"/> + </properties> + </tile> + </tileset> + <tileset firstgid="235" name="numbers" tilewidth="32" tileheight="32" tilecount="36" columns="6"> + <image source="../../../../../../../tiled-maps/gfx/numbers.png" width="192" height="192"/> + </tileset> + <layer name="Terrain" width="6" height="11"> + <data encoding="base64" compression="zlib"> + eJwrZmBgOAjEjGgYmxg+8WIccsVQzALEJkg0TJwRygfRGwiop6U7Ac8WDHE= + </data> + </layer> +</map> diff --git a/game/modules/tome/data/zones/test/samples/wfctest.tmx b/game/modules/tome/data/zones/test/samples/wfctest.tmx new file mode 100644 index 0000000000000000000000000000000000000000..74dd827bb4a776ffd9cd68121612c545b06be781 --- /dev/null +++ b/game/modules/tome/data/zones/test/samples/wfctest.tmx @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> +<map version="1.0" orientation="orthogonal" renderorder="right-down" width="16" height="16" tilewidth="32" tileheight="32" nextobjectid="31"> + <properties> + <property name="status_all" value="{no_teleport=true}"/> + </properties> + <tileset firstgid="1" name="dg_dungeon32" tilewidth="32" tileheight="32" tilecount="90"> + <image source="../../../../../../../tiled-maps/gfx/dg_dungeon32.gif" width="288" height="320"/> + <tile id="0"> + <properties> + <property name="id" value="#"/> + </properties> + </tile> + <tile id="3"> + <properties> + <property name="id" value="+"/> + </properties> + </tile> + <tile id="51"> + <properties> + <property name="id" value="_"/> + </properties> + </tile> + <tile id="54"> + <properties> + <property name="id" value="*"/> + </properties> + </tile> + <tile id="63"> + <properties> + <property name="id" value="O"/> + </properties> + </tile> + </tileset> + <tileset firstgid="91" name="tome-terrains" tilewidth="32" tileheight="32" tilecount="144"> + <image source="../../../../../../../tiled-maps/gfx/tome-terrains.png" width="256" height="576"/> + <tile id="22"> + <properties> + <property name="id" value=";"/> + </properties> + </tile> + </tileset> + <tileset firstgid="235" name="numbers" tilewidth="32" tileheight="32" tilecount="36"> + <image source="../../../../../../../tiled-maps/gfx/numbers.png" width="192" height="192"/> + </tileset> + <layer name="Terrain" width="16" height="16"> + <data encoding="base64" compression="zlib"> + eJxzYGBgcADiQiiNjLGJYRMvRMPYxHCJk6KfWDNJUUsN9xMKL3LCihz/kaN/KMhjC0N8YTrYzKck/mgdvwBZdmTy + </data> + </layer> +</map> diff --git a/game/modules/tome/data/zones/test/samples/wfctest2.tmx b/game/modules/tome/data/zones/test/samples/wfctest2.tmx new file mode 100644 index 0000000000000000000000000000000000000000..0c4f7cf419d4d5739953c31e6da04b5e03e5062c --- /dev/null +++ b/game/modules/tome/data/zones/test/samples/wfctest2.tmx @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<map version="1.0" tiledversion="1.0.3" orientation="orthogonal" renderorder="right-down" width="16" height="16" tilewidth="32" tileheight="32" nextobjectid="31"> + <properties> + <property name="status_all" value="{no_teleport=true}"/> + </properties> + <tileset firstgid="1" name="dg_dungeon32" tilewidth="32" tileheight="32" tilecount="90" columns="9"> + <image source="../../../../../../../tiled-maps/gfx/dg_dungeon32.gif" width="288" height="320"/> + <tile id="0"> + <properties> + <property name="id" value="#"/> + </properties> + </tile> + <tile id="3"> + <properties> + <property name="id" value="+"/> + </properties> + </tile> + <tile id="51"> + <properties> + <property name="id" value="."/> + </properties> + </tile> + <tile id="54"> + <properties> + <property name="id" value="*"/> + </properties> + </tile> + </tileset> + <tileset firstgid="91" name="tome-terrains" tilewidth="32" tileheight="32" tilecount="144" columns="8"> + <image source="../../../../../../../tiled-maps/gfx/tome-terrains.png" width="256" height="576"/> + <tile id="22"> + <properties> + <property name="id" value=";"/> + </properties> + </tile> + <tile id="36"> + <properties> + <property name="id" value="T"/> + </properties> + </tile> + <tile id="52"> + <properties> + <property name="id" value="="/> + </properties> + </tile> + </tileset> + <tileset firstgid="235" name="numbers" tilewidth="32" tileheight="32" tilecount="36" columns="6"> + <image source="../../../../../../../tiled-maps/gfx/numbers.png" width="192" height="192"/> + </tileset> + <layer name="Terrain" width="16" height="16"> + <data encoding="base64" compression="zlib"> + eJxjYKAfKETD5Ojph+AGUvT3Y8FDST+6GdjCD1/YFqLpw6W3HorxmY3LjfUMuPXjA+h21zPgt4sY/eS4gVL91AgDmBmk+B+XOcQCAL8wLm8= + </data> + </layer> +</map> diff --git a/game/modules/tome/data/zones/test/samples/wfctest3.tmx b/game/modules/tome/data/zones/test/samples/wfctest3.tmx new file mode 100644 index 0000000000000000000000000000000000000000..aee128810c7729ba4bfbeb5a702560051a865608 --- /dev/null +++ b/game/modules/tome/data/zones/test/samples/wfctest3.tmx @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<map version="1.0" orientation="orthogonal" renderorder="right-down" width="16" height="16" tilewidth="32" tileheight="32" nextobjectid="31"> + <properties> + <property name="status_all" value="{no_teleport=true}"/> + </properties> + <tileset firstgid="1" name="dg_dungeon32" tilewidth="32" tileheight="32" tilecount="90"> + <image source="../../../../../../../tiled-maps/gfx/dg_dungeon32.gif" width="288" height="320"/> + <tile id="0"> + <properties> + <property name="id" value="#"/> + </properties> + </tile> + <tile id="3"> + <properties> + <property name="id" value="+"/> + </properties> + </tile> + <tile id="27"> + <properties> + <property name="id" value="#"/> + </properties> + </tile> + <tile id="51"> + <properties> + <property name="id" value="."/> + </properties> + </tile> + <tile id="54"> + <properties> + <property name="id" value="*"/> + </properties> + </tile> + </tileset> + <tileset firstgid="91" name="tome-terrains" tilewidth="32" tileheight="32" tilecount="144"> + <image source="../../../../../../../tiled-maps/gfx/tome-terrains.png" width="256" height="576"/> + </tileset> + <tileset firstgid="235" name="numbers" tilewidth="32" tileheight="32" tilecount="36"> + <image source="../../../../../../../tiled-maps/gfx/numbers.png" width="192" height="192"/> + </tileset> + <layer name="Terrain" width="16" height="16"> + <data encoding="base64" compression="zlib"> + eJxjYKAOMMGCSdUrg4SJNQObXlLMQNbLiISRzSDGfpg+FiQ2sfZToh/ZDErigFZgNF0M7nQBAEC8Dvc= + </data> + </layer> +</map> diff --git a/game/modules/tome/data/zones/test/samples/wfctest4.tmx b/game/modules/tome/data/zones/test/samples/wfctest4.tmx new file mode 100644 index 0000000000000000000000000000000000000000..993ceb9155b9c10c3456681b2a8bed79204690c9 --- /dev/null +++ b/game/modules/tome/data/zones/test/samples/wfctest4.tmx @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<map version="1.0" tiledversion="1.0.3" orientation="orthogonal" renderorder="right-down" width="20" height="20" tilewidth="32" tileheight="32" nextobjectid="31"> + <properties> + <property name="status_all" value="{no_teleport=true}"/> + </properties> + <tileset firstgid="1" name="dg_dungeon32" tilewidth="32" tileheight="32" tilecount="90" columns="9"> + <image source="../../../../../../../tiled-maps/gfx/dg_dungeon32.gif" width="288" height="320"/> + <tile id="0"> + <properties> + <property name="id" value="#"/> + </properties> + </tile> + <tile id="3"> + <properties> + <property name="id" value="+"/> + </properties> + </tile> + <tile id="51"> + <properties> + <property name="id" value="."/> + </properties> + </tile> + <tile id="54"> + <properties> + <property name="id" value="_"/> + </properties> + </tile> + <tile id="58"> + <properties> + <property name="id" value="r"/> + </properties> + </tile> + </tileset> + <tileset firstgid="91" name="tome-terrains" tilewidth="32" tileheight="32" tilecount="144" columns="8"> + <image source="../../../../../../../tiled-maps/gfx/tome-terrains.png" width="256" height="576"/> + </tileset> + <tileset firstgid="235" name="numbers" tilewidth="32" tileheight="32" tilecount="36" columns="6"> + <image source="../../../../../../../tiled-maps/gfx/numbers.png" width="192" height="192"/> + </tileset> + <layer name="Terrain" width="20" height="20"> + <data encoding="base64" compression="zlib"> + eJxjZGBgYARiEyiNDxOjBoSt0fBQMg9dDp99Q8U8fGFEDfNgmAWHeSxo6nDpxyZPyH349DLiEackDVHbPBYqmYctrHDFKblqBto85LjGZx56msOmhpi0hm43Kf4lFPekpiNqm0cNNQBZdyK0 + </data> + </layer> +</map> diff --git a/game/modules/tome/data/zones/test/samples/wfctest5.tmx b/game/modules/tome/data/zones/test/samples/wfctest5.tmx new file mode 100644 index 0000000000000000000000000000000000000000..6c13ee129e5773947d6b4f20fe4928f81aeacfa7 --- /dev/null +++ b/game/modules/tome/data/zones/test/samples/wfctest5.tmx @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<map version="1.0" tiledversion="1.0.3" orientation="orthogonal" renderorder="right-down" width="16" height="16" tilewidth="32" tileheight="32" nextobjectid="31"> + <properties> + <property name="status_all" value="{no_teleport=true}"/> + </properties> + <tileset firstgid="1" name="dg_dungeon32" tilewidth="32" tileheight="32" tilecount="90" columns="9"> + <image source="../../../../../../../tiled-maps/gfx/dg_dungeon32.gif" width="288" height="320"/> + <tile id="0"> + <properties> + <property name="id" value="#"/> + </properties> + </tile> + <tile id="3"> + <properties> + <property name="id" value="+"/> + </properties> + </tile> + <tile id="51"> + <properties> + <property name="id" value="."/> + </properties> + </tile> + <tile id="54"> + <properties> + <property name="id" value="_"/> + </properties> + </tile> + <tile id="58"> + <properties> + <property name="id" value="r"/> + </properties> + </tile> + </tileset> + <tileset firstgid="91" name="tome-terrains" tilewidth="32" tileheight="32" tilecount="144" columns="8"> + <image source="../../../../../../../tiled-maps/gfx/tome-terrains.png" width="256" height="576"/> + </tileset> + <tileset firstgid="235" name="numbers" tilewidth="32" tileheight="32" tilecount="36" columns="6"> + <image source="../../../../../../../tiled-maps/gfx/numbers.png" width="192" height="192"/> + </tileset> + <layer name="Terrain" width="16" height="16"> + <data encoding="base64" compression="zlib"> + eJxjZGBgYKQDtkbChOSR1ZlAMSX6KbUfWR06H59+EyzqWajgflx+wGa+CVSMWP3E+m+w6ccWfujqcIU9MfqxxSUx+k2I0Eus+2mBAexcFhw= + </data> + </layer> +</map> diff --git a/game/modules/tome/data/zones/test/zone.lua b/game/modules/tome/data/zones/test/zone.lua index a52ef14710606900e221a557f55bc545c5cfe3b2..344cb52540076839ef580e943460353534069a26 100644 --- a/game/modules/tome/data/zones/test/zone.lua +++ b/game/modules/tome/data/zones/test/zone.lua @@ -24,16 +24,28 @@ return { max_level = 4, decay = {300, 800}, actor_adjust_level = function(zone, level, e) return zone.base_level + e:getRankLevelAdjust() + level.level-1 + rng.range(-1,2) end, - width = 50, height = 50, - all_remembered = true, + width = 80, height = 80, + -- all_remembered = true, all_lited = true, no_level_connectivity = true, + nicer_tiler_overlay = "DungeonWallsGrass", -- events_by_level = true, -- _max_level_generation_count = 2, generator = { map = { +-- [[ + class = "engine.generator.map.MapScript", + ['<'] = "UP", ['>'] = "DOWN", + ['.'] = "FLOOR", ['+'] = "DOOR", ['#'] = "WALL", + ['_'] = "FLOOR", ['O'] = "WALL", + [';'] = "GRASS", ['T'] = "TREE", + ['='] = "DEEP_WATER", + -- mapscript = "!cavernous_forest", + mapscript = "!testroom2", + -- mapscript = "!inner_outer", +--]] --[[ class = "engine.generator.map.Hexacle", up = "FLOOR", @@ -75,7 +87,7 @@ return { down = "JUNGLE_GRASS_DOWN6", door = "JUNGLE_GRASS", --]] --- [[ +--[[ class = "engine.generator.map.Roomer", nb_rooms = 10, edge_entrances = {4,6}, @@ -91,8 +103,9 @@ return { --]] }, actor = { - class = "mod.class.generator.actor.Random", + class = "engine.generator.actor.OnSpots", nb_npc = {0, 0}, + nb_spots = true, on_spot_chance = 90, spot_filters = {{type="spawn-spot"}}, }, --[[ object = { @@ -106,7 +119,7 @@ return { ]] }, post_process = function(level) -- testing level generation failure - if level.level >=3 then level.force_recreate = true end + -- if level.level >=3 then level.force_recreate = true end end, --[[ diff --git a/game/modules/tome/dialogs/ShowMap.lua b/game/modules/tome/dialogs/ShowMap.lua index dd38cd59293dfd8a0913ae1d91d9e590657d4fb7..8927dd28ce4f04087bcc47d1393bfacfe241b2d2 100644 --- a/game/modules/tome/dialogs/ShowMap.lua +++ b/game/modules/tome/dialogs/ShowMap.lua @@ -30,7 +30,7 @@ function _M:init(mm_mode) self.ui = "parchment" - self.bsize = 22 + self.bsize = 10 local map = game.level.map local mw, mh = map.w * self.bsize, map.h * self.bsize local Mw, Mh = math.floor(game.w * 0.9), math.floor(game.h * 0.9) diff --git a/game/thirdparty/algorithms/LICENSE b/game/thirdparty/algorithms/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..7db3a0ef236052d11301bd3fc6ecdb61aa612043 --- /dev/null +++ b/game/thirdparty/algorithms/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Xianshun Chen + +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. diff --git a/game/thirdparty/algorithms/binarysearch.lua b/game/thirdparty/algorithms/binarysearch.lua new file mode 100644 index 0000000000000000000000000000000000000000..3fb6804aaabe46df26b2f33c2e2b998140c90b5b --- /dev/null +++ b/game/thirdparty/algorithms/binarysearch.lua @@ -0,0 +1,31 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 29/6/2017 +-- Time: 3:39 PM +-- To change this template use File | Settings | File Templates. +-- + +binarysearch = {} +binarysearch.__index = binarysearch + +function binarysearch.indexOf(a, value, comparator) + local lo = 0 + local hi = a:size() - 1 + while lo <= hi do + local mid = lo + math.floor((hi - lo) / 2) + local cmp = comparator(value, a:get(mid)) + if cmp < 0 then + hi = mid - 1 + elseif cmp > 0 then + lo = mid + 1 + else + return mid + end + end + return -1 + +end + +return binarysearch + diff --git a/game/thirdparty/algorithms/data/hashmap.lua b/game/thirdparty/algorithms/data/hashmap.lua new file mode 100644 index 0000000000000000000000000000000000000000..ab76de606152cae8e8ae285d77f16a940ad618ba --- /dev/null +++ b/game/thirdparty/algorithms/data/hashmap.lua @@ -0,0 +1,119 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 26/6/2017 +-- Time: 12:48 AM +-- To change this template use File | Settings | File Templates. +-- + +local hashmap = {} + +hashmap.__index = hashmap + +hashmap.Node = {} +hashmap.Node.__index = hashmap.Node + +function hashmap.Node.create(key, value) + local s = {} + setmetatable(s, hashmap.Node) + + s.key = key + s.value = value + s.next = nil + + return s +end + +function hashmap.create(hash) + local s = {} + setmetatable(s, hashmap) + + if hash == nil then + hash = function(x) return x end + end + + s.s = {} + s.N = 0 + s.hash = hash + + return s +end + +function hashmap:put(key, value) + local h = self.hash(key) + local x = self.s[h] + local found = false + while x ~= nil do + if x.key == key then + found = true + x.value = value + break + end + x = x.next + end + + if found == false then + local old = self.s[h] + self.s[h] = hashmap.Node.create(key,value) + self.s[h].next = old + self.N = self.N + 1 + end + +end + +function hashmap:get(key) + local h = self.hash(key) + local x = self.s[h] + while x ~= nil do + if x.key == key then + return x.value + end + x = x.next + end + return nil +end + +function hashmap:containsKey(key) + local h = self.hash(key) + local x = self.s[h] + while x ~= nil do + if x.key == key then + return true + end + x = x.next + end + return false +end + +function hashmap:size() + return self.N +end + +function hashmap:isEmpty() + return self.N == 0 +end + +function hashmap:remove(key) + local h = self.hash(key) + local x = self.s[h] + local prev_x = nil + while x ~= nil do + if x.key == key then + local value = x.value + if prev_x == nil then + self.s[h] = x.next + else + prev_x.next = x.next + end + self.N = self.N - 1 + return value + end + prev_x = x + x = x.next + end + + return nil +end + +return hashmap + diff --git a/game/thirdparty/algorithms/data/hashset.lua b/game/thirdparty/algorithms/data/hashset.lua new file mode 100644 index 0000000000000000000000000000000000000000..157c7549a527b39675e5ccd96312fa6a2e07b678 --- /dev/null +++ b/game/thirdparty/algorithms/data/hashset.lua @@ -0,0 +1,103 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 28/6/2017 +-- Time: 1:17 PM +-- To change this template use File | Settings | File Templates. +-- + +local hashset = {} + +hashset.__index = hashset + +hashset.Node = {} +hashset.Node.__index = hashset.Node + +function hashset.Node.create(key) + local s = {} + setmetatable(s, hashset.Node) + + s.key = key + s.next = nil + + return s +end + +function hashset.create(hash) + local s = {} + setmetatable(s, hashset) + + if hash == nil then + hash = function(x) return x end + end + + s.s = {} + s.N = 0 + s.hash = hash + + return s +end + +function hashset:add(key) + local h = self.hash(key) + local x = self.s[h] + local found = false + while x ~= nil do + if x.key == key then + found = true + break + end + x = x.next + end + + if found == false then + local old = self.s[h] + self.s[h] = hashset.Node.create(key) + self.s[h].next = old + self.N = self.N + 1 + end + +end + +function hashset:contains(key) + local h = self.hash(key) + local x = self.s[h] + while x ~= nil do + if x.key == key then + return true + end + x = x.next + end + return false +end + +function hashset:size() + return self.N +end + +function hashset:isEmpty() + return self.N == 0 +end + +function hashset:remove(key) + local h = self.hash(key) + local x = self.s[h] + local prev_x = nil + while x ~= nil do + if x.key == key then + if prev_x == nil then + self.s[h] = x.next + else + prev_x.next = x.next + end + self.N = self.N - 1 + end + prev_x = x + x = x.next + end + + return nil +end + +return hashset + diff --git a/game/thirdparty/algorithms/data/list.lua b/game/thirdparty/algorithms/data/list.lua new file mode 100644 index 0000000000000000000000000000000000000000..37ccab00d2979f38248e4274074fa5f027ae5baa --- /dev/null +++ b/game/thirdparty/algorithms/data/list.lua @@ -0,0 +1,145 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 25/6/2017 +-- Time: 10:13 AM +-- To change this template use File | Settings | File Templates. +-- + +local list = {} + +list.ArrayList = {} +list.ArrayList.__index = list.ArrayList + +function list.ArrayList.create() + local s = {} + setmetatable(s, list.ArrayList) + + s.a = { nil } + s.aLen = 1 + s.N = 0 + return s +end + +function list.ArrayList.createWith(a, aLen, N) + local s = {} + setmetatable(s, list.ArrayList) + + s.a = a + s.aLen = aLen + s.N = N + return s +end + +function list.create() + return list.ArrayList.create() +end + +function list.createWith(a, aLen, N) + return list.ArrayList.createWith(a, aLen, N) +end + +function list.ArrayList:makeCopy() + local temp = {} + for key,val in pairs(self.a) do + temp[key] = val + end + return list.ArrayList.createWith(temp, self.aLen, self.N) +end + +function list.ArrayList:add(value) + self.a[self.N] = value + self.N = self.N + 1 + if self.N == self.aLen then + self:resize(self.aLen * 2) + end +end + +function list.ArrayList:set(index,value) + self.a[index] = value +end + +function list.ArrayList:get(index) + local temp = self.a[index] + return temp +end + +function list.ArrayList:removeAt(index) + if index == self.N-1 then + self.N = self.N - 1 + return + end + for i = index+1,self.N-1 do + self.a[i-1]=self.a[i] + end + self.N = self.N - 1 + if self.N == math.floor(self.aLen / 4) then + self:resize(math.floor(self.aLen / 2)) + end + +end + +function list.ArrayList:indexOf(value) + if self.N == 0 then + return -1 + end + for i=0,self.N-1 do + if self.a[i] == value then + return i + end + end + return -1 +end + +function list.ArrayList:remove(value) + local index = self:indexOf(value) + self:removeAt(index) +end + +function list.ArrayList:resize(newSize) + local temp = {} + for i = 0,(newSize-1) do + temp[i] = self.a[i] + end + + self.a = temp + self.aLen = newSize +end + +function list.ArrayList:size() + return self.N +end + +function list.ArrayList:isEmpty() + return self.N == 0 +end + +function list.ArrayList:enumerate() + local temp = {} + for i = 0,(self.N-1) do + temp[i] = self.a[i] + end + return temp +end + +function list.ArrayList:isSortedAscendingly(comparator) + for i=0,(self:size()-2) do + if comparator(a:get(i), a:get(i+1)) > 0 then + return false + end + + end + return true +end + +function list.ArrayList:isSortedDescendingly(comparator) + for i=0,(self:size()-2) do + if comparator(a:get(i), a:get(i+1)) < 0 then + return false + end + + end + return true +end + +return list diff --git a/game/thirdparty/algorithms/data/maxpq.lua b/game/thirdparty/algorithms/data/maxpq.lua new file mode 100644 index 0000000000000000000000000000000000000000..16dc3ad8f410084dfcd7d15d1e5af07edf008b17 --- /dev/null +++ b/game/thirdparty/algorithms/data/maxpq.lua @@ -0,0 +1,109 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 25/6/2017 +-- Time: 11:33 PM +-- To change this template use File | Settings | File Templates. +-- + +local maxpq = {} + +maxpq.__index = maxpq + +function maxpq.create(comparer) + local s = {} + setmetatable(s, maxpq) + + s.s = { nil } + s.aLen = 1 + s.N = 0 + s.comparer = comparer + + return s +end + +function maxpq:enqueue(value) + if self.N + 1 == self.aLen then + self:resize(self.aLen * 2) + end + self.N = self.N + 1 + self.s[self.N] = value + self:swim(self.N) +end + +function maxpq:delMax() + if self.N == 0 then + return nil + end + + local value = self.s[1] + self:exchange(self.s, 1, self.N) + self.N = self.N - 1 + self:sink(1) + if self.N + 1 == math.floor(self.aLen / 4) then + self:resize(math.floor(self.aLen / 2)) + end + + return value + +end + +function maxpq:resize(newSize) + local temp = {} + for i = 0,(newSize-1) do + temp[i] = self.s[i] + end + + self.s = temp + self.aLen = newSize +end + +function maxpq:swim(k) + while k > 1 do + + local parent = math.floor(k / 2) + if self:greater(self.s[k], self.s[parent]) then + self:exchange(self.s, k, parent) + k = parent + else + break + end + end +end + +function maxpq:sink(k) + while k * 2 <= self.N do + local child = k * 2 + if child < self.N and self:greater(self.s[child+1], self.s[child]) then + child = child + 1 + end + if self:greater(self.s[child], self.s[k]) then + self:exchange(self.s, child, k) + k = child + else + break + end + end + +end + +function maxpq:size() + return self.N +end + +function maxpq:isEmpty() + return self.N == 0 +end + +function maxpq:greater(a1, a2) + return self.comparer(a1, a2) > 0 +end + +function maxpq:exchange(s, i, j) + local temp = s[i] + s[i] = s[j] + s[j] = temp +end + +return maxpq + diff --git a/game/thirdparty/algorithms/data/minpq.lua b/game/thirdparty/algorithms/data/minpq.lua new file mode 100644 index 0000000000000000000000000000000000000000..b29d4714665da3049d9eaf2852d5e217cafa4854 --- /dev/null +++ b/game/thirdparty/algorithms/data/minpq.lua @@ -0,0 +1,109 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 25/6/2017 +-- Time: 11:33 PM +-- To change this template use File | Settings | File Templates. +-- + +local minpq = {} + +minpq.__index = minpq + +function minpq.create(comparer) + local s = {} + setmetatable(s, minpq) + + s.s = { nil } + s.aLen = 1 + s.N = 0 + s.comparer = comparer + + return s +end + +function minpq:enqueue(value) + if self.N + 1 == self.aLen then + self:resize(self.aLen * 2) + end + self.N = self.N + 1 + self.s[self.N] = value + self:swim(self.N) +end + +function minpq:delMin() + if self.N == 0 then + return nil + end + + local value = self.s[1] + self:exchange(self.s, 1, self.N) + self.N = self.N - 1 + self:sink(1) + if self.N + 1 == math.floor(self.aLen / 4) then + self:resize(math.floor(self.aLen / 2)) + end + + return value + +end + +function minpq:resize(newSize) + local temp = {} + for i = 0,(newSize-1) do + temp[i] = self.s[i] + end + + self.s = temp + self.aLen = newSize +end + +function minpq:swim(k) + while k > 1 do + + local parent = math.floor(k / 2) + if self:less(self.s[k], self.s[parent]) then + self:exchange(self.s, k, parent) + k = parent + else + break + end + end +end + +function minpq:sink(k) + while k * 2 <= self.N do + local child = k * 2 + if child < self.N and self:less(self.s[child+1], self.s[child]) then + child = child + 1 + end + if self:less(self.s[child], self.s[k]) then + self:exchange(self.s, child, k) + k = child + else + break + end + end + +end + +function minpq:size() + return self.N +end + +function minpq:isEmpty() + return self.N == 0 +end + +function minpq:less(a1, a2) + return self.comparer(a1, a2) < 0 +end + +function minpq:exchange(s, i, j) + local temp = s[i] + s[i] = s[j] + s[j] = temp +end + +return minpq + diff --git a/game/thirdparty/algorithms/data/queue.lua b/game/thirdparty/algorithms/data/queue.lua new file mode 100644 index 0000000000000000000000000000000000000000..8a4a3d2ecd9c0bfd1bd8c647f58cb73decba97e6 --- /dev/null +++ b/game/thirdparty/algorithms/data/queue.lua @@ -0,0 +1,161 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 25/6/2017 +-- Time: 8:10 PM +-- To change this template use File | Settings | File Templates. +-- + +local queue = {} + +queue.LinkedListQueue = {} +queue.LinkedListQueue.__index = queue.LinkedListQueue + +queue.Node = {} +queue.Node.__index = queue.Node + +function queue.Node.create(value) + local s = {} + setmetatable(s, queue.Node) + + s.value = value + s.next = nil + + return s +end + +function queue.LinkedListQueue.create() + local s = {} + setmetatable(s, queue.LinkedListQueue) + + s.first = nil + s.last = nil + s.N = 0 + + return s +end + +function queue.create() + return queue.LinkedListQueue.create() +end + +function queue.LinkedListQueue:enqueue(value) + local oldLast = self.last + self.last = queue.Node.create(value) + if oldLast ~= nil then + oldLast.next = self.last + end + if self.first == nil then + self.first = self.last + end + self.N = self.N + 1 +end + +function queue.LinkedListQueue:dequeue() + local oldFirst = self.first + if oldFirst == nil then + return nil + end + self.first = oldFirst.next + local value = oldFirst.value + if self.first == nil then + self.last = nil + end + self.N = self.N - 1 + return value +end + +function queue.LinkedListQueue:size() + return self.N +end + +function queue.LinkedListQueue:isEmpty() + return self.N == 0 +end + +function queue.LinkedListQueue:enumerate() + local index = 0 + local temp = {} + local x = self.first + while x ~= nil do + local value = x.value + temp[index] = value + index = index + 1 + x = x.next + end + + return temp + +end + +queue.ArrayQueue = {} +queue.ArrayQueue.__index = queue.ArrayQueue + +function queue.ArrayQueue.create() + local s = {} + setmetatable(s, queue.ArrayQueue) + + s.head = 0 + s.tail = 0 + s.a = { nil } + s.aLen = 0 + + return s +end + +function queue.ArrayQueue:enqueue(value) + self.a[self.tail] = value + self.tail = self.tail + 1 + if self.tail - self.head == self.aLen then + self:resize(self.aLen * 2) + end + +end + +function queue.ArrayQueue:size() + return self.tail - self.head +end + +function queue.ArrayQueue:isEmpty() + return self.tail == self.head +end + +function queue.ArrayQueue:dequeue(value) + if self.tail == self.head then + return nil + end + + value = self.a[self.head] + self.head = self.head + 1 + if self.tail - self.head == math.floor(self.aLen / 4) then + self:resize(math.floor(self.aLen / 2)) + end + + return value +end + +function queue.ArrayQueue:resize(newSize) + local temp = {} + for i = 0,(newSize-1) do + if i < self.tail - self.head then + temp[i] = nil + else + temp[i] = self.a[i + self.head] + end + end + + self.a = temp + self.aLen = newSize + self.tail = self.tail - self.head + self.head = 0 +end + +function queue.ArrayQueue:enumerate() + local temp = {} + for i = self.head,self.tail-1 do + temp[i - self.head] = self.a[i] + end + return temp +end + +return queue diff --git a/game/thirdparty/algorithms/data/redblacktree.lua b/game/thirdparty/algorithms/data/redblacktree.lua new file mode 100644 index 0000000000000000000000000000000000000000..ab96e4ceb231bebff740dcc65dacf0dcf322ae57 --- /dev/null +++ b/game/thirdparty/algorithms/data/redblacktree.lua @@ -0,0 +1,250 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 25/6/2017 +-- Time: 11:32 PM +-- To change this template use File | Settings | File Templates. +-- + +local redblacktree = {} +redblacktree.__index = redblacktree + +redblacktree.Node = {} +redblacktree.Node.__index = redblacktree.Node + +function redblacktree.Node.create(key, value) + local s = {} + setmetatable(s, redblacktree.Node) + + s.key = key + s.value = value + s.left = nil + s.right = nil + s.count = 0 + s.red = true + + return s +end + +function redblacktree.create(comparator) + local s = {} + setmetatable(s, redblacktree) + + if comparator == nil then + comparator = function(a1, a2) return a1 - a2 end + end + + s.root = nil + s.N = 0 + s.comparator = comparator + + return s +end + +function redblacktree:put(key, value) + self.root = self:_put(self.root, key, value) +end + +function redblacktree:_put(x, key, value) + if x == nil then + x = redblacktree.Node.create(key, value) + end + + local comp = self.comparator(key, x.key) + if comp < 0 then + x.left = self:_put(x.left, key, value) + elseif comp > 0 then + x.right = self:_put(x.right, key, value) + else + x.value = value + end + + if self:isRed(x.left) == false and self:isRed(x.right) then + x = self:rotateLeft(x) + elseif self:isRed(x.left) and self:isRed(x.left.left) then + x= self:rotateRight(x) + elseif self:isRed(x.left) and self:isRed(x.right) then + self:flipColor(x) + end + + x.count = self:count(x.left) + self:count(x.right) + 1 + return x +end + +function redblacktree:isRed(x) + if x == nil then + return false + end + return x.red +end + +function redblacktree:flipColor(x) + x.left.red = false + x.right.red = false + x.red = true +end + +function redblacktree:rotateLeft(x) + local h = x.right + x.right = h.left + h.left = x + x.red = h.red + h.red = false + + x = h + x.count = self:count(x.left) + self:count(x.right) + 1 + + return x +end + +function redblacktree:rotateRight(x) + local h = x.left + x.left = h.right + h.right = x + x.red = h.red + h.red = false + + x = h + x.count = self:count(x.left) + self:count(x.right) + 1 + + return x +end + +function redblacktree:count(x) + if x == nil then + return 0 + end + return x.count +end + +function redblacktree:get(key) + local x = self:_get(self.root, key) + if x == nil then + return nil + end + + return x.value +end + +function redblacktree:containsKey(key) + local x = self:_get(self.root, key) + return x ~= nil +end + +function redblacktree:_get(x, key, value) + if x == nil then + return nil + end + + local comp = self.comparator(key, x.key) + if comp < 0 then + return self:_get(x.left, key) + elseif comp > 0 then + return self:_get(x.right, key) + else + return x + end +end + +function redblacktree:size() + return self:count(self.root) +end + +function redblacktree:isEmpty() + return self:size() == 0 +end + +function redblacktree:remove(key) + self.root = self:_remove(self.root, key) +end + +function redblacktree:_min(x) + if x.left == nil then + return x + end + + return self:_min(x.left) +end + +function redblacktree:_max(x) + if x.right == nil then + return x + end + + return self:_max(x.right) +end + +function redblacktree:_delMin(x) + if x.left == nil then + return x.right + end + x.left = self:_delMin(x.left) + return x +end + +function redblacktree:_remove(x, key) + if x == nil then + return nil + end + + local comp = self.comparator(key, x.key) + if comp < 0 then + x.left = self:_remove(x.left, key) + elseif comp > 0 then + x.right = self:_remove(x.right, key) + else + if x.left == nil then + x = x.right + elseif x.right == nil then + x = x.left + else + local m = self:_min(x.right) + m.left = x.left + m.right = self:_delMin(x.right) + x = m + end + end + + if x ~= nil then + if self:isRed(x.left) == false and self:isRed(x.right) then + x = self:rotateLeft(x) + elseif self:isRed(x.left) and self:isRed(x.left.left) then + x= self:rotateRight(x) + elseif self:isRed(x.left) and self:isRed(x.right) then + self:flipColor(x) + end + + x.count = self:count(x.left) + self:count(x.right) + 1 + end + + return x +end + +function redblacktree:minKey() + if self.root == nil then + return nil + end + + local x = self:_min(self.root) + if x ~= nil then + return x.key + end + return nil +end + +function redblacktree:maxKey() + if self.root == nil then + return nil + end + + local x= self:_max(self.root) + + if x ~= nil then + return x.key + end + return nil +end + +return redblacktree + + diff --git a/game/thirdparty/algorithms/data/stack.lua b/game/thirdparty/algorithms/data/stack.lua new file mode 100644 index 0000000000000000000000000000000000000000..f35c38572fe81a862269d1182b0ecb14408a23ae --- /dev/null +++ b/game/thirdparty/algorithms/data/stack.lua @@ -0,0 +1,143 @@ +--- +--- Created by chen0. +--- DateTime: 24/6/2017 1:50 PM +--- + +local stack = {} + +stack.Node = {} +stack.Node.__index = stack.Node + +function stack.Node.create(value) + local s = {} + setmetatable(s, stack.Node) + + s.value = value + s.next = nil + + return s +end + +stack.LinkedListStack = {} +stack.LinkedListStack.__index = stack.LinkedListStack + +function stack.LinkedListStack.create() + local s = {} + setmetatable(s, stack.LinkedListStack) + + s.first = nil + s.N = 0 + + return s +end + +function stack.create() + return stack.LinkedListStack.create() +end + +function stack.LinkedListStack:push(value) + local oldFirst = self.first + self.first = stack.Node.create(value) + self.first.next = oldFirst + self.N = self.N + 1 +end + +function stack.LinkedListStack:pop() + local oldFirst = self.first + if oldFirst == nil then + return nil + end + local value = oldFirst.value + self.first = oldFirst.next + self.N = self.N - 1 + return value +end + +function stack.LinkedListStack:size() + return self.N +end + +function stack.LinkedListStack:isEmpty() + return self.N == 0 +end + +function stack.LinkedListStack:enumerate() + local x = self.first + local index = 0 + local temp = {} + while x ~= nil do + temp[index] = x.value + index = index + 1 + x = x.next + end + + return temp + +end + +stack.ArrayStack = {} +stack.ArrayStack.__index = stack.ArrayStack + +function stack.ArrayStack.create() + local s = {} + setmetatable(s, stack.ArrayStack) + + s.a = { nil } + s.N = 0 + s.aLen = 1 + + return s +end + +function stack.ArrayStack:push(value) + self.a[self.N] = value + self.N = self.N + 1 + if self.N == self.aLen then + self:resize(self.aLen * 2) + end +end + +function stack.ArrayStack:resize(newSize) + local temp = {} + for i = 0,(newSize-1) do + temp[i] = self.a[i] + end + + self.a = temp + self.aLen = newSize +end + +function stack.ArrayStack:pop() + if self.N == 0 then + return nil + end + + local value = self.a[self.N-1] + self.N = self.N - 1 + + if self.N == math.floor(self.aLen / 4) then + self:resize(math.floor(self.aLen / 2)) + end + return value +end + +function stack.ArrayStack:size() + return self.N +end + +function stack.ArrayStack:isEmpty() + return self.N == 0 +end + +function stack.ArrayStack:enumerate() + local temp = {} + for i = 0,(self.N-1) do + temp[i] = self.a[i] + end + return temp +end + +return stack + + + diff --git a/game/thirdparty/algorithms/shuffling.lua b/game/thirdparty/algorithms/shuffling.lua new file mode 100644 index 0000000000000000000000000000000000000000..93baf6c5a8d7a2d65b094f15dcae6bea4ccf1ef4 --- /dev/null +++ b/game/thirdparty/algorithms/shuffling.lua @@ -0,0 +1,27 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 29/6/2017 +-- Time: 11:47 AM +-- To change this template use File | Settings | File Templates. +-- + +local shuffling = {} + +function shuffling.shuffle(a) + local N = a:size() + for i=1,(N-1) do + local j = math.random(i+1) - 1 + shuffling.exchange(a, i, j) + end + +end + +function shuffling.exchange(a, i, j) + local temp = a:get(i) + a:set(i, a:get(j)) + a:set(j, temp) +end + +return shuffling + diff --git a/game/thirdparty/algorithms/sorting/heapsort.lua b/game/thirdparty/algorithms/sorting/heapsort.lua new file mode 100644 index 0000000000000000000000000000000000000000..6b2a7f324bf458c70a4de7c539e49a58f8b0ce18 --- /dev/null +++ b/game/thirdparty/algorithms/sorting/heapsort.lua @@ -0,0 +1,54 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 29/6/2017 +-- Time: 10:30 AM +-- To change this template use File | Settings | File Templates. +-- + +local heapsort = {} + +heapsort.__index = heapsort + +function heapsort.sort(a, comparator) + local N = a:size() + for i=(math.floor(N/2)),1,-1 do + heapsort._sink(a, i, N, comparator) + end + + while N > 1 do + heapsort._exchange(a, heapsort.index(1), heapsort.index(N)) + N = N - 1 + heapsort._sink(a, 1, N, comparator) + end +end + +function heapsort.index(i) + return i - 1 +end + +function heapsort._exchange(a, i, j) + local temp = a:get(i) + a:set(i, a:get(j)) + a:set(j, temp) +end + +function heapsort._sink(a, k, N, comparator) + while 2 * k <= N do + local child = k * 2 + if child < N and comparator(a:get(heapsort.index(child+1)), a:get(heapsort.index(child))) > 0 then + child = child + 1 + end + if comparator(a:get(heapsort.index(child)), a:get(heapsort.index(k))) > 0 then + heapsort._exchange(a, heapsort.index(child), heapsort.index(k)) + k = child + else + break + end + end + +end + + +return heapsort + diff --git a/game/thirdparty/algorithms/sorting/insertion.lua b/game/thirdparty/algorithms/sorting/insertion.lua new file mode 100644 index 0000000000000000000000000000000000000000..ee27b01eed05c5a0ddd7c0edc1e583b422922f1f --- /dev/null +++ b/game/thirdparty/algorithms/sorting/insertion.lua @@ -0,0 +1,31 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 28/6/2017 +-- Time: 10:44 AM +-- To change this template use File | Settings | File Templates. +-- + +insertion = {} +insertion.__index = insertion + +function insertion.sort(a, comparator) + local N = a:size() + for i=1,(N-1) do + for j=(i-1),0,-1 do + if comparator(a:get(j+1), a:get(j)) < 0 then + insertion.exchange(a, j, j+1) + else + break + end + end + end +end + +function insertion.exchange(a, i, j) + local temp = a:get(i) + a:set(i, a:get(j)) + a:set(j, temp) +end + +return insertion diff --git a/game/thirdparty/algorithms/sorting/mergesort.lua b/game/thirdparty/algorithms/sorting/mergesort.lua new file mode 100644 index 0000000000000000000000000000000000000000..90e66ce74595f4107a09730e3ceed413974a3ffb --- /dev/null +++ b/game/thirdparty/algorithms/sorting/mergesort.lua @@ -0,0 +1,61 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 29/6/2017 +-- Time: 4:20 AM +-- To change this template use File | Settings | File Templates. +-- + +local mergesort = {} +mergesort.__index = mergesort + +function mergesort.sort(a, comparator) + local aux = require('lualgorithms.data.list').create() + for i=0,(a:size()-1) do + aux:add(0) + end + mergesort._sort(a, aux, 0, a:size()-1, comparator) +end + +function mergesort._sort(a, aux, lo, hi, comparator) + if lo >= hi then + return + end + + local mid = lo + math.floor((hi - lo) / 2) + mergesort._sort(a, aux, lo, mid, comparator) + mergesort._sort(a, aux, mid+1, hi, comparator) + mergesort._merge(a, aux, lo, mid, hi, comparator) +end + +function mergesort._merge(a, aux, lo, mid, hi, comparator) + local i = lo + local j = mid+1 + for k=lo,hi do + aux:set(k, a:get(k)) + end + + for k=lo,hi do + if i > mid then + a:set(k, aux:get(j)) + j = j+1 + elseif j > hi then + a:set(k, aux:get(i)) + i = i + 1 + else + local cmp = comparator(aux:get(i), aux:get(j)) + if cmp <= 0 then + a:set(k, aux:get(i)) + i = i + 1 + else + a:set(k, aux:get(j)) + j = j + 1 + end + end + end + + +end + +return mergesort + diff --git a/game/thirdparty/algorithms/sorting/quicksort.lua b/game/thirdparty/algorithms/sorting/quicksort.lua new file mode 100644 index 0000000000000000000000000000000000000000..4616bdfd25ab7384fb3bdcc872645f947a94e229 --- /dev/null +++ b/game/thirdparty/algorithms/sorting/quicksort.lua @@ -0,0 +1,62 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 29/6/2017 +-- Time: 10:04 AM +-- To change this template use File | Settings | File Templates. +-- + +local quicksort = {} +quicksort.__index = quicksort + +function quicksort.sort(a, comparator) + quicksort._sort(a, 0, a:size()-1, comparator) +end + +function quicksort._sort(a, lo, hi, comparator) + if lo >= hi then + return + end + + local j = quicksort._partition(a, lo, hi, comparator) + quicksort._sort(a, lo, j-1, comparator) + quicksort._sort(a, j+1, hi, comparator) +end + +function quicksort._partition(a, lo, hi, comparator) + local i = lo + local j = hi + 1 + local v = a:get(lo) + while true do + i = i + 1 + while comparator(a:get(i), v) < 0 do + i = i+1 + if i >= hi then + break + end + end + j = j - 1 + while comparator(v, a:get(j)) < 0 do + j = j - 1 + if j <= lo then + break + end + end + + if i >= j then + break + end + + quicksort.exchange(a, i, j) + end + quicksort.exchange(a, lo, j) + return j +end + +function quicksort.exchange(a, i, j) + local temp = a:get(i) + a:set(i, a:get(j)) + a:set(j, temp) +end + +return quicksort diff --git a/game/thirdparty/algorithms/sorting/quicksort3ways.lua b/game/thirdparty/algorithms/sorting/quicksort3ways.lua new file mode 100644 index 0000000000000000000000000000000000000000..ca76fbb8efd69d56e9e3efd5ec159999605b277c --- /dev/null +++ b/game/thirdparty/algorithms/sorting/quicksort3ways.lua @@ -0,0 +1,51 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 29/6/2017 +-- Time: 10:14 AM +-- To change this template use File | Settings | File Templates. +-- + +local quicksort3ways = {} + +quicksort3ways.__index = quicksort3ways + +function quicksort3ways.sort(a, comparator) + quicksort3ways._sort(a, 0, a:size()-1, comparator) +end + +function quicksort3ways._sort(a, lo, hi, comparator) + if lo >= hi then + return + end + + local i = lo + local lt = lo + local gt = hi + local v = a:get(lo) + while i <= gt do + local cmp = comparator(a:get(i), v) + if cmp < 0 then + quicksort3ways.exchange(a, i, lt) + i = i + 1 + lt = lt + 1 + elseif cmp > 0 then + quicksort3ways.exchange(a, i, gt) + gt = gt - 1 + else + i = i + 1 + end + end + + quicksort3ways._sort(a, lo, lt-1, comparator) + quicksort3ways._sort(a, gt+1, hi, comparator) +end + +function quicksort3ways.exchange(a, i, j) + local temp = a:get(i) + a:set(i, a:get(j)) + a:set(j, temp) +end + +return quicksort3ways + diff --git a/game/thirdparty/algorithms/sorting/selection.lua b/game/thirdparty/algorithms/sorting/selection.lua new file mode 100644 index 0000000000000000000000000000000000000000..493fa364577aab2235c49f49f5a8ec4d0e8eb91e --- /dev/null +++ b/game/thirdparty/algorithms/sorting/selection.lua @@ -0,0 +1,35 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 27/6/2017 +-- Time: 9:59 PM +-- To change this template use File | Settings | File Templates. +-- + +local selection = {} +selection.__index = selection + +function selection.sort(a, comparer) + local N = a:size() + + for i=0,(N-1) do + local J = i + for j=i,(N-1) do + if comparer(a:get(j), a:get(J)) < 0 then + J = j + end + end + + selection.exchange(a, i, J) + end +end + +function selection.exchange(a, i, j) + local temp = a:get(i) + a:set(i, a:get(j)) + a:set(j, temp) +end + +return selection + + diff --git a/game/thirdparty/algorithms/sorting/shellsort.lua b/game/thirdparty/algorithms/sorting/shellsort.lua new file mode 100644 index 0000000000000000000000000000000000000000..37cd8db7e6589acd8e1051d8282792c1950fd948 --- /dev/null +++ b/game/thirdparty/algorithms/sorting/shellsort.lua @@ -0,0 +1,40 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 28/6/2017 +-- Time: 1:07 PM +-- To change this template use File | Settings | File Templates. +-- + +local shellsort = {} +shellsort.__index = shellsort + +function shellsort.sort(a, comparator) + local N = a:size() + local h = 0 + while h < math.floor(N / 3) do + h = h * 3 + 1 + end + + for step=h,1,-1 do + for i=step,(N-1) do + for j=i,step,-step do + if comparator(a:get(j), a:get(j-step)) < 0 then + shellsort.exchange(a, j, j-step) + else + break + end + end + end + end + +end + +function shellsort.exchange(a, i, j) + local temp = a:get(i) + a:set(i, a:get(j)) + a:set(j, temp) +end + +return shellsort + diff --git a/game/thirdparty/algorithms/tries/rwaytries.lua b/game/thirdparty/algorithms/tries/rwaytries.lua new file mode 100644 index 0000000000000000000000000000000000000000..c4ef303e36d6c0189246525ffcc0091dd0288313 --- /dev/null +++ b/game/thirdparty/algorithms/tries/rwaytries.lua @@ -0,0 +1,152 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 29/6/2017 +-- Time: 5:10 PM +-- To change this template use File | Settings | File Templates. +-- + +local rwaytries = {} + +rwaytries.__index = rwaytries + +rwaytries.Node = {} + +rwaytries.Node.__index = rwaytries.Node + +function rwaytries.charAt(s, i) + if string.len(s)-1 < i then + return -1 + end + + return string.byte(s, i+1) +end + +function rwaytries.Node.create(key) + local s = {} + setmetatable(s, rwaytries.Node) + + s.nodes = {} + s.value = nil + return s +end + +function rwaytries.create() + local s = {} + setmetatable(s, rwaytries) + + s.root = nil + s.N = 0 + return s +end + +function rwaytries:put(key, value) + self.root = self:_put(self.root, key, value, 0) +end + +function rwaytries:_put(x, key, value, d) + + + if x == nil then + x = rwaytries.Node.create() + end + + if d == key:len() then + if x.value == nil then + self.N = self.N + 1 + end + x.value = value + return x + end + + local c = rwaytries.charAt(key, d) + x.nodes[c] = self:_put(x.nodes[c], key, value, d + 1) + + return x +end + +function rwaytries:get(key) + local x = self:_get(self.root, key, 0) + if x ~= nil then + return x.value + end + return nil +end + +function rwaytries:_get(x, key, d) + if x == nil then + return nil + end + + if d == key:len() then + return x + end + + local c = rwaytries.charAt(key, d) + return self:_get(x.nodes[c],key, d+1) +end + +function rwaytries:remove(key) + local x = self:_get(self.root, key, 0) + if x ~= nil then + if x.value ~= nil then + self.N = self.N - 1 + end + x.value = nil + end +end + + + +function rwaytries:containsKey(key) + local x = self:_get(self.root, key, 0) + if x == nil then + return false + end + + return x.value ~= nil +end + +function rwaytries:size() + return self.N +end + +function rwaytries:isEmpty() + return self.N == 0 +end + +function rwaytries:keys() + local queue = require('lualgorithms.data.list').create() + + self:collect(self.root, '', queue) + return queue +end + +function rwaytries:keysWithPrefix(prefix) + local x = self:_get(self.root, prefix, 0) + + local queue = require('lualgorithms.data.list').create() + + self:collect(x, prefix, queue) + return queue +end + +function rwaytries:collect(x, prefix, queue) + if x == nil then + return + end + + if x.value ~=nil then + queue:add(prefix) + end + + for key,value in pairs(x.nodes) do + self:collect(value, prefix .. string.char(key), queue) + end + +end + +return rwaytries + + + diff --git a/game/thirdparty/algorithms/unionfind.lua b/game/thirdparty/algorithms/unionfind.lua new file mode 100644 index 0000000000000000000000000000000000000000..c2488e1016516fa61a8f83ec49771b64997fd969 --- /dev/null +++ b/game/thirdparty/algorithms/unionfind.lua @@ -0,0 +1,49 @@ +-- +-- Created by IntelliJ IDEA. +-- User: chen0 +-- Date: 29/6/2017 +-- Time: 4:49 PM +-- To change this template use File | Settings | File Templates. +-- + +local unionfind = {} + +unionfind.__index = unionfind + +function unionfind.create() + local s = {} + setmetatable(s, unionfind) + + s.id = {} + return s +end + +function unionfind:union(v, w) + local pv = self:root(v) + local pw = self:root(w) + if pv ~= pw then + self.id[pv] = pw + end + +end + +function unionfind:root(v) + if self.id[v] == nil then + self.id[v] = v + end + local x = v + while x ~= self.id[x] do + self.id[x] = self.id[self.id[x]] + x = self.id[x] + end + + return x + +end + +function unionfind:connected(v, w) + return self:root(v) == self:root(w) +end + +return unionfind + diff --git a/game/thirdparty/licenses/lua-algorithms.txt b/game/thirdparty/licenses/lua-algorithms.txt new file mode 100644 index 0000000000000000000000000000000000000000..7db3a0ef236052d11301bd3fc6ecdb61aa612043 --- /dev/null +++ b/game/thirdparty/licenses/lua-algorithms.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Xianshun Chen + +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. diff --git a/premake4.lua b/premake4.lua index dc05ecba97e64ddf4750557013789a03ce05b710..5351f8e423e6983a89065920d78a879fb27255c5 100644 --- a/premake4.lua +++ b/premake4.lua @@ -71,9 +71,30 @@ configuration "windows" } end +function cppconfig(what) + links { "stdc++" } +end + configuration "macosx" - buildoptions { "-isysroot /Developer/SDKs/MacOSX10.6.sdk", "-mmacosx-version-min=10.6" } + premake.gcc.cc = 'clang' + premake.gcc.cxx = 'clang++' + buildoptions { "-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.7.sdk", "-mmacosx-version-min=10.7" } + function cppconfig(what) + if what == "web" then + buildoptions { "-stdlib=libstdc++" } + linkoptions { "-stdlib=libstdc++" } + else + buildoptions { "-stdlib=libc++" } + linkoptions { "-stdlib=libc++" } + end + links { "stdc++" } + end + includedirs { + "/Library/Frameworks/SDL2.framework/Headers", + "/Library/Frameworks/SDL2_image.framework/Headers", + "/Library/Frameworks/SDL2_ttf.framework/Headers", + } configuration "Debug" defines { } diff --git a/src/main.c b/src/main.c index 1353db4449f9d8f0f27f89fbcbdf10442ef0059b..023621e75dd2bfe765527500841b221860331947 100644 --- a/src/main.c +++ b/src/main.c @@ -36,6 +36,7 @@ #include "physfs.h" #include "physfsrwops.h" #include "core_lua.h" +#include "wfc/lua_wfc_external.h" #include "getself.h" #include "music.h" #include "serial.h" @@ -1176,6 +1177,7 @@ void boot_lua(int state, bool rebooting, int argc, char *argv[]) luaopen_zlib(L); luaopen_bit(L); luaopen_wait(L); + luaopen_wfc(L); physfs_reset_dir_allowed(L); diff --git a/src/wfc/direction.hpp b/src/wfc/direction.hpp new file mode 100644 index 0000000000000000000000000000000000000000..46d2ea8be3dae70241cadfbbbb4ac8c47942cf3a --- /dev/null +++ b/src/wfc/direction.hpp @@ -0,0 +1,16 @@ +#pragma once + + +/** + * A direction is represented by an unsigned integer in the range [0; 3]. + * The x and y values of the direction can be retrieved in these tables. + */ +constexpr int directions_x[4] = {0, -1, 1, 0}; +constexpr int directions_y[4] = {-1, 0, 0, 1}; + +/** + * Return the opposite direction of direction. + */ +constexpr unsigned get_opposite_direction(unsigned direction) noexcept { + return 3 - direction; +} diff --git a/src/wfc/lua_wfc.cpp b/src/wfc/lua_wfc.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d6eecaa4c6705b82f395579f16f17806ee033a8b --- /dev/null +++ b/src/wfc/lua_wfc.cpp @@ -0,0 +1,184 @@ +/* + TE4 - T-Engine 4 + Copyright (C) 2009 - 2018 Nicolas Casalini + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + Nicolas Casalini "DarkGod" + darkgod@te4.org +*/ + +extern "C" { +#include "lauxlib.h" +#include "lualib.h" +#include "auxiliar.h" +#include "lua_wfc_external.h" +#include "SFMT.h" +} +#include "stdlib.h" +#include "string.h" +#include "lua_wfc.hpp" + +static WFCOverlapping *parse_config_overlapping(lua_State *L) { + // Iterate the sample lines to find max size + int sample_w = 9999; + int sample_h = lua_objlen(L, 1); + for (int y = 0; y < sample_h; y++) { + lua_rawgeti(L, 1, y + 1); + size_t len; + unsigned char *line = (unsigned char*)strdup(luaL_checklstring(L, -1, &len)); + if (len < sample_w) sample_w = len; + lua_pop(L, 1); + } + + WFCOverlapping *config = new WFCOverlapping( + luaL_checknumber(L, 4), // n + luaL_checknumber(L, 5), // symmetry + lua_toboolean(L, 6), // periodic_out + lua_toboolean(L, 7), // periodic_in + lua_toboolean(L, 8), // has_foundation + sample_w, sample_h, + luaL_checknumber(L, 2), // output.w + luaL_checknumber(L, 3) // output.h + ); + + // Iterate the sample lines to import them + for (int y = 0; y < sample_h; y++) { + lua_rawgeti(L, 1, y + 1); + size_t len; + unsigned char *line = (unsigned char*)strdup(luaL_checklstring(L, -1, &len)); + for (int x = 0; x < len; x++) { + config->sample.get(y, x) = line[x]; + } + + lua_pop(L, 1); + } + return config; +} + +static void free_config_overlapping(WFCOverlapping *config) { + delete config; +} + +static void generate_table_from_output(lua_State *L, WFCOverlapping *config) { + lua_newtable(L); + printf("===========RESULT\n"); + char *buf = new char[config->output.width+1]; + for (int y = 0; y < config->output.height; y++) { + for (int x = 0; x < config->output.width; x++) { + printf("%c", config->output.get(y, x)); + buf[x] = config->output.get(y, x); + } + buf[config->output.width] = '\0'; + printf("\n"); + + lua_pushlstring(L, buf, config->output.width); + lua_rawseti(L, -2, y + 1); + } + delete[] buf; + printf("===========\n"); +} + +static bool wfc_generate_overlapping(WFCOverlapping *config) { + OverlappingWFCOptions options = {config->periodic_in, config->periodic_out, config->output.height, config->output.width, config->symmetry, config->has_foundation, config->n}; + OverlappingWFC<char> wfc(config->sample, options, gen_rand32()); + cl::optional<Array2D<char>> success = wfc.run(); + if (success.has_value()) { + config->output.import(success.value()); + return true; + } else { + return false; + } +} + +static int lua_wfc_overlapping(lua_State *L) { + WFCOverlapping *config = parse_config_overlapping(L); + + if (wfc_generate_overlapping(config)) { + generate_table_from_output(L, config); + } else { + lua_pushnil(L); + } + + // Cleanup + free_config_overlapping(config); + + return 1; +} + +static int thread_generate_overlapping(void *ptr) { + WFCOverlapping *config = (WFCOverlapping*)ptr; + if (wfc_generate_overlapping(config)) { + return 1; + } else { + return 0; + } +} + +static int lua_wfc_overlapping_async(lua_State *L) { + WFCOverlapping *config = parse_config_overlapping(L); + + SDL_Thread *thread = SDL_CreateThread(thread_generate_overlapping, "particles", config); + + WFCAsync *async = (WFCAsync*)lua_newuserdata(L, sizeof(WFCAsync)); + auxiliar_setclass(L, "wfc{async}", -1); + async->mode = WFCAsyncMode::OVERLAPPING; + async->overlapping_config = config; + async->thread = thread; + return 1; +} + +static int lua_wfc_wait_all_async(lua_State *L) { + return 0; +} + +static int lua_wfc_wait_async(lua_State *L) { + WFCAsync *async = (WFCAsync*)auxiliar_checkclass(L, "wfc{async}", 1); + + int ret; + SDL_WaitThread(async->thread, &ret); + + if (async->mode == WFCAsyncMode::OVERLAPPING) { + if (ret == 1) { + generate_table_from_output(L, async->overlapping_config); + } else { + lua_pushnil(L); + } + // Cleanup + free_config_overlapping(async->overlapping_config); + } + + return 1; +} + +static const struct luaL_Reg async_reg[] = +{ + {"wait", lua_wfc_wait_async}, + {NULL, NULL}, +}; + +static const struct luaL_Reg wfclib[] = +{ + {"asyncOverlapping", lua_wfc_overlapping_async}, + {"asyncWaitAll", lua_wfc_wait_all_async}, + {"overlapping", lua_wfc_overlapping}, + {NULL, NULL}, +}; + +int luaopen_wfc(lua_State *L) { + auxiliar_newclass(L, "wfc{async}", async_reg); + luaL_openlib(L, "core.generator.wfc", wfclib, 0); + lua_settop(L, 0); + return 1; +} diff --git a/src/wfc/lua_wfc.hpp b/src/wfc/lua_wfc.hpp new file mode 100644 index 0000000000000000000000000000000000000000..b5090f80f1ac08a369265a4bc8b1b613fcbdf572 --- /dev/null +++ b/src/wfc/lua_wfc.hpp @@ -0,0 +1,51 @@ +/* + TE4 - T-Engine 4 + Copyright (C) 2009 - 2018 Nicolas Casalini + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + Nicolas Casalini "DarkGod" + darkgod@te4.org +*/ + +#ifndef _WFC_LUA_H_ +#define _WFC_LUA_H_ +#include "tSDL.h" + +#include "wfc.hpp" +#include "overlapping_wfc.hpp" + +struct WFCOverlapping { + unsigned int n; + unsigned int symmetry; + bool periodic_out; + bool periodic_in; + bool has_foundation; + + Array2D<char> sample; + Array2D<char> output; + + WFCOverlapping(int n, int symmetry, bool periodic_out, bool periodic_in, bool has_foundation, int sample_w, int sample_h, int output_w, int output_h) : + n(n), symmetry(symmetry), periodic_out(periodic_out), periodic_in(periodic_in), has_foundation(has_foundation), sample(sample_h, sample_w), output(output_h, output_w) + {} +}; + +enum class WFCAsyncMode { OVERLAPPING, TILED }; +struct WFCAsync { + WFCAsyncMode mode = WFCAsyncMode::OVERLAPPING; + WFCOverlapping *overlapping_config = nullptr; + SDL_Thread *thread; +}; + +#endif diff --git a/src/wfc/lua_wfc_external.h b/src/wfc/lua_wfc_external.h new file mode 100644 index 0000000000000000000000000000000000000000..ac3e844f8aef4ed773b52d86c6bacddbb0e693ee --- /dev/null +++ b/src/wfc/lua_wfc_external.h @@ -0,0 +1,29 @@ +/* + TE4 - T-Engine 4 + Copyright (C) 2009 - 2018 Nicolas Casalini + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + Nicolas Casalini "DarkGod" + darkgod@te4.org +*/ + +#ifndef _WFC_LUA_EXTERNAL_H_ +#define _WFC_LUA_EXTERNAL_H_ + +#include "lua.h" + +extern int luaopen_wfc(lua_State *L); + +#endif diff --git a/src/wfc/optional.hpp b/src/wfc/optional.hpp new file mode 100644 index 0000000000000000000000000000000000000000..01880e3bb41808764fef22bfdff54031b54b7445 --- /dev/null +++ b/src/wfc/optional.hpp @@ -0,0 +1,613 @@ +// Copyright (c) 2016, Charles Lechasseur +// Distributed under the MIT license (see LICENSE). + +// Implementation of C++17's std::optional + +#ifndef CL_OPTIONAL_H +#define CL_OPTIONAL_H + +#include <functional> +#include <stdexcept> +#include <string> +#include <type_traits> +#include <utility> + + +// Compiler detection +#if defined(_MSC_VER) +# if _MSC_VER >= 1800 // Visual Studio 2013 +# define CL_OPTIONAL_VARIADIC +# define CL_OPTIONAL_EXPLICIT_OP_BOOL +# define CL_OPTIONAL_TEMPLATE_FUNCS_DEFAULT_PARAMS +# endif +# if _MSC_VER >= 1900 // Visual Studio 2015 +# define CL_OPTIONAL_NORETURN +# define CL_OPTIONAL_MEMBERS_REF_QUALIFIERS +# endif +# if _MSC_VER >= 1910 // Visual Studio 2017 +# define CL_OPTIONAL_NOEXCEPT // Note: in theory this is supported in VC14, but a bug in VC14 update 3 can cause compiler errors +# define CL_OPTIONAL_CONSTEXPR +# endif +#elif defined(__clang__) +# if __has_feature(cxx_noexcept) +# define CL_OPTIONAL_NOEXCEPT +# endif +# if __has_feature(cxx_constexpr) && __cplusplus >= 201402L +# define CL_OPTIONAL_CONSTEXPR +# endif +# if __has_feature(cxx_attributes) +# define CL_OPTIONAL_NORETURN +# endif +# if __has_feature(cxx_variadic_templates) +# define CL_OPTIONAL_VARIADIC +# endif +# if __has_feature(cxx_reference_qualified_functions) && __cplusplus >= 201402L +# define CL_OPTIONAL_MEMBERS_REF_QUALIFIERS +# endif +# if __has_feature(cxx_explicit_conversions) +# define CL_OPTIONAL_EXPLICIT_OP_BOOL +# endif +# if __has_feature(cxx_default_function_template_args) +# define CL_OPTIONAL_TEMPLATE_FUNCS_DEFAULT_PARAMS +# endif +#elif defined(__GNUG__) +# if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8) +# define CL_OPTIONAL_NOEXCEPT +# define CL_OPTIONAL_NORETURN +# define CL_OPTIONAL_VARIADIC +# define CL_OPTIONAL_MEMBERS_REF_QUALIFIERS +# define CL_OPTIONAL_EXPLICIT_OP_BOOL +# define CL_OPTIONAL_TEMPLATE_FUNCS_DEFAULT_PARAMS +# endif +# if __GNUC__ >= 5 +# define CL_OPTIONAL_CONSTEXPR +# endif +#endif + + +// Define CL_OPTIONAL_NOEXCEPT to get noexcept specifications +#ifdef CL_OPTIONAL_NOEXCEPT +# define _CL_OPT_NOEXCEPT noexcept +# define _CL_OPT_NOEXCEPTEX(...) noexcept(__VA_ARGS__) +#else +# define _CL_OPT_NOEXCEPT +# define _CL_OPT_NOEXCEPTEX(...) +#endif + +// Define CL_OPTIONAL_CONSTEXPR to get constexpr specifications +#ifdef CL_OPTIONAL_CONSTEXPR +# define _CL_OPT_CONSTEXPR constexpr +# define _CL_OPT_CONSTEXPR_OR_CONST constexpr +#else +# define _CL_OPT_CONSTEXPR +# define _CL_OPT_CONSTEXPR_OR_CONST const +#endif + +// Define CL_OPTIONAL_VARIADIC to get variadic templates +#ifdef CL_OPTIONAL_VARIADIC +# define _CL_OPT_VARIADIC 1 +#else +# define _CL_OPT_VARIADIC 0 +#endif + +// Define CL_OPTIONAL_NORETURN to get noreturn specifications +#ifdef CL_OPTIONAL_NORETURN +# define _CL_OPT_NORETURN [[noreturn]] +#else +# define _CL_OPT_NORETURN +#endif + +// Define CL_OPTIONAL_MEMBERS_REF_QUALIFIERS to get lvalue- and rvalue-qualified members +#ifdef CL_OPTIONAL_MEMBERS_REF_QUALIFIERS +# define _CL_OPT_MEMBERS_REF_QUALIFIERS 1 +# define _CL_OPT_LVALUE_REF_QUALIF & +#else +# define _CL_OPT_MEMBERS_REF_QUALIFIERS 0 +# define _CL_OPT_LVALUE_REF_QUALIF +#endif + +// Define CL_OPTIONAL_EXPLICIT_OP_BOOL to get explicit operator bool() +#ifdef CL_OPTIONAL_EXPLICIT_OP_BOOL +# define _CL_OPT_OP_EXPLICIT explicit +#else +# define _CL_OPT_OP_EXPLICIT +#endif + +// Define CL_OPTIONAL_TEMPLATE_FUNCS_DEFAULT_PARAMS to get template methods +// with default template params, like assignment operator from value +#ifdef CL_OPTIONAL_TEMPLATE_FUNCS_DEFAULT_PARAMS +# define _CL_OPT_TEMPL_FUNC_DEF_PARAMS 1 +#else +# define _CL_OPT_TEMPL_FUNC_DEF_PARAMS 0 +#endif + + +#if _CL_OPT_VARIADIC +# include <initializer_list> +#endif + +namespace cl { + +// Type indicating an optional object with no value. See: +// http://en.cppreference.com/w/cpp/utility/optional/nullopt_t +struct nullopt_t { + _CL_OPT_CONSTEXPR nullopt_t(int) { } +}; + +// Instance of nullopt_t passed to optional's members. See: +// http://en.cppreference.com/w/cpp/utility/optional/nullopt +_CL_OPT_CONSTEXPR_OR_CONST nullopt_t nullopt = 0; + +#if _CL_OPT_VARIADIC +// Empty type used for in_place functions. See: +// http://en.cppreference.com/w/cpp/utility/in_place_tag +struct in_place_tag { + in_place_tag() = delete; +private: + in_place_tag(int) { } + friend in_place_tag in_place(); +}; + +// Function to identify an optional method with in-place construction. See: +// http://en.cppreference.com/w/cpp/utility/in_place +inline in_place_tag in_place() { return in_place_tag(0); } +using in_place_t = in_place_tag (&)(); +#endif // _CL_OPT_VARIADIC + +// Exception thrown by optional when trying to access +// an optional with no value. See: +// http://en.cppreference.com/w/cpp/utility/optional/bad_optional_access +class bad_optional_access : public std::logic_error +{ +public: + bad_optional_access(const std::string& what_arg) + : std::logic_error(what_arg) { } + bad_optional_access(const char* what_arg) + : std::logic_error(what_arg) { } +}; + +// Wrapper for an optional value. Part of future C++17 API. See: +// http://en.cppreference.com/w/cpp/utility/optional +template<typename T> +class optional +{ + static_assert(!std::is_reference<T>::value, "cl::optional does not work with reference types"); + static_assert(std::is_destructible<T>::value, "cl::optional requires type to be Destructible"); + +private: + typename std::aligned_storage<sizeof(T), std::alignment_of<T>::value>::type stg_; // We store any value here. + T* const pval_; // Pointer to value stored in stg_. + bool has_value_; // Whether we have a value in stg_. + + // Exception helpers + _CL_OPT_NORETURN void throw_bad_optional_access() const { + throw bad_optional_access("bad_optional_access"); + } + + // Helper for swap when one has a value and the other does not + static void swap_helper(optional& from, optional& to) _CL_OPT_NOEXCEPTEX(std::is_nothrow_move_constructible<T>::value && + std::is_nothrow_destructible<T>::value) + { + new (to.pval_) T(std::move(*from.pval_)); + from.pval_->~T(); + to.has_value_ = true; + from.has_value_ = false; + } + +public: + typedef T value_type; + + // Constructors of optional without a value + _CL_OPT_CONSTEXPR optional() _CL_OPT_NOEXCEPT + : stg_(), pval_(reinterpret_cast<T*>(&stg_)), has_value_(false) { } + _CL_OPT_CONSTEXPR optional(nullopt_t) _CL_OPT_NOEXCEPT + : stg_(), pval_(reinterpret_cast<T*>(&stg_)), has_value_(false) { } + + // Copy constructor + optional(const optional& obj) _CL_OPT_NOEXCEPTEX(std::is_nothrow_copy_constructible<T>::value) + : stg_(), pval_(reinterpret_cast<T*>(&stg_)), has_value_(obj.has_value_) + { + if (has_value_) { + new (pval_) T(*obj.pval_); + } + } + + // Move constructor (note: does not steal has_value_, see API) + optional(optional&& obj) _CL_OPT_NOEXCEPTEX(std::is_nothrow_move_constructible<T>::value) + : stg_(), pval_(reinterpret_cast<T*>(&stg_)), has_value_(obj.has_value_) + { + if (has_value_) { + new (pval_) T(std::move(*obj.pval_)); + } + } + + // Constructors of optional with a value + optional(const T& val) _CL_OPT_NOEXCEPTEX(std::is_nothrow_copy_constructible<T>::value) + : stg_(), pval_(reinterpret_cast<T*>(&stg_)), has_value_(true) + { + new (pval_) T(val); + } + optional(T&& val) _CL_OPT_NOEXCEPTEX(std::is_nothrow_move_constructible<T>::value) + : stg_(), pval_(reinterpret_cast<T*>(&stg_)), has_value_(true) + { + new (pval_) T(std::move(val)); + } + +#if _CL_OPT_VARIADIC + // In-place constructor of optional with a value + template<typename... Args> + _CL_OPT_CONSTEXPR explicit optional(in_place_t, Args&&... args) + _CL_OPT_NOEXCEPTEX(std::is_nothrow_constructible<T, Args&&...>::value) + : stg_(), pval_(reinterpret_cast<T*>(&stg_)), has_value_(true) + { + new (pval_) T(std::forward<Args>(args)...); + } + + // Same but with an initializer_list thrown in just for fun + template<typename U, typename... Args, + typename = typename std::enable_if<std::is_constructible<T, std::initializer_list<U>&, Args&&...>::value, void>::type> + _CL_OPT_CONSTEXPR explicit optional(in_place_t, + std::initializer_list<U> ilist, + Args&&... args) + _CL_OPT_NOEXCEPTEX(std::is_nothrow_constructible<T, std::initializer_list<U>&, Args&&...>::value) + : stg_(), pval_(reinterpret_cast<T*>(&stg_)), has_value_(true) + { + new (pval_) T(ilist, std::forward<Args>(args)...); + } +#endif // _CL_OPT_VARIADIC + + // Destructor + ~optional() _CL_OPT_NOEXCEPTEX(std::is_nothrow_destructible<T>::value) { + if (has_value_) { + // #clp TODO we should not do this for trivially-destructible types as per API + pval_->~T(); + } + } + + // Assignment operator that clears any held value + optional& operator=(nullopt_t) _CL_OPT_NOEXCEPTEX(std::is_nothrow_destructible<T>::value) + { + if (has_value_) { + pval_->~T(); + has_value_ = false; + } + return *this; + } + + // Copy assignment operator + optional& operator=(const optional& obj) _CL_OPT_NOEXCEPTEX(std::is_nothrow_copy_constructible<T>::value && + std::is_nothrow_copy_assignable<T>::value && + std::is_nothrow_destructible<T>::value) + { + if (obj.has_value_) { + if (has_value_) { + // Can assign + *pval_ = *obj.pval_; + } else { + // Need to construct + new (pval_) T(*obj.pval_); + has_value_ = true; + } + } else if (has_value_) { + pval_->~T(); + has_value_ = false; + } + return *this; + } + + // Move assignment (note: does not steal has_value_, see API) + optional& operator=(optional&& obj) _CL_OPT_NOEXCEPTEX(std::is_nothrow_move_constructible<T>::value && + std::is_nothrow_move_assignable<T>::value && + std::is_nothrow_destructible<T>::value) + { + if (obj.has_value_) { + if (has_value_) { + // Can assign + *pval_ = std::move(*obj.pval_); + } else { + // Need to construct + new (pval_) T(std::move(*obj.pval_)); + has_value_ = true; + } + } else if (has_value_) { + pval_->~T(); + has_value_ = false; + } + return *this; + } + +#if _CL_OPT_TEMPL_FUNC_DEF_PARAMS + // Assignment from a value + template<typename U, + typename = typename std::enable_if<std::is_same<typename std::decay<U>::type, T>::value, void>::type> + optional& operator=(U&& val) + _CL_OPT_NOEXCEPTEX(std::is_nothrow_constructible<T, U>::value && + std::is_nothrow_assignable<T, U>::value) + { + if (has_value_) { + // Can assign + *pval_ = std::forward<U>(val); + } else { + // Need to construct + new (pval_) T(std::forward<U>(val)); + has_value_ = true; + } + return *this; + } +#endif // _CL_OPT_TEMPL_FUNC_DEF_PARAMS + + // Unchecked operators that access the value + _CL_OPT_CONSTEXPR const T* operator->() const { + return pval_; + } + _CL_OPT_CONSTEXPR T* operator->() { + return pval_; + } + _CL_OPT_CONSTEXPR const T& operator*() const _CL_OPT_LVALUE_REF_QUALIF { + return *pval_; + } + _CL_OPT_CONSTEXPR T& operator*() _CL_OPT_LVALUE_REF_QUALIF { + return *pval_; + } +#if _CL_OPT_MEMBERS_REF_QUALIFIERS + _CL_OPT_CONSTEXPR const T&& operator*() const && { + return std::move(*pval_); + } + _CL_OPT_CONSTEXPR T&& operator*() && { + return std::move(*pval_); + } +#endif // _CL_OPT_MEMBERS_REF_QUALIFIERS + + // Value detection + _CL_OPT_CONSTEXPR _CL_OPT_OP_EXPLICIT operator bool() const _CL_OPT_NOEXCEPT { + return has_value_; + } + _CL_OPT_CONSTEXPR bool has_value() const _CL_OPT_NOEXCEPT { + return has_value_; + } + + // Value accessors that throw if optional doesn't have a value + _CL_OPT_CONSTEXPR const T& value() const _CL_OPT_LVALUE_REF_QUALIF { + if (!has_value_) { + throw_bad_optional_access(); + } + return *pval_; + } + _CL_OPT_CONSTEXPR T& value() _CL_OPT_LVALUE_REF_QUALIF { + if (!has_value_) { + throw_bad_optional_access(); + } + return *pval_; + } +#if _CL_OPT_MEMBERS_REF_QUALIFIERS + _CL_OPT_CONSTEXPR const T&& value() const && { + if (!has_value_) { + throw_bad_optional_access(); + } + return std::move(*pval_); + } + _CL_OPT_CONSTEXPR T&& value() && { + if (!has_value_) { + throw_bad_optional_access(); + } + return std::move(*pval_); + } +#endif // _CL_OPT_MEMBERS_REF_QUALIFIERS + + // Value accessors with an alternative if optional doesn't have a value + template<typename U> + _CL_OPT_CONSTEXPR T value_or(U&& default_val) const _CL_OPT_LVALUE_REF_QUALIF + _CL_OPT_NOEXCEPTEX(std::is_nothrow_copy_constructible<T>::value && + std::is_nothrow_constructible<T, U>::value) + { + return has_value_ ? *pval_ : T(std::forward<U>(default_val)); + } +#if _CL_OPT_MEMBERS_REF_QUALIFIERS + template<typename U> + _CL_OPT_CONSTEXPR T value_or(U&& default_val) && + _CL_OPT_NOEXCEPTEX(std::is_nothrow_move_constructible<T>::value && + std::is_nothrow_constructible<T, U>::value) + { + return has_value_ ? std::move(*pval_) : T(std::forward<U>(default_val)); + } +#endif // _CL_OPT_MEMBERS_REF_QUALIFIERS + + // Swap implementation + void swap(optional& obj) _CL_OPT_NOEXCEPTEX(std::is_nothrow_move_constructible<T>::value && + std::is_nothrow_destructible<T>::value && + noexcept(std::swap(std::declval<T&>(), std::declval<T&>()))) // #clp TODO using std::swap here seems wrong, no? + { + if (obj.has_value_) { + if (has_value_) { + using std::swap; + swap(*pval_, *obj.pval_); + } else { + swap_helper(obj, *this); + } + } else if (has_value_) { + swap_helper(*this, obj); + } + } + // Non-member version of swap for ADL + friend void swap(optional& obj1, optional& obj2) { + obj1.swap(obj2); + } + + // Removes any value held + void reset() _CL_OPT_NOEXCEPTEX(std::is_nothrow_destructible<T>::value) + { + if (has_value_) { + pval_->~T(); + has_value_ = false; + } + } + +#if _CL_OPT_VARIADIC + // Destroys any held value and constructs a new one in-place + template<typename... Args> + void emplace(Args&&... args) _CL_OPT_NOEXCEPTEX(std::is_nothrow_destructible<T>::value && + std::is_nothrow_constructible<T, Args...>::value) + { + reset(); + new (pval_) T(std::forward<Args>(args)...); + has_value_ = true; + } + template<typename U, typename... Args, + typename = typename std::enable_if<std::is_constructible<T, std::initializer_list<U>&, Args&&...>::value, void>::type> + void emplace(std::initializer_list<U> ilist, + Args&&... args) _CL_OPT_NOEXCEPTEX(std::is_nothrow_destructible<T>::value && + std::is_nothrow_constructible<T, std::initializer_list<U>&, Args&&...>::value) + { + reset(); + new (pval_) T(ilist, std::forward<Args>(args)...); + has_value_ = true; + } +#endif // _CL_OPT_VARIADIC + + // Compares two optionals. One without a value is less than one with a value. + friend bool operator==(const optional& lhs, const optional& rhs) { + return lhs.has_value_ == rhs.has_value_ && + (!lhs.has_value_ || *lhs.pval_ == *rhs.pval_); + } + friend bool operator!=(const optional& lhs, const optional& rhs) { + return !(lhs == rhs); + } + friend bool operator<(const optional& lhs, const optional& rhs) { + if (lhs.has_value_) { + if (rhs.has_value_) { + return *lhs.pval_ < *rhs.pval_; + } else { + return false; + } + } else { + return rhs.has_value_; + } + } + friend bool operator<=(const optional& lhs, const optional& rhs) { + return !(rhs < lhs); + } + friend bool operator>(const optional& lhs, const optional& rhs) { + return rhs < lhs; + } + friend bool operator>=(const optional& lhs, const optional& rhs) { + return !(lhs < rhs); + } + + // Compares optional with a nullopt (like comparing with one without a value). + friend bool operator==(const optional& lhs, nullopt_t) _CL_OPT_NOEXCEPT { + return !lhs.has_value_; + } + friend bool operator==(nullopt_t, const optional& rhs) _CL_OPT_NOEXCEPT { + return !rhs.has_value_; + } + friend bool operator!=(const optional& lhs, nullopt_t) _CL_OPT_NOEXCEPT { + return lhs.has_value_; + } + friend bool operator!=(nullopt_t, const optional& rhs) _CL_OPT_NOEXCEPT { + return rhs.has_value_; + } + friend bool operator<(const optional&, nullopt_t) _CL_OPT_NOEXCEPT { + return false; + } + friend bool operator<(nullopt_t, const optional& rhs) _CL_OPT_NOEXCEPT { + return rhs.has_value_; + } + friend bool operator<=(const optional& lhs, nullopt_t) _CL_OPT_NOEXCEPT { + return !lhs.has_value_; + } + friend bool operator<=(nullopt_t, const optional&) _CL_OPT_NOEXCEPT { + return true; + } + friend bool operator>(const optional& lhs, nullopt_t) _CL_OPT_NOEXCEPT { + return lhs.has_value_; + } + friend bool operator>(nullopt_t, const optional&) _CL_OPT_NOEXCEPT { + return false; + } + friend bool operator>=(const optional&, nullopt_t) _CL_OPT_NOEXCEPT { + return true; + } + friend bool operator>=(nullopt_t, const optional& rhs) _CL_OPT_NOEXCEPT { + return !rhs.has_value_; + } + + // Compares optional with a value (like comparing with one with a value). + friend bool operator==(const optional& lhs, const T& rhs) { + return lhs.has_value_ ? *lhs.pval_ == rhs : false; + } + friend bool operator==(const T& lhs, const optional& rhs) { + return rhs.has_value_ ? lhs == *rhs.pval_ : false; + } + friend bool operator!=(const optional& lhs, const T& rhs) { + return lhs.has_value_ ? *lhs.pval_ != rhs : true; + } + friend bool operator!=(const T& lhs, const optional& rhs) { + return rhs.has_value_ ? lhs != *rhs.pval_ : true; + } + friend bool operator<(const optional& lhs, const T& rhs) { + return lhs.has_value_ ? *lhs.pval_ < rhs : true; + } + friend bool operator<(const T& lhs, const optional& rhs) { + return rhs.has_value_ ? lhs < *rhs.pval_ : false; + } + friend bool operator<=(const optional& lhs, const T& rhs) { + return lhs.has_value_ ? *lhs.pval_ <= rhs : true; + } + friend bool operator<=(const T& lhs, const optional& rhs) { + return rhs.has_value_ ? lhs <= *rhs.pval_ : false; + } + friend bool operator>(const optional& lhs, const T& rhs) { + return lhs.has_value_ ? *lhs.pval_ > rhs : false; + } + friend bool operator>(const T& lhs, const optional& rhs) { + return rhs.has_value_ ? lhs > *rhs.pval_ : true; + } + friend bool operator>=(const optional& lhs, const T& rhs) { + return lhs.has_value_ ? *lhs.pval_ >= rhs : false; + } + friend bool operator>=(const T& lhs, const optional& rhs) { + return rhs.has_value_ ? lhs >= *rhs.pval_ : true; + } +}; + +// Creates an optional by inferring type. See: +// http://en.cppreference.com/w/cpp/utility/optional/make_optional +template<typename T> +_CL_OPT_CONSTEXPR auto make_optional(T&& val) + -> optional<typename std::decay<T>::type> +{ + return optional<typename std::decay<T>::type>(std::forward<T>(val)); +} + +#if _CL_OPT_VARIADIC +// Creates an optional from arguments. See: +// http://en.cppreference.com/w/cpp/utility/optional/make_optional +template<typename T, typename... Args> +_CL_OPT_CONSTEXPR auto make_optional(Args&&... args) + -> optional<T> +{ + return optional<T>(in_place, std::forward<Args>(args)...); +} +template<typename T, typename U, typename... Args> +_CL_OPT_CONSTEXPR auto make_optional(std::initializer_list<U> ilist, Args&&... args) + -> optional<T> +{ + return optional<T>(in_place, ilist, std::forward<Args>(args)...); +} +#endif // _CL_OPT_VARIADIC + +} // cl + +namespace std { + +// Specialize hash for optional +template<typename T> +struct hash<cl::optional<T>> { + size_t operator()(const cl::optional<T>& obj) const { + return obj.has_value() ? hash<T>()(*obj) : 0; + } +}; + +} // std + +#endif // CL_OPTIONAL_H diff --git a/src/wfc/overlapping_wfc.hpp b/src/wfc/overlapping_wfc.hpp new file mode 100644 index 0000000000000000000000000000000000000000..ca162e3175e1a909aaeb61031223c1cf8f98f7fd --- /dev/null +++ b/src/wfc/overlapping_wfc.hpp @@ -0,0 +1,286 @@ +#pragma once + +#include "wfc.hpp" +#include <iostream> + +/** + * Options needed to use the overlapping wfc. + */ +struct OverlappingWFCOptions { + bool periodic_input; // True if the input is toric. + bool periodic_output; // True if the output is toric. + unsigned out_height; // The height of the output in pixels. + unsigned out_width; // The width of the output in pixels. + unsigned symmetry; // The number of symmetries (the order is defined in wfc). + bool ground; // True if the ground needs to be set (see init_ground). + unsigned pattern_size; // The width and height in pixel of the patterns. + + /** + * Get the wave height given these options. + */ + unsigned get_wave_height() const noexcept { + return periodic_output ? out_height : out_height - pattern_size + 1; + } + + /** + * Get the wave width given these options. + */ + unsigned get_wave_width() const noexcept { + return periodic_output ? out_width : out_width - pattern_size + 1; + } +}; + +/** + * Class generating a new image with the overlapping WFC algorithm. + */ +template<typename T> +class OverlappingWFC { + +private: + /** + * The input image. T is usually a color. + */ + Array2D<T> input; + + /** + * Options needed by the algorithm. + */ + OverlappingWFCOptions options; + + /** + * The array of the different patterns extracted from the input. + */ + vector<Array2D<T>> patterns; + + /** + * The underlying generic WFC algorithm. + */ + WFC wfc; + + /** + * Constructor initializing the wfc. + * This constructor is called by the other constructors. + * This is necessary in order to initialize wfc only once. + */ + OverlappingWFC(const Array2D<T>& input, const OverlappingWFCOptions& options, const int& seed, + const pair<vector<Array2D<T>>, vector<double>>& patterns, + const vector<array<vector<unsigned>, 4>>& propagator) noexcept : + input(input), + options(options), + patterns(patterns.first), + wfc(options.periodic_output, seed, patterns.second, propagator, + options.get_wave_height(), options.get_wave_width()) + { + // If necessary, the ground is set. + if(options.ground) { + init_ground(wfc, input, patterns.first, options); + } + } + + /** + * Constructor used only to call the other constructor with more computed parameters. + */ + OverlappingWFC(const Array2D<T>& input, const OverlappingWFCOptions& options, const int& seed, + const pair<vector<Array2D<T>>, vector<double>>& patterns) noexcept : + OverlappingWFC(input, options, seed, patterns, generate_compatible(patterns.first)) + {} + + + /** + * Init the ground of the output image. + * The lowest middle pattern is used as a floor (and ceiling when the input is toric) + * and is placed at the lowest possible pattern position in the output image, on all its width. + * The pattern cannot be used at any other place in the output image. + */ + static void init_ground(WFC& wfc, const Array2D<T>& input, const vector<Array2D<T>>& patterns, const OverlappingWFCOptions& options) noexcept { + unsigned ground_pattern_id = get_ground_pattern_id(input, patterns, options); + + // Place the pattern in the ground. + for(unsigned j = 0; j < options.get_wave_width(); j++) { + for(unsigned p = 0; p < patterns.size(); p++) { + if(ground_pattern_id != p) { + wfc.remove_wave_pattern(options.get_wave_height() - 1, j, p); + } + } + } + + // Remove the pattern from the other positions. + for(unsigned i = 0; i < options.get_wave_height() - 1; i++) { + for(unsigned j = 0; j < options.get_wave_width(); j++) { + wfc.remove_wave_pattern(i, j, ground_pattern_id); + } + } + + // Propagate the information with wfc. + wfc.propagate(); + } + + /** + * Return the id of the lowest middle pattern. + */ + static unsigned get_ground_pattern_id(const Array2D<T>& input, const vector<Array2D<T>>& patterns, const OverlappingWFCOptions& options) noexcept { + // Get the pattern. + Array2D<T> ground_pattern = input.get_sub_array(input.height - 1, input.width / 2, options.pattern_size, options.pattern_size); + + // Retrieve the id of the pattern. + for(unsigned i = 0; i < patterns.size(); i++) { + if(ground_pattern == patterns[i]) { + return i; + } + } + + // The pattern exists. + assert(false); + return 0; + } + + /** + * Return the list of patterns, as well as their probabilities of apparition. + */ + static pair<vector<Array2D<T>>, vector<double>> get_patterns(const Array2D<T>& input, const OverlappingWFCOptions& options) noexcept { + unordered_map<Array2D<T>, unsigned> patterns_id; + vector<Array2D<T>> patterns; + + // The number of time a pattern is seen in the input image. + vector<double> patterns_frequency; + + vector<Array2D<T>> symmetries(8, Array2D<T>(options.pattern_size, options.pattern_size)); + unsigned max_i = options.periodic_input ? input.height : input.height - options.pattern_size + 1; + unsigned max_j = options.periodic_input ? input.width : input.width - options.pattern_size + 1; + + for(unsigned i = 0; i < max_i; i++) { + for(unsigned j = 0; j < max_j; j++) { + // Compute the symmetries of every pattern in the image. + symmetries[0].data = input.get_sub_array(i, j, options.pattern_size, options.pattern_size).data; + symmetries[1].data = symmetries[0].reflected().data; + symmetries[2].data = symmetries[0].rotated().data; + symmetries[3].data = symmetries[2].reflected().data; + symmetries[4].data = symmetries[2].rotated().data; + symmetries[5].data = symmetries[4].reflected().data; + symmetries[6].data = symmetries[4].rotated().data; + symmetries[7].data = symmetries[6].reflected().data; + + // The number of symmetries in the option class define which symetries will be used. + for(unsigned k = 0; k<options.symmetry; k++) { + auto res = patterns_id.insert(make_pair(symmetries[k],patterns.size())); + + // If the pattern already exist, we just have to increase its number of appearance. + if(!res.second) { + patterns_frequency[res.first->second] += 1; + } else { + patterns.push_back(symmetries[k]); + patterns_frequency.push_back(1); + } + } + } + } + + return {patterns, patterns_frequency}; + } + + /** + * Return true if the pattern1 is compatible with pattern2 + * when pattern2 is at a distance (dy,dx) from pattern1. + */ + static bool agrees(const Array2D<T>& pattern1, const Array2D<T>& pattern2, int dy, int dx) noexcept { + unsigned xmin = dx < 0 ? 0 : dx; + unsigned xmax = dx < 0 ? dx + pattern2.width : pattern1.width; + unsigned ymin = dy < 0 ? 0 : dy; + unsigned ymax = dy < 0 ? dy + pattern2.height : pattern1.width; + + // Iterate on every pixel contained in the intersection of the two pattern. + for(unsigned y = ymin; y < ymax; y++) { + for(unsigned x = xmin; x < xmax; x++) { + // Check if the color is the same in the two patterns in that pixel. + if(pattern1.get(y,x) != pattern2.get(y-dy,x-dx)) { + return false; + } + } + } + return true; + } + + /** + * Precompute the function agrees(pattern1, pattern2, dy, dx). + * If agrees(pattern1, pattern2, dy, dx), then compatible[pattern1][direction] contains pattern2, + * where direction is the direction defined by (dy, dx) (see direction.hpp). + */ + static vector<array<vector<unsigned>, 4>> generate_compatible(const vector<Array2D<T>>& patterns) noexcept { + vector<array<vector<unsigned>, 4>> compatible = vector<array<vector<unsigned>, 4>>(patterns.size()); + + // Iterate on every dy, dx, pattern1 and pattern2 + for(unsigned pattern1 = 0; pattern1 < patterns.size(); pattern1++) { + for(unsigned direction = 0; direction < 4; direction++) { + for(unsigned pattern2 = 0; pattern2 < patterns.size(); pattern2++) { + if(agrees(patterns[pattern1], patterns[pattern2], directions_y[direction], directions_x[direction])) { + compatible[pattern1][direction].push_back(pattern2); + } + } + } + } + + return compatible; + } + + + /** + * Transform a 2D array containing the patterns id to a 2D array containing the pixels. + */ + Array2D<T> to_image(const Array2D<unsigned>& output_patterns) const noexcept { + Array2D<T> output = Array2D<T>(options.out_height, options.out_width); + + if(options.periodic_output) { + for(unsigned y = 0; y < options.get_wave_height(); y++) { + for(unsigned x = 0; x < options.get_wave_width(); x++) { + output.get(y,x) = patterns[output_patterns.get(y,x)].get(0,0); + } + } + } else { + for(unsigned y = 0; y < options.get_wave_height(); y++) { + for(unsigned x = 0; x < options.get_wave_width(); x++) { + output.get(y, x) = patterns[output_patterns.get(y,x)].get(0,0); + } + } + for(unsigned y = 0; y < options.get_wave_height(); y++) { + const Array2D<T>& pattern = patterns[output_patterns.get(y, options.get_wave_width()-1)]; + for(unsigned dx = 1; dx < options.pattern_size; dx++) { + output.get(y, options.get_wave_width() - 1 + dx) = pattern.get(0, dx); + } + } + for(unsigned x = 0; x < options.get_wave_width(); x++) { + const Array2D<T>& pattern = patterns[output_patterns.get(options.get_wave_height() - 1, x)]; + for(unsigned dy = 1; dy < options.pattern_size; dy++) { + output.get(options.get_wave_height() - 1 + dy, x) = pattern.get(dy, 0); + } + } + const Array2D<T>& pattern = patterns[output_patterns.get(options.get_wave_height() - 1, options.get_wave_width() - 1)]; + for(unsigned dy = 1; dy < options.pattern_size; dy++) { + for(unsigned dx = 1; dx < options.pattern_size; dx++) { + output.get(options.get_wave_height() - 1 + dy, options.get_wave_width() - 1 + dx) = pattern.get(dy, dx); + } + } + } + + return output; + } + +public: + + /** + * The constructor used by the user. + */ + OverlappingWFC(const Array2D<T>& input, const OverlappingWFCOptions& options, int seed) noexcept : + OverlappingWFC(input, options, seed, get_patterns(input, options)) + {} + + /** + * Run the WFC algorithm, and return the result if the algorithm succeeded. + */ + cl::optional<Array2D<T>> run() noexcept { + cl::optional<Array2D<unsigned>> result = wfc.run(); + if(result.has_value()) { + return to_image(*result); + } + return nullopt; + } +}; diff --git a/src/wfc/propagator.hpp b/src/wfc/propagator.hpp new file mode 100644 index 0000000000000000000000000000000000000000..933f6f43636d5b35afffd642954e769cd268d9f8 --- /dev/null +++ b/src/wfc/propagator.hpp @@ -0,0 +1,155 @@ +#pragma once + +#include <vector> +#include <tuple> +#include <array> +#include "utils/array2D.hpp" +#include "utils/array3D.hpp" +#include "wave.hpp" +#include "direction.hpp" + +using namespace std; + +/** + * Propagate information about patterns in the wave. + */ +class Propagator { + +private: + + /** + * The size of the patterns. + */ + const unsigned patterns_size; + + /** + * propagator[pattern1][direction] contains all the patterns that can be placed in + * next to pattern1 in the direction direction. + */ + const vector<array<vector<unsigned>, 4>> propagator; + + /** + * The wave width and height. + */ + const unsigned wave_width; + const unsigned wave_height; + + /** + * True if the wave and the output is toric. + */ + const bool periodic_output; + + /** + * All the tuples (y, x, pattern) that should be propagated. + * The tuple should be propagated when wave.get(y, x, pattern) is set to false. + */ + vector<tuple<unsigned, unsigned, unsigned>> propagating; + + /** + * compatible.get(y, x, pattern)[direction] contains the number of patterns present in the wave + * that can be placed in the cell next to (y,x) in the opposite direction of direction without being + * in contradiction with pattern placed in (y,x). + * If wave.get(y, x, pattern) is set to false, then compatible.get(y, x, pattern) has every element negative or null + */ + Array3D<array<int, 4>> compatible; + + /** + * Initialize compatible. + */ + void init_compatible() noexcept { + array<int, 4> value; + // We compute the number of pattern compatible in all directions. + for(unsigned y = 0; y < wave_height; y++) { + for(unsigned x = 0; x < wave_width; x++) { + for(unsigned pattern = 0; pattern < patterns_size; pattern++) { + for(int direction = 0; direction < 4; direction++) { + value[direction] = propagator[pattern][get_opposite_direction(direction)].size(); + } + compatible.get(y, x, pattern) = value; + } + } + } + } + +public: + + /** + * Constructor building the propagator and initializing compatible. + */ + Propagator(unsigned wave_height, unsigned wave_width, bool periodic_output, + vector<array<vector<unsigned>, 4>> propagator) noexcept : + patterns_size(propagator.size()), propagator(propagator), wave_width(wave_width), + wave_height(wave_height), periodic_output(periodic_output), + compatible(wave_height, wave_width, patterns_size) + { + init_compatible(); + } + + /** + * Add an element to the propagator. + * This function is called when wave.get(y, x, pattern) is set to false. + */ + void add_to_propagator(unsigned y, unsigned x, unsigned pattern) noexcept { + // All the direction are set to 0, since the pattern cannot be set in (y,x). + array<int, 4> temp = {0,0,0,0}; + compatible.get(y,x,pattern) = temp; + propagating.emplace_back(y, x, pattern); + } + + /** + * Propagate the information given with add_to_propagator. + */ + void propagate(Wave& wave) noexcept { + + // We propagate every element while there is element to propagate. + while(propagating.size() != 0) { + + // The cell and pattern that has been set to false. + unsigned y1, x1, pattern; + tie(y1, x1, pattern) = propagating.back(); + propagating.pop_back(); + + // We propagate the information in all 4 directions. + for(unsigned direction = 0; direction < 4; direction++) { + + // We get the next cell in the direction direction. + int dx = directions_x[direction]; + int dy = directions_y[direction]; + int x2, y2; + if(periodic_output) { + x2 = ((int)x1 + dx + (int)wave.width) % wave.width; + y2 = ((int)y1 + dy + (int)wave.height) % wave.height; + } else { + x2 = x1 + dx; + y2 = y1 + dy; + if(x2 < 0 || x2 >= (int)wave.width) { + continue; + } + if(y2 < 0 || y2 >= (int)wave.height) { + continue; + } + } + + // The index of the second cell, and the patterns compatible + unsigned i2 = x2 + y2 * wave.width; + const vector<unsigned>& patterns = propagator[pattern][direction]; + + // For every pattern that could be placed in that cell without being in contradiction with pattern1 + for(auto it = patterns.begin(), it_end = patterns.end(); it < it_end; ++it) { + + // We decrease the number of compatible patterns in the opposite direction + // If the pattern was discarded from the wave, the element is still negative, which is not a problem + array<int, 4>& value = compatible.get(y2, x2, *it); + value[direction]--; + + // If the element was set to 0 with this operation, we need to remove the pattern + // from the wave, and propagate the information + if(value[direction] == 0) { + add_to_propagator(y2, x2, *it); + wave.set(i2, *it, false); + } + } + } + } + } +}; diff --git a/src/wfc/utils/array2D.hpp b/src/wfc/utils/array2D.hpp new file mode 100644 index 0000000000000000000000000000000000000000..6769d7e442ca8f1a6b57d8d8db07191b49417dbb --- /dev/null +++ b/src/wfc/utils/array2D.hpp @@ -0,0 +1,142 @@ +#pragma once + +#include <vector> +#include <ostream> +#include "assert.h" +#include <iostream> +using namespace std; + +/** + * Represent a 2D array. + * The 2D array is stored in a single array, to improve cache usage. + */ +template<typename T> +class Array2D { + +public: + /** + * Height and width of the 2D array. + */ + unsigned height; + unsigned width; + + /** + * The array containing the data of the 2D array. + */ + vector<T> data; + + /** + * Build a 2D array given its height and width. + * All the array elements are initialized to default value. + */ + Array2D(unsigned height, unsigned width) noexcept : + height(height), width(width), data(width * height) { + } + + /** + * Build a 2D array given its height and width. + * All the array elements are initialized to value. + */ + Array2D(unsigned height, unsigned width, T value) noexcept : + height(height), width(width), data(width * height, value) { + } + + void import(Array2D<T> &src) noexcept { + height = src.height; + width = src.width; + data.clear(); + data = src.data; + } + + /** + * Return a const reference to the element in the i-th line and j-th column. + * i must be lower than height and j lower than width. + */ + const T& get(unsigned i, unsigned j) const noexcept { + assert(i < height && j < width); + return data[j + i * width]; + } + + /** + * Return a reference to the element in the i-th line and j-th column. + * i must be lower than height and j lower than width. + */ + T& get(unsigned i, unsigned j) noexcept { + assert(i < height && j < width); + return data[j + i * width]; + } + + /** + * Return the current 2D array reflected along the x axis. + */ + Array2D<T> reflected() const noexcept { + Array2D<T> result = Array2D<T>(width, height); + for(unsigned y = 0; y < height; y++) { + for(unsigned x = 0; x < width; x++) { + result.get(y, x) = get(y, width - 1 - x); + } + } + return result; + } + + /** + * Return the current 2D array rotated 90° clockwise + */ + Array2D<T> rotated() const noexcept { + Array2D<T> result = Array2D<T>(width, height); + for(unsigned y = 0; y < width; y++) { + for(unsigned x = 0; x < height; x++) { + result.get(y, x) = get(x, width - 1 - y); + } + } + return result; + } + + /** + * Return the sub 2D array starting from (y,x) and with size (sub_width, sub_height). + * The current 2D array is considered toric for this operation. + */ + Array2D<T> get_sub_array(unsigned y, unsigned x, unsigned sub_width, unsigned sub_height) const noexcept { + Array2D<T> sub_array_2d = Array2D<T>(sub_width, sub_height); + for(unsigned ki = 0; ki < sub_height; ki++) { + for(unsigned kj = 0; kj < sub_width; kj++) { + sub_array_2d.get(ki, kj) = get((y+ki) % height, (x+kj) % width); + } + } + return sub_array_2d; + } + + /** + * Check if two 2D arrays are equals. + */ + bool operator==(const Array2D& a) const noexcept { + if(height != a.height || width != a.width) { + return false; + } + + for(unsigned i = 0; i<data.size(); i++) { + if(a.data[i] != data[i]) { + return false; + } + } + return true; + } +}; + +/** + * Hash function. + */ +namespace std { + template<typename T> + class hash<Array2D<T>> { + public: + size_t operator()(const Array2D<T> &a) const noexcept + { + std::size_t seed = a.data.size(); + for(const T& i: a.data) { + seed ^= hash<T>()(i) + (size_t)0x9e3779b9 + (seed << 6) + (seed >> 2); + } + return seed; + } + }; +} diff --git a/src/wfc/utils/array3D.hpp b/src/wfc/utils/array3D.hpp new file mode 100644 index 0000000000000000000000000000000000000000..1751cab57610bb5cad462f2f71c63f310833735e --- /dev/null +++ b/src/wfc/utils/array3D.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include <vector> +#include "assert.h" + +using namespace std; + +/** + * Represent a 3D array. + * The 3D array is stored in a single array, to improve cache usage. + */ +template<typename T> +class Array3D { + +public: + /** + * The dimensions of the 3D array. + */ + const unsigned height; + const unsigned width; + const unsigned depth; + + /** + * The array containing the data of the 3D array. + */ + vector<T> data; + + /** + * Build a 2D array given its height, width and depth. + * All the arrays elements are initialized to default value. + */ + Array3D(unsigned height, unsigned width, unsigned depth) noexcept : + height(height), width(width), depth(depth), data(width * height * depth) { + } + + /** + * Build a 2D array given its height, width and depth. + * All the arrays elements are initialized to value + */ + Array3D(unsigned height, unsigned width, unsigned depth, T value) noexcept : + height(height), width(width), depth(depth), data(width * height * depth, value) { + } + + /** + * Return a const reference to the element in the i-th line, j-th column, and k-th depth. + * i must be lower than height, j lower than width, and k lower than depth. + */ + const T& get(unsigned i, unsigned j, unsigned k) const noexcept { + assert(i < height && j < width << k < depth); + return data[i * width * depth + j * depth + k]; + } + + /** + * Return a reference to the element in the i-th line, j-th column, and k-th depth. + * i must be lower than height, j lower than width, and k lower than depth. + */ + T& get(unsigned i, unsigned j, unsigned k) noexcept { + return data[i * width * depth + j * depth + k]; + } + + /** + * Check if two 3D arrays are equals. + */ + bool operator==(const Array3D& a) const noexcept { + if(height != a.height || width != a.width || depth != a.depth) { + return false; + } + + for(unsigned i = 0; i<data.size(); i++) { + if(a.data[i] != data[i]) { + return false; + } + } + return true; + } +}; diff --git a/src/wfc/utils/color.hpp b/src/wfc/utils/color.hpp new file mode 100644 index 0000000000000000000000000000000000000000..4208fc40f1a05c92cab49f8a4d3c4d772bc1ed5a --- /dev/null +++ b/src/wfc/utils/color.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include <iostream> +#include <vector> + +using namespace std; diff --git a/src/wfc/wave.hpp b/src/wfc/wave.hpp new file mode 100644 index 0000000000000000000000000000000000000000..9c25a44934b01bb878173e2bfd91151660c9dea5 --- /dev/null +++ b/src/wfc/wave.hpp @@ -0,0 +1,215 @@ +#pragma once + +#include <vector> +#include <stdint.h> +#include <limits> +#include <math.h> +#include <random> +#include <iostream> +#include "utils/array2D.hpp" + +using namespace std; + +/** + * Struct containing the values needed to compute the entropy of all the cells. + * This struct is updated every time the wave is changed. + * p'(pattern) is equal to patterns_frequencies[pattern] if wave.get(cell, pattern) is set to true, otherwise 0. + */ +struct EntropyMemoisation { + vector<double> plogp_sum; // The sum of p'(pattern) * log(p'(pattern)). + vector<double> sum; // The sum of p'(pattern). + vector<double> log_sum; // The log of sum. + vector<unsigned> nb_patterns; // The number of patterns present in the wave in the cell. + vector<double> entropy; // The entropy of the cell. +}; + +/** + * Contains the pattern possibilities in every cell. + * Also contains information about cell entropy. + */ +class Wave { +private: + + /** + * The patterns frequencies p given to wfc. + */ + const vector<double> patterns_frequencies; + + /** + * The precomputation of p * log(p). + */ + const vector<double> plogp_patterns_frequencies; + + /** + * The precomputation of min (p * log(p)) / 2. + * This is used to define the maximum value of the noise. + */ + const double half_min_plogp; + + /** + * The memoisation of important values for the computation of entropy. + */ + EntropyMemoisation memoisation; + + /** + * This value is set to true if there is a contradiction in the wave (all elements set to false in a cell). + */ + bool is_impossible; + + /** + * The number of distinct patterns. + */ + const unsigned nb_patterns; + + /** + * The actual wave. data.get(index, pattern) is equal to 0 if the pattern can be placed in the cell index. + */ + Array2D<uint8_t> data; + + /** + * Return distribution * log(distribution). + */ + static vector<double> get_plogp(const vector<double>& distribution) noexcept { + vector<double> plogp; + for(unsigned i = 0; i < distribution.size(); i++) { + plogp.push_back(distribution[i] * log(distribution[i])); + } + return plogp; + } + + /** + * Return min(v) / 2. + */ + static double get_half_min(const vector<double>& v) noexcept { + double half_min = numeric_limits<double>::infinity(); + for(unsigned i = 0; i < v.size(); i++) { + half_min = min(half_min, v[i] / 2.0); + } + return half_min; + } + +public: + /** + * The size of the wave. + */ + const unsigned width; + const unsigned height; + const unsigned size; + + /** + * Initialize the wave with every cell being able to have every pattern. + */ + Wave(unsigned height, unsigned width, const vector<double>& patterns_frequencies) noexcept : + patterns_frequencies(patterns_frequencies), + plogp_patterns_frequencies(get_plogp(patterns_frequencies)), + half_min_plogp(get_half_min(plogp_patterns_frequencies)), + is_impossible(false), + nb_patterns(patterns_frequencies.size()), + data(width * height, nb_patterns, 1), + width(width), height(height), size(height * width) + { + // Initialize the memoisation of entropy. + double base_entropy = 0; + double base_s = 0; + double half_min_plogp = numeric_limits<double>::infinity(); + for(unsigned i = 0; i < nb_patterns; i++) { + half_min_plogp = min(half_min_plogp, plogp_patterns_frequencies[i] / 2.0); + base_entropy += plogp_patterns_frequencies[i]; + base_s += patterns_frequencies[i]; + } + double log_base_s = log(base_s); + double entropy_base = log_base_s - base_entropy / base_s; + memoisation.plogp_sum = vector<double>(width * height, base_entropy); + memoisation.sum = vector<double>(width * height, base_s); + memoisation.log_sum = vector<double>(width * height, log_base_s); + memoisation.nb_patterns = vector<unsigned>(width * height, nb_patterns); + memoisation.entropy = vector<double>(width * height, entropy_base); + } + + /** + * Return true if pattern can be placed in cell index. + */ + bool get(unsigned index, unsigned pattern) const noexcept { + return data.get(index, pattern); + } + + /** + * Return true if pattern can be placed in cell (i,j) + */ + bool get(unsigned i, unsigned j, unsigned pattern) const noexcept { + return get(i * width + j, pattern); + } + + /** + * Set the value of pattern in cell index. + */ + void set(unsigned index, unsigned pattern, bool value) noexcept { + bool old_value = data.get(index, pattern); + // If the value isn't changed, nothing needs to be done. + if(old_value == value) { + return; + } + // Otherwise, the memoisation should be updated. + data.get(index, pattern) = value; + memoisation.plogp_sum[index] -= plogp_patterns_frequencies[pattern]; + memoisation.sum[index] -= patterns_frequencies[pattern]; + memoisation.log_sum[index] = log(memoisation.sum[index]); + memoisation.nb_patterns[index]--; + memoisation.entropy[index] = memoisation.log_sum[index] - memoisation.plogp_sum[index] / memoisation.sum[index]; + // If there is no patterns possible in the cell, then there is a contradiction. + if(memoisation.nb_patterns[index] == 0) { + is_impossible = true; + } + } + + /** + * Set the value of pattern in cell (i,j). + */ + void set(unsigned i, unsigned j, unsigned pattern, bool value) noexcept { + set(i * width + j, pattern, value); + } + + /** + * Return the index of the cell with lowest entropy different of 0. + * If there is a contradiction in the wave, return -2. + * If every cell is decided, return -1. + */ + int get_min_entropy(minstd_rand& gen) const noexcept { + if(is_impossible) { + return -2; + } + + std::uniform_real_distribution<> dis(0,half_min_plogp); + + // The minimum entropy (plus a small noise) + double min = numeric_limits<double>::infinity(); + int argmin = -1; + + for(unsigned i = 0; i < size; i++) { + + // If the cell is decided, we do not compute the entropy (which is equal to 0). + double nb_patterns = memoisation.nb_patterns[i]; + if(nb_patterns == 1) { + continue; + } + + // Otherwise, we take the memoised entropy. + double entropy = memoisation.entropy[i]; + + // We first check if the entropy is less than the minimum. + // This is important to reduce noise computation (which is not negligible). + if(entropy <= min) { + + // Then, we add noise to decide randomly which will be chosen. + // noise is smaller than the smallest p * log(p), so the minimum entropy will always be chosen. + double noise = dis(gen); + if(entropy + noise < min) { + min = entropy + noise; + argmin = i; + } + } + } + return argmin; + } + +}; diff --git a/src/wfc/wfc.hpp b/src/wfc/wfc.hpp new file mode 100644 index 0000000000000000000000000000000000000000..6c8a7a4174265573a1793754a49ef300f47c6abc --- /dev/null +++ b/src/wfc/wfc.hpp @@ -0,0 +1,177 @@ +#pragma once + +#include <unordered_map> +#include <limits> +#include <cmath> +#include <random> + +#include "optional.hpp" +#include "utils/array2D.hpp" +#include "wave.hpp" +#include "propagator.hpp" + +using namespace std; +using namespace cl; + +/** + * Class containing the generic WFC algorithm. + */ +class WFC { +private: + + /** + * The random number generator. + */ + minstd_rand gen; + + /** + * The wave, indicating which patterns can be put in which cell. + */ + Wave wave; + + /** + * The distribution of the patterns as given in input. + */ + const vector<double> patterns_frequencies; + + /** + * The number of distinct patterns. + */ + const unsigned nb_patterns; + + /** + * The propagator, used to propagate the information in the wave. + */ + Propagator propagator; + + /** + * True if the output is periodic. + */ + const bool periodic_output; + + /** + * Transform the wave to a valid output (a 2d array of patterns that aren't in contradiction). + * This function should be used only when all cell of the wave are defined. + */ + Array2D<unsigned> wave_to_output() const noexcept { + Array2D<unsigned> output_patterns(wave.height, wave.width); + for(unsigned i = 0; i< wave.size; i++) { + for(unsigned k = 0; k < nb_patterns; k++) { + if(wave.get(i, k)) { + output_patterns.data[i] = k; + } + } + } + return output_patterns; + } + + +public: + + /** + * Basic constructor initializing the algorithm. + */ + WFC(bool periodic_output, int seed, vector<double> patterns_frequencies, + vector<array<vector<unsigned>, 4>> propagator, unsigned wave_height, unsigned wave_width) noexcept + : gen(seed), wave(wave_height, wave_width, patterns_frequencies), + patterns_frequencies(patterns_frequencies), nb_patterns(propagator.size()), + propagator(wave.height, wave.width, periodic_output, propagator), + periodic_output(periodic_output) + { + } + + /** + * Run the algorithm, and return a result if it succeeded. + */ + optional<Array2D<unsigned>> run() noexcept { + while(true) { + + // Define the value of an undefined cell. + ObserveStatus result = observe(); + + // Check if the algorithm has terminated. + if(result == failure) { + return nullopt; + } else if(result == success) { + return wave_to_output(); + } + + // Propagate the information. + propagator.propagate(wave); + } + } + + /** + * Return value of observe. + */ + enum ObserveStatus { + success, // WFC has finished and has succeeded. + failure, // WFC has finished and failed. + to_continue // WFC isn't finished. + }; + + /** + * Define the value of the cell with lowest entropy. + */ + ObserveStatus observe() noexcept { + // Get the cell with lowest entropy. + int argmin = wave.get_min_entropy(gen); + + // If there is a contradiction, the algorithm has failed. + if(argmin == -2) { + return failure; + } + + // If the lowest entropy is 0, then the algorithm has succeeded and finished. + if(argmin == -1) { + wave_to_output(); + return success; + } + + // Choose an element according to the pattern distribution + double s = 0; + for(unsigned k = 0; k < nb_patterns; k++) { + s+= wave.get(argmin,k) ? patterns_frequencies[k] : 0; + } + + std::uniform_real_distribution<> dis(0,s); + double random_value = dis(gen); + unsigned chosen_value = nb_patterns - 1; + + for(unsigned k = 0; k < nb_patterns; k++) { + random_value -= wave.get(argmin,k) ? patterns_frequencies[k] : 0; + if(random_value <= 0) { + chosen_value = k; + break; + } + } + + // And define the cell with the pattern. + for(unsigned k = 0; k < nb_patterns; k++) { + if(wave.get(argmin, k) != (k == chosen_value)) { + propagator.add_to_propagator(argmin / wave.width, argmin % wave.width, k); + wave.set(argmin, k, false); + } + } + + return to_continue; + } + + /** + * Propagate the information of the wave. + */ + void propagate() noexcept { + propagator.propagate(wave); + } + + /** + * Remove pattern from cell (i,j). + */ + void remove_wave_pattern(unsigned i, unsigned j, unsigned pattern) noexcept { + if(wave.get(i, j, pattern)) { + wave.set(i, j, pattern, false); + propagator.add_to_propagator(i, j, pattern); + } + } +}; +