Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev PR #21

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/documentation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: leafo/gh-actions-lua@v5
- uses: leafo/gh-actions-luarocks@v2
- uses: leafo/gh-actions-lua@v9
- uses: leafo/gh-actions-luarocks@v4
- name: Install ldoc
run: luarocks install ldoc
- name: Generate documentation
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@ jobs:
lua-version: ['5.1', '5.2', '5.3', 'luajit']
steps:
- uses: actions/checkout@v1
- uses: leafo/gh-actions-lua@v5
- uses: leafo/gh-actions-lua@v9
with:
luaVersion: ${{ matrix.lua-version }}
- uses: leafo/gh-actions-luarocks@v2
- uses: leafo/gh-actions-luarocks@v4
- name: Run tests
run: luarocks test
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: leafo/gh-actions-lua@v5
- uses: leafo/gh-actions-lua@v9
with:
luaVersion: "luajit"
- uses: leafo/gh-actions-luarocks@v2
- uses: leafo/gh-actions-luarocks@v4
- name: Install rocks
run: |
luarocks install luacov
luarocks install luaunit
luarocks install luaunit 3.3
luarocks install luacov-reporter-lcov
- name: Run tests
run: |
Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,20 @@ Initial release
- Improved error messages on some subpattern methods
- Slightly improved example gallery generation
- Changed to using LuaRocks as test runner

# 0.6 (WIP)

## Features
- A raycasting tool for determining 'visible' areas of a pattern
from a source cell.

## Bugfix
- Fixed GitHub actions workflows by bumping `gh-action-lua` and
`gh-action-luarocks` versions.
- Fixed luaunit at v3.3

## Misc
- Adjust `pattern.sum` so that it can also take a single table of patterns as
an argument (pattern.sum({a,b,c}) instead of just pattern.sum(a,b,c)).
- Relaxed the assertions on the nature of distance measures in Mitchell
sampling / Poisson disc sampling.
1 change: 1 addition & 0 deletions config.ld
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ file = {
'./forma/subpattern.lua',
'./forma/automata.lua',
'./forma/neighbourhood.lua'
'./forma/raycasting.lua'
}
16 changes: 16 additions & 0 deletions examples/raycasting.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- Raycasting
-- This generates a messy random blocking pattern, selects a random point
-- within it, and casts rays from that point to identify a 'visible' area.

local subpattern = require('forma.subpattern')
local primitives = require('forma.primitives')
local raycasting = require("forma.raycasting")

-- Generate a domain and a messy 'blocking' pattern
local domain = primitives.square(80, 20)
local blocks = subpattern.random(domain, 100)
domain = domain - blocks

-- Cast rays in all direction from a random point in the domain
local traced = raycasting.cast_360(domain:rcell(), domain, 10)
subpattern.print_patterns(domain,{blocks, traced}, {'#', '+'})
1 change: 1 addition & 0 deletions forma/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ primitives = require('forma.primitives')
subpattern = require('forma.subpattern')
automata = require('forma.automata')
neighbourhood = require('forma.neighbourhood')
raycasting = require('forma.raycasting')

8 changes: 7 additions & 1 deletion forma/pattern.lua
Original file line number Diff line number Diff line change
Expand Up @@ -692,10 +692,16 @@ function pattern.intersection(...)
end

