gemai/context.lua

139 lines
3.5 KiB
Lua

gemai.context = {}
function gemai.context.new(def, data)
local self = {}
self.def = b.t.combine({
global_actions = {},
global_events = {},
global_flags = {},
states = {},
}, table.copy(def))
for k,state in pairs(self.def.states) do
state.flags = b.t.combine(self.def.global_flags, state.flags or {})
state.events = b.t.combine(self.def.global_events, state.events or {})
state.actions = b.t.icombine(self.def.global_actions, state.actions or {})
end
setmetatable(self, {
__index = gemai.context,
})
self:load(b.t.combine({
state = "init",
-- Time since last state change.
state_time = 0,
-- Time since context creation.
live_time = 0,
-- Delta since last step.
step_time = 0,
-- Event queue.
events = {},
-- Current event parameters.
params = {},
}, data or {}))
return self
end
function gemai.context:load(data)
self.data = data
end
function gemai.context:save(data)
return self.data
end
-- Human readable debug description.
function gemai.context:debug_desc()
return "unknown"
end
function gemai.context:is_valid()
return true
end
-- Get the current state definition.
function gemai.context:state()
return self:assert(self.def.states[self.data.state], "no such state: " .. tostring(self.data.state))
end
-- Run an AI step.
-- <dtime> seconds have elapsed since the last step.
function gemai.context:step(dtime)
-- Update timers.
self.data.step_time = dtime
self.data.live_time = self.data.live_time + dtime
self.data.state_time = self.data.state_time + dtime
while #self.data.events > 0 do
-- Pop the next event.
local event = self.data.events[1]
table.remove(self.data.events, 1)
-- Empty string means ignore.
if self:state().events[event.name] ~= "" then
-- Update state from according to the event handler.
self.data.state = self:assert(self:state().events[event.name], "state '" .. self.data.state .. "' does not have event '" .. event.name .. "'")
-- Update current params.
self.data.params = event.params
-- Reset time in state.
self.data.state_time = 0
-- If this is a terminating event, clear the remaining queued events.
if event.terminate then
self.data.events = {}
end
-- Break on the first actionable event we find.
break
end
end
-- Run all state actions.
for _,action in ipairs(self:state().actions) do
-- If the context is no longer valid, stop processing.
if not self:is_valid() then
break
end
self:assert(gemai.actions[action], "gemai action does not exist: " .. action)(self)
-- If the action inserted a breaking event, don't process any more actions.
if self.data.events[1] and self.data.events[#self.data.events].break_state and self:state().events[self.data.events[#self.data.events].name] ~= "" then
break
end
end
end
function gemai.context:fire_event(event, params, options)
local options = b.t.combine({
-- No events adding before this one will be fired.
clear = false,
-- No events added after this one will propagate.
terminate = true,
-- This event will stop action processing.
break_state = true,
}, options)
-- If this is a clearing event, clear the previous queued events.
if options.clear then
self.data.events = {}
end
table.insert(self.data.events, {
name = event,
params = params or {},
terminate = options.terminate,
break_state = options.break_state,
})
end
function gemai.context:assert(condition, message)
local message = (message and (message .. " ") or "") .. "[gemai: " .. self:debug_desc() .. (" %s]"):format(self.data.state)
if condition then
return condition
else
error(message, 2)
end
end