Skip to content
A generalized PICO-8 framework for ECS and prototype-oriented programming.
Lua
Branch: master
Clone or download

Latest commit

Fetching latest commit…
Cannot retrieve the latest commit at this time.

Files

Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
img
LICENSE
README.md
fila.p8

README.md

Maintenance License Made With PICO-8

PICO-Tween

Fila is a generalized PICO-8 framework derived from ECS paradigm and prototype-oriented programming. It allows developers to model both data and logic in an unprecedented way.

Demo

-- create a new fila instance
local life = fila()

-- life --> "mortal"
life:knot("mortal")
assert(life:find("mortal"))

-- ===========================

-- create a child instance of
-- life
local animal = life()

assert(animal:get_parent()
    == life)
assert(animal:is(life))

-- * life is mortal (knot)
--   animal is life (instance)
--   animal is mortal
--    (knot inheritance)
assert(animal:find("mortal"))

-- ===========================

local bird = animal()

assert(bird:is(animal))
assert(bird:is(life))
assert(bird:find("mortal"))

-- bird -[true]-> "can_fly"
bird:knot("can_fly", true)

local res, param
res, param = bird:find("can_fly")
assert(res == true)
assert(param == true)
assert(not animal:find("can_fly"))

-- ===========================

local penguin = bird()

-- override parent's knot
penguin:knot("can_fly", false)

res, param = penguin:find("can_fly")
assert(res == true)
assert(param == false)

-- ===========================

-- more compact way to create
-- an instance with knots
local ostrich = bird {
    can_fly = false,
    "funny", -- no parameter
}

-- more compact way to get knot
assert(ostrich:get("can_fly")
    == false)
assert(ostrich:get("funny")
    == nil) -- no parameter
    
-- life:get("can_fly")
-- ! error: knot not found

-- ostrich:unknot("mortal")
-- ! error: knot not found
--   * parent's knots cannot be
--     removed from children

ostrich:knot("mortal")
ostrich:unknot("mortal")
-- unknot only removes knots
-- from the instance itself,
-- life is still mortal
assert(ostrich:find("mortal"))

-- ===========================

assert(bird:get_children_count()
    == 2)

local children = {}
for child in bird:iter_children() do
    children[child] = true
end

assert(children[penguin])
assert(children[ostrich])

-- ===========================

local knots = {}
for knot, param in penguin:iter_knots() do
    knots[knot] = param == nil
        and "no_param" or param
end

assert(knots["can_fly"] == false)
-- iter_knots only provide
-- knots added specifically
-- on this instance
assert(knots["mortal"] == nil)

-- ===========================

-- group containing children
-- of life that has remaining-
-- _lifetime less or equal to 0
local dead_grp =
    life:group(function(f)
        local res, l =
            f:find("remaining_lifetime")
        return res and l <= 0
    end)
    
-- when an life is added to the
-- group, knot it with "dead"
dead_grp:on_add(function(g, f)
    f:knot("dead")
end)

-- when an life is removed from
-- the group, unknot "dead"
dead_grp:on_remove(function(g, f)
    f:unknot("dead")
end)

-- * use unlisten_add and
--   unlisten_remove to remove
--   callbacks from group

local old_penguin = penguin {
    remaining_lifetime = 1
}

assert(not dead_grp:has(old_penguin))
assert(not old_penguin:find("dead"))

old_penguin:knot(
    "remaining_lifetime", 0)

assert(dead_grp:count() == 1)
assert(dead_grp:has(old_penguin))
assert(old_penguin:find("dead"))
    
local old_ostrich = ostrich {
    remaining_lifetime = -1
}

assert(dead_grp:count() == 2)
assert(dead_grp:has(old_ostrich))
assert(old_ostrich:find("dead"))

-- don't misinterpret * child
-- instance * as 'child' in
-- real world. it's more like
-- * derived concept *.
-- a child instance of old_-
-- penguin inherits all the
-- knots from old_penguin, thus
-- is also dead (unfortunately)
local old_penguin_2 =
    old_penguin {
        other_knot = "other_param"
    }
assert(old_penguin_2:find("dead"))

local dead = {}

for i, f in dead_grp:iter() do
    dead[f] = true
end

assert(dead[old_penguin])
assert(dead[old_ostrich])

-- let's revive them!

old_penguin:knot(
    "remaining_lifetime", 41)
old_ostrich:knot(
    "remaining_lifetime", 42)
    
assert(not old_penguin:find("dead"))
assert(not old_ostrich:find("dead"))