--- Generate a pattern consisting of the sum of existing patterns
-- @param ... patterns for summation
-- @param ... patterns for summation, can be either a table ({a,b}) or a list of arguments (a,b)
-- @return A pattern consisting of the sum of the input patterns
function pattern.sum(...)
local patterns = {...}
-- Handle a single, table argument of patterns ({a,b,c}) rather than (a,b,c)
if #patterns == 1 then
if type(patterns[1]) == 'table' then
patterns = patterns[1]
end
end
assert(#patterns > 1, "pattern.sum requires at least two patterns as arguments")
local total = pattern.clone(patterns[1])
for i=2, #patterns, 1 do
Expand Down
108 changes: 108 additions & 0 deletions forma/raycasting.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
--- Ray tracing algorithms
-- Algorithms for identifying visible segments of a pattern from a single cell
-- This can be used for 'field of view' applications
-- Sources:
-- http:--www.adammil.net/blog/v125_Roguelike_Vision_Algorithms.html
-- http://www.roguebasin.com/index.php?title=LOS_using_strict_definition

local ray = {}

local cell = require('forma.cell')
local pattern = require('forma.pattern')

--- Casts a ray from a start to an end cell.
-- Returns {true/false} if the cast is successful/blocked.
-- Adapted from: http://www.roguebasin.com/index.php?title=LOS_using_strict_definition
-- @param v0 starting cell of ray
-- @param v1 end cell of ray
-- @param the domain in which we are casting
-- @return true or false depending on whether the ray was successfully cast
function ray.cast(v0, v1, domain)
assert(getmetatable(v0) == cell, "ray.cast requires a cell as the first argument")
assert(getmetatable(v1) == cell, "ray.cast requires a cell as the second argument")
assert(getmetatable(domain) == pattern, "ray.cast requires a pattern as the third argument")
-- Start or end cell was already blocked
if domain:has_cell(v0.x, v0.y) == false or domain:has_cell(v1.x, v1.y) == false then
return false
end
-- Initialise line walk
local dv = v1 - v0
local sx = (v0.x < v1.x) and 1 or -1
local sy = (v0.y < v1.y) and 1 or -1
-- Rasterise step by step
local nx = v0:clone()
local denom = cell.euclidean(v1, v0)
while (nx.x ~= v1.x or nx.y ~= v1.y) do
-- Ray is blocked
if domain:has_cell(nx.x, nx.y) == false then
return false
-- Ray is not blocked, calculate next step
elseif(math.abs(dv.y * (nx.x - v0.x + sx) - dv.x * (nx.y - v0.y)) / denom < 0.5) then
nx.x = nx.x + sx
elseif(math.abs(dv.y * (nx.x - v0.x) - dv.x * (nx.y - v0.y + sy)) / denom < 0.5) then
nx.y = nx.y + sy
else
nx.x = nx.x + sx
nx.y = nx.y + sy
end
end
-- Successfully traced a ray
return true
end

--- Casts rays from a start cell across an octant.
-- @param v0 starting cell of ray
-- @param the domain in which we are casting
-- @param the octant identifier (integer between 1 and 8)
-- @param radius the maximum length of the ray
-- @return the pattern illuminated by the ray casting
function ray.cast_octant(v0, domain, oct, ray_length)
assert(getmetatable(v0) == cell, "ray.cast_octant requires a cell as the first argument")
assert(getmetatable(domain) == pattern, "ray.cast_octant requires a pattern as the second argument")
assert(type(oct) == 'number', "ray.cast_octant requires a number as the third argument")
assert(type(ray_length) == 'number', "ray.cast_octant requires a number as the fourth argument")
local function transformOctant(r, c)
if oct == 1 then return r, -c end
if oct == 2 then return r, c end
if oct == 3 then return c, r end
if oct == 4 then return -c, r end
if oct == 5 then return -r, c end
if oct == 6 then return -r, -c end
if oct == 7 then return -c, -r end
if oct == 8 then return c, -r end
end
local lit_pattern = pattern.new()
for row=1,ray_length,1 do
for col=0,row,1 do
local tcol,trow = transformOctant(row,col)
local v1 = v0:clone() + cell.new(tcol, -trow)
if cell.euclidean2(v0, v1) < ray_length*ray_length then
ray_status = ray.cast(v0,v1,domain)
-- Successful ray casting, add to the illuminated pattern
if ray_status == true then
lit_pattern:insert(v1.x, v1.y)
end
end
end
end
return lit_pattern
end

--- Casts rays from a starting cell in all directions
-- @param v0 starting cell of ray
-- @param the domain in which we are casting
-- @param the maximum length of the ray
-- @return the pattern illuminated by the ray casting
function ray.cast_360(v, domain, ray_length)
assert(getmetatable(v) == cell, "ray.cast_360 requires a cell as the first argument")
assert(getmetatable(domain) == pattern, "ray.cast_360 requires a pattern as the second argument")
assert(type(ray_length) == 'number', "ray.cast_360 requires a number as the third argument")
local lit_pattern = pattern.new():insert(v.x, v.y)
for ioct=1,8,1 do
local np = ray.cast_octant(v, domain, ioct, ray_length)
lit_pattern = lit_pattern + np
end
return lit_pattern
end

return ray
6 changes: 2 additions & 4 deletions forma/subpattern.lua
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ end
-- @return a Poisson-disc sample of `domain`
function subpattern.poisson_disc(ip, distance, radius, rng)
assert(getmetatable(ip) == pattern, "subpattern.poisson_disc requires a pattern as the first argument")
assert(distance(cell.new(5,5), cell.new(5,5)) == 0,
"subpattern.poisson_disc requires a distance measure as the second argument")
assert(type(distance) == 'function', "subpattern.poisson_disc requires a distance measure as an argument")
assert(type(radius) == "number", "subpattern.poisson_disc requires a number as the target radius")
if rng == nil then rng = math.random end
local sample = pattern.new()
Expand Down Expand Up @@ -118,8 +117,7 @@ function subpattern.mitchell_sample(ip, distance, n, k, rng)
"subpattern.mitchell_sample requires a pattern as the first argument")
assert(ip:size() >= n,
"subpattern.mitchell_sample requires a pattern with at least as many points as in the requested sample")
assert(distance(cell.new(5,5), cell.new(5,5)) == 0,
"subpattern.mitchell_sample requires a distance measure as the second argument")
assert(type(distance) == 'function', "subpattern.mitchell_sample requires a distance measure as an argument")
assert(type(n) == "number", "subpattern.mitchell_sample requires a target number of samples")
assert(type(k) == "number", "subpattern.mitchell_sample requires a target number of candidate tries")
if rng == nil then rng = math.random end
Expand Down
14 changes: 3 additions & 11 deletions rockspec/forma-scm-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,9 @@ dependencies = {
"lua >= 5.1"
}
build = {
type = "builtin",
modules = {
forma = "forma/init.lua",
["forma.automata"] = "forma/automata.lua",
["forma.cell"] = "forma/cell.lua",
["forma.neighbourhood"] = "forma/neighbourhood.lua",
["forma.pattern"] = "forma/pattern.lua",
["forma.primitives"] = "forma/primitives.lua",
["forma.subpattern"] = "forma/subpattern.lua",
},
type = "none",
copy_directories = {
"forma",
"tests"
}
}
Expand All @@ -45,5 +37,5 @@ test = {
flags = {"-v"}
}
test_dependencies = {
"luaunit >=3.3"
"luaunit ==3.3"
}
2 changes: 2 additions & 0 deletions tests/pattern.lua
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,13 @@ function testPattern:testSum()
{0,0,0,0,0}})
local tp12 = primitives.square(5)
local sum = pattern.sum(tp1, tp2)
local sum_v2 = pattern.sum({tp1, tp2})
lu.assertEquals(tp1+tp2, tp12)
lu.assertEquals(tp1+tp2, sum)
lu.assertEquals(tp12, sum)
lu.assertNotEquals(tp1, sum)
lu.assertNotEquals(tp2, sum)
lu.assertEquals(sum, sum_v2)
end