-- since old_penguin_2 is the
-- child instance and does not
-- override its parent's
-- remaining_lifetime knot,
-- when the parent gets revived
-- it revives at the same time
assert(not old_penguin_2:find("dead"))

assert(dead_grp:count() == 0)
assert(not dead_grp:has(old_penguin))
assert(not dead_grp:has(old_ostrich))

-- groups do not act on filae
-- containing them
life:knot(
    "remaining_lifetime", -1)
assert(not life:find("dead"))
life:unknot(
    "remaining_lifetime")
    
-- sadly, old_penguin does not
-- hold the answer to life,
-- the universe, and everything
old_penguin:knot(
    "remaining_lifetime", 0)
assert(old_penguin:find("dead"))
assert(old_penguin_2:find("dead"))

-- groups does not get released
-- after not referenced any
-- more, you must destroy them
-- manually
dead_grp:destroy()

-- when getting destroyed, all
-- filae in the group are
-- removed, triggering on_remove
-- callbacks
assert(not old_penguin:find("dead"))
assert(not old_penguin_2:find("dead"))

-- * groups created by group
--   method have very poor
--   performance since the
--   predicate function has to
--   be executed each time when
--   any child of the instance
--   knots or unknots anything

-- * more performance can be
--   gained by seperating
--   groups into different fila
--   instances that really need
--   them

-- * for better performance,
--   use * fast group *

-- ===========================

-- fast groups are literally
-- faster but limited in their
-- instance selecting method:
-- they can only be used to
-- select instances that have
-- specific set of knots

-- g contains all the instances
-- of life which have remainin-
-- g_lifetime knot (and any
-- pamameter of it)
local g = life:fast_group(
    "remaining_lifetime")
    
-- note that fast groups are
-- cached - invoking fast_group
-- twice with the same knots as
-- arguments on the same fila
-- gets the same group
assert(g == life:fast_group(
    "remaining_lifetime"))
    
assert(g:count() == 3)
assert(g:has(old_penguin))
assert(g:has(old_penguin_2))
assert(g:has(old_ostrich))

local dead = {}

for i, f in g:iter() do
    dead[f] = true
end

assert(dead[old_penguin])
assert(dead[old_penguin_2])
assert(dead[old_ostrich])

-- on_add_iter listens the add
-- event just as on_add, while
-- also invoking the callback
-- for each fila that has been
-- added to the group before
g:on_add_iter(function(g, f)
    -- filae in the group must
    -- have remaining_lifetime
    -- knot
    local l =
        f:get("remaining_lifetime")
    if l <= 0 then
        f:knot("dead")
    end
end)

g:on_remove(function(g, f)
    f:unknot("dead")
end)

assert(old_penguin:find("dead"))
assert(old_penguin_2:find("dead"))
-- recall that old_ostrich has
-- remaining lifetime of 42
assert(not old_ostrich:find("dead"))

-- now let's revive old_penguin
-- (and old_penguin_2)
old_penguin:knot(
    "remaining_lifetime", 42)

-- but old penguin is still
-- dead! this is because on_add
-- callback only gets invoked
-- when remaining_lifetime knot
-- is added to a fila (a knot
-- event), not when modified
-- (a reknot event)
assert(old_penguin:find("dead"))

-- * to achieve the result we
--   want, i.e. invoking some
--   callbacks each time when a
--   fila that has specific set
--   of knots changes one of
--   those knots, promote the
--   existing group to or
--   create a new * reactive
--   group *

-- ===========================

-- create a new reactive group
-- (if a fast group with the
-- same knot arguments has 
-- already existed, it will be
-- promoted to reactive group)
local g = life:reactive_group(
    "remaining_lifetime")