-- Test insert methods
Expand Down
36 changes: 36 additions & 0 deletions tests/raycasting.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
--- Tests of basic forma primitives
local lu = require('luaunit')
local pattern = require("forma.pattern")
local primitives = require("forma.primitives")
local subpattern = require("forma.subpattern")
local raycasting = require("forma.raycasting")

testRaycasting = {}

-- Test single ray ------------------------------------------------
function testRaycasting:testRay()
-- Test here is simmilar to line primitives
-- Draw a bunch of lines, check their properties
local domain = primitives.square(100)
for _=1, 100, 1 do
local success = raycasting.cast( domain:rcell(), domain:rcell(), domain )
-- Must succeed
lu.assertTrue(success)
end
end

-- Test 360 raycasting ------------------------------------------------
function testRaycasting:test360()
local domain = primitives.square(100)
for _=1, 100, 1 do
local start = domain:rcell()
local traced = raycasting.cast_360( start, domain, 5 )
-- Must have start cells
lu.assertTrue(traced:has_cell(start.x, start.y))
-- Most be contained within the domain
lu.assertEquals(pattern.intersection(domain, traced), traced)
-- Must consist of one contiguous area
local floodfill = subpattern.floodfill(traced, start)
lu.assertEquals(floodfill, traced)
end
end
1 change: 1 addition & 0 deletions tests/run.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ require('tests.primitives')
require('tests.neighbourhood')
require('tests.subpattern')
require('tests.automata')
require('tests.raycasting')

math.randomseed(0)

Expand Down