-- on_react callbacks are
-- invoked each time when a
-- fila is added to the group
-- or updates its knots that
-- are used by this group to
-- select filae (in this case,
-- it's remaining_lifetime)

-- just like on_add_iter, call-
-- backs added by on_react will
-- be invoked firstly for each
-- existing fila in the group
g:on_react(function(g, f)
    local l =
        f:get("remaining_lifetime")
    if l <= 0 then
        f:knot("dead")
    else
        -- try_unknot won't throw
        -- an error if the knot does
        -- not exist
        f:try_unknot("dead")
    end
end)

-- * for removing the callback,
--   use unlisten_react

-- now, old_penguin and old_pe-
-- guin_2 are alive!
assert(not old_penguin:find("dead"))
assert(not old_penguin_2:find("dead"))

old_ostrich:knot(
    "remaining_lifetime", 0)
assert(old_ostrich:find("dead"))

-- * in essence, a group repre-
--   sents some sort of rule
--   that must be abided by all
--   the child instances of the
--   fila containing the group

-- ===========================

-- as you may have gussed,
-- fast groups and reactive
-- groups can have multiple
-- knot targets as their
-- arguments to select child
-- instances (which are
-- called knottees in fila
-- paradigm, so knot = knottee
-- + optional parameter)

-- a dead life will start
-- decomposing
life:fast_group("dead")
    :on_add(function(g, f)
        f:knot("decomposing")
    end)

local during_decomposing
local goodbye

-- a decomposing life which
-- has decomposing time greater
-- or equal to 1 will be
-- destroyed (for fila, it's
-- done by reset method) 
life:reactive_group(
    "decomposing",
    "decomposing_time")
    :on_react(function(g, f)
        if f:get("decomposing_time")
            >= 1 then
            f:reset()
        end
    end)
    -- on_* methods returns the
    -- group itself, so you can
    -- chain them together
    :on_add(function(g, f)
        during_decomposing = true
    end)
    :on_remove(function(g, f)
        goodbye = true
    end)

old_penguin:knot(
    "remaining_lifetime", 0)
    
assert(old_penguin:find(
    "dead"))
assert(old_penguin:find(
    "decomposing"))

assert(not during_decomposing)

old_penguin:knot(
    "decomposing_time", 0)
    
assert(during_decomposing)

old_penguin:knot(
    "decomposing_time", 1)
    
assert(goodbye)

-- once a fila is reset it will
-- be detached from its parent,
-- and all the knots and groups
-- will be removed, as if you 
-- reassign the variable with
-- an empty fila created by
-- `fila()`
local ks = {}
for k, p in
    old_penguin:iter_knots() do
    ks[#ks+1] = k
end
assert(#ks == 0)
assert(old_penguin:get_parent()
    == nil)
    
-- reset is recursive
assert(old_penguin:get_children_count()
    == 0)
assert(old_penguin_2:get_parent()
    == nil)

-- old_penguin and old_penguin-
-- _2 now are totally dead, but
-- you can reuse those empty
-- filae left by them and
-- * reattach * them to other
-- fila, which, in a romantic
-- interpretation, is analogous 
-- to the reincarnation of life
local plant = life()
local angiosperm = plant()
angiosperm:knot("has_flower")

local rose = old_penguin
rose:reattach(angiosperm)
assert(rose:is(plant))
assert(rose:find("has_flower"))

local patchouli = old_penguin_2
patchouli:reattach(angiosperm)
assert(patchouli:is(plant))
assert(patchouli:find("has_flower"))

-- ===========================

-- it is possible to lift a
-- fila to the same level as
-- its parent using lift method

local super_penguin = penguin()
super_penguin:knot(
    "can_fly", true)

local super_penguin_2 =
    super_penguin()
    
assert(super_penguin_2:get_parent()
    == super_penguin)
assert(super_penguin_2:get(
    "can_fly") == true)
    
super_penguin_2:lift()

assert(super_penguin_2:get_parent()
    == penguin)
    
-- when lifting, all knots
-- owned by fila's parent will
-- be copied to the fila
assert(super_penguin_2:get(
    "can_fly") == true)
    
-- ===========================

-- detach method repeats
-- lifting the fila until it
-- has no more parent (at top
-- level)

local super_penguin_template =
    fila()

super_penguin_template
    :knot("can_fly", true)
    
local mega_super_penguin_template =
    super_penguin_template()
    
mega_super_penguin_template
    :knot("can_fly_into_space")
    
local the_penguin =
    mega_super_penguin_template()

the_penguin:detach()

assert(the_penguin:get_parent()
    == nil)
assert(the_penguin:get(
    "can_fly") == true)
assert(the_penguin:find(
    "can_fly_into_space"))

-- using reattach to attach the
-- penguin to penguin fila
the_penguin:reattach(penguin)
assert(the_penguin:get_parent()
    == penguin)

-- reattach will automatically
-- invoke detach method if the
-- fila has non-nil parent, so
-- you can use it directly
local the_penguin_2 =
    mega_super_penguin_template()
the_penguin_2:reattach(penguin)

assert(the_penguin_2:get(
    "can_fly") == true)
assert(the_penguin_2:find(
    "can_fly_into_space"))
assert(the_penguin_2:get_parent()
    == penguin)
You can’t perform that action at this time.