# This file is part of LiSE, a framework for life simulation games.
# Copyright (c) Zachary Spector, public@zacharyspector.com
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3.
#
# 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Database access and query builder
Sometimes you want to know when some stat of a LiSE entity had a particular
value. To find out, construct a historical query and pass it to
``Engine.turns_when``, like this::
physical = engine.character['physical']
that = physical.thing['that']
hist_loc = that.historical('location')
print(list(engine.turns_when(hist_loc == 'there')))
You'll get the turns when ``that`` was ``there``.
Other comparison operators like ``>`` and ``<`` work as well.
"""
import operator
from collections.abc import MutableMapping, Sequence, Set
from itertools import chain
from operator import gt, lt, eq, ne, le, ge
from functools import partialmethod
from time import monotonic
from queue import Queue
from threading import Thread
from typing import Any, List, Callable, Tuple
import numpy as np
from sqlalchemy import select, and_, Table
from sqlalchemy.sql.functions import func
from .alchemy import meta, gather_sql
from .allegedb import query
from .exc import (IntegrityError, OperationalError)
from .util import EntityStatAccessor
import LiSE
def windows_union(windows: List[Tuple[int, int]]) -> List[Tuple[int, int]]:
"""Given a list of (beginning, ending), return a minimal version that
contains the same ranges.
:rtype: list
"""
def fix_overlap(left, right):
if left == right:
return [left]
assert left[0] < right[0]
if left[1] >= right[0]:
if right[1] > left[1]:
return [(left[0], right[1])]
else:
return [left]
return [left, right]
if len(windows) == 1:
return windows
none_left = []
none_right = []
otherwise = []
for window in windows:
if window[0] is None:
none_left.append(window)
elif window[1] is None:
none_right.append(window)
else:
otherwise.append(window)
res = []
otherwise.sort()
for window in none_left:
if not res:
res.append(window)
continue
res.extend(fix_overlap(res.pop(), window))
while otherwise:
window = otherwise.pop(0)
if not res:
res.append(window)
continue
res.extend(fix_overlap(res.pop(), window))
for window in none_right:
if not res:
res.append(window)
continue
res.extend(fix_overlap(res.pop(), window))
return res
def intersect2(left, right):
"""Return intersection of 2 windows of time"""
if left == right:
return left
elif left == (None, None) or left == ((None, None), (None, None)):
return right
elif right == (None, None) or right == ((None, None), (None, None)):
return left
elif left[0] is None or left[0] == (None, None):
if right[0] is None or right[0] == (None, None):
return None, min((left[1], right[1]))
elif right[1] is None or right[1] == (None, None):
if left[1] <= right[0]:
return left[1], right[0]
else:
return None
elif right[0] <= left[1]:
return right[0], left[1]
else:
return None
elif left[1] is None or left[1] == (None, None):
if right[0] is None or right[0] == (None, None):
return left[0], right[1]
elif left[0] <= right[0]:
return right
elif right[1] is None or right[1] == (None, None):
return max((left[0], right[0])), (None, None) if isinstance(
left[0], tuple) else None
elif left[0] <= right[1]:
return left[0], right[1]
else:
return None
# None not in left
elif right[0] is None or right[0] == (None, None):
return left[0], min((left[1], right[1]))
elif right[1] is None or right[1] == (None, None):
if left[1] >= right[0]:
return right[0], left[1]
else:
return None
if left > right:
(left, right) = (right, left)
if left[1] >= right[0]:
if right[1] > left[1]:
return right[0], left[1]
else:
return right
return None
def windows_intersection(
windows: List[Tuple[int, int]]) -> List[Tuple[int, int]]:
"""Given a list of (beginning, ending), describe where they overlap.
Only ever returns one item, but puts it in a list anyway, to be like
``windows_union``.
:rtype: list
"""
if len(windows) == 0:
return []
elif len(windows) == 1:
return list(windows)
done = [windows[0]]
for window in windows[1:]:
res = intersect2(done.pop(), window)
if res:
done.append(res)
else:
return done
return done
def _the_select(tab: Table, val_col='value'):
return select(
tab.c.turn.label('turn_from'), tab.c.tick.label('tick_from'),
func.lead(tab.c.turn).over(order_by=(tab.c.turn,
tab.c.tick)).label('turn_to'),
func.lead(tab.c.tick).over(order_by=(tab.c.turn,
tab.c.tick)).label('tick_to'),
tab.c[val_col])
def _make_graph_val_select(graph: bytes, stat: bytes, branches: List[str],
mid_turn: bool):
tab: Table = meta.tables['graph_val']
if mid_turn:
return _the_select(tab).where(
and_(tab.c.graph == graph, tab.c.key == stat,
tab.c.branch.in_(branches)))
ticksel = select(tab.c.graph, tab.c.key, tab.c.branch, tab.c.turn,
func.max(tab.c.tick).label('tick')).group_by(
tab.c.graph, tab.c.key, tab.c.branch,
tab.c.turn).where(
and_(
tab.c.graph == graph, tab.c.key == stat,
tab.c.branch.in_(branches))).subquery()
return _the_select(tab).select_from(
tab.join(
ticksel,
and_(tab.c.graph == ticksel.c.graph, tab.c.key == ticksel.c.key,
tab.c.branch == ticksel.c.branch,
tab.c.turn == ticksel.c.turn,
tab.c.tick == ticksel.c.tick)))
def _make_node_val_select(graph: bytes, node: bytes, stat: bytes,
branches: List[str], mid_turn: bool):
tab: Table = meta.tables['node_val']
if mid_turn:
return _the_select(tab).where(
and_(tab.c.graph == graph, tab.c.node == node, tab.c.key == stat,
tab.c.branch.in_(branches)))
ticksel = select(tab.c.graph, tab.c.node, tab.c.key, tab.c.branch,
tab.c.turn,
func.max(tab.c.tick).label('tick')).where(
and_(tab.c.graph == graph, tab.c.node == node,
tab.c.key == stat,
tab.c.branch.in_(branches))).group_by(
tab.c.graph, tab.c.node, tab.c.key,
tab.c.branch, tab.c.turn).subquery()
return _the_select(tab).select_from(
tab.join(
ticksel,
and_(tab.c.graph == ticksel.c.graph, tab.c.node == ticksel.c.node,
tab.c.key == ticksel.c.key,
tab.c.branch == ticksel.c.branch,
tab.c.turn == ticksel.c.turn,
tab.c.tick == ticksel.c.tick)))
def _make_location_select(graph: bytes, thing: bytes, branches: List[str],
mid_turn: bool):
tab: Table = meta.tables['things']
if mid_turn:
return _the_select(tab, val_col='location').where(
and_(tab.c.character == graph, tab.c.thing == thing,
tab.c.branch.in_(branches)))
ticksel = select(tab.c.character, tab.c.thing, tab.c.branch, tab.c.turn,
func.max(tab.c.tick).label('tick')).where(
and_(tab.c.character == graph,
tab.c.thing == thing,
tab.c.branch.in_(branches))).group_by(
tab.c.character, tab.c.thing,
tab.c.branch, tab.c.turn).subquery()
return _the_select(tab, val_col='location').select_from(
tab.join(
ticksel,
and_(tab.c.character == ticksel.c.character,
tab.c.thing == ticksel.c.thing,
tab.c.branch == ticksel.c.branch,
tab.c.turn == ticksel.c.turn,
tab.c.tick == ticksel.c.tick)))
def _make_edge_val_select(graph: bytes, orig: bytes, dest: bytes, idx: int,
stat: bytes, branches: List[str], mid_turn: bool):
tab: Table = meta.tables['edge_val']
if mid_turn:
return _the_select(tab).where(
and_(tab.c.graph == graph, tab.c.orig == orig, tab.c.dest == dest,
tab.c.idx == idx, tab.c.key == stat,
tab.c.branches.in_(branches)))
ticksel = select(
tab.c.graph, tab.c.orig, tab.c.dest, tab.c.idx, tab.c.key,
tab.c.branch, tab.c.turn,
tab.c.tick if mid_turn else func.max(tab.c.tick).label('tick')).where(
and_(tab.c.graph == graph, tab.c.orig == orig, tab.c.dest == dest,
tab.c.idx == idx, tab.c.key == stat,
tab.c.branch.in_(branches))).group_by(
tab.c.graph, tab.c.orig, tab.c.dest, tab.c.idx,
tab.c.key, tab.c.branch, tab.c.turn).subquery()
return _the_select(tab).select_from(
tab.join(
ticksel,
and_(tab.c.graph == ticksel.c.graph, tab.c.orig == ticksel.c.orig,
tab.c.dest == ticksel.c.dest, tab.c.idx == ticksel.c.idx,
tab.c.key == ticksel.c.key,
tab.c.branch == ticksel.c.branch,
tab.c.turn == ticksel.c.turn,
tab.c.tick == ticksel.c.tick)))
def _make_side_sel(entity, stat, branches: List[str], pack: callable,
mid_turn: bool):
from .character import AbstractCharacter
from .node import Place
from .node import Thing
from .portal import Portal
if isinstance(entity, AbstractCharacter):
return _make_graph_val_select(pack(entity.name), pack(stat), branches,
mid_turn)
elif isinstance(entity, Place):
return _make_node_val_select(pack(entity.character.name),
pack(entity.name), pack(stat),
branches, mid_turn)
elif isinstance(entity, Thing):
if stat == 'location':
return _make_location_select(pack(entity.character.name),
pack(entity.name), branches,
mid_turn)
else:
return _make_node_val_select(pack(entity.character.name),
pack(entity.name), pack(stat),
branches, mid_turn)
elif isinstance(entity, Portal):
return _make_edge_val_select(pack(entity.character.name),
pack(entity.origin.name),
pack(entity.destination.name), 0,
pack(stat), branches, mid_turn)
else:
raise TypeError(f"Unknown entity type {type(entity)}")
def _getcol(alias: "StatusAlias"):
from .node import Thing
if isinstance(alias.entity, Thing) and alias.stat == 'location':
return 'location'
return 'value'
[docs]
class QueryResult(Sequence, Set):
"""A slightly lazy tuple-like object holding a history query's results
Testing for membership of a turn number in a QueryResult only evaluates
the predicate for that turn number, and testing for membership of nearby
turns is fast. Accessing the start or the end of the QueryResult only
evaluates the initial or final item. Other forms of access cause the whole
query to be evaluated in parallel.
"""
def __init__(self, windows_l, windows_r, oper, end_of_time):
self._past_l = windows_l
self._future_l = []
self._past_r = windows_r
self._future_r = []
self._oper = oper
self._list = None
self._trues = set()
self._falses = set()
self._end_of_time = end_of_time
def __iter__(self):
if self._list is None:
self._generate()
return iter(self._list)
def __reversed__(self):
if self._list is None:
self._generate()
return reversed(self._list)
def __len__(self):
if not self._list:
self._generate()
return len(self._list)
def __getitem__(self, item):
if not self._list:
if item == 0:
return self._first()
elif item == -1:
return self._last()
self._generate()
return self._list[item]
def _generate(self):
raise NotImplementedError("_generate")
def _first(self):
raise NotImplementedError("_first")
def _last(self):
raise NotImplementedError("_last")
def __str__(self):
return f"<{self.__class__.__name__} containing {list(self)}>"
def __repr__(self):
return (f"<{self.__class__.__name__}({self._past_l}, {self._past_r},"
f"{self._oper}, {self._end_of_time})")
class QueryResultEndTurn(QueryResult):
def _generate(self):
spans = []
left = []
right = []
for turn_from, turn_to, l_v, r_v in _yield_intersections(
chain(iter(self._past_l), reversed(self._future_l)),
chain(iter(self._past_r), reversed(self._future_r)),
until=self._end_of_time):
spans.append((turn_from, turn_to))
left.append(l_v)
right.append(r_v)
bools = self._oper(np.array(left), np.array(right))
self._list = _list = []
append = _list.append
add = self._trues.add
for span, buul in zip(spans, bools):
if buul:
for turn in range(*span):
append(turn)
add(turn)
def __contains__(self, item):
if self._list is not None:
return item in self._trues
elif item in self._trues:
return True
elif item in self._falses:
return False
future_l = self._future_l
past_l = self._past_l
future_r = self._future_r
past_r = self._past_r
if not past_l:
if not future_l:
return False
past_l.append((future_l.pop()))
if not past_r:
if not future_r:
return False
past_r.append((future_r.pop()))
while past_l and past_l[-1][0] > item:
future_l.append(past_l.pop())
while future_l and future_l[-1][0] <= item:
past_l.append(future_l.pop())
while past_r and past_r[-1][0] > item:
future_r.append(past_r.pop())
while future_r and future_r[-1][0] <= item:
past_r.append(future_r.pop())
ret = self._oper(past_l[-1][2], past_r[-1][2])
if ret:
self._trues.add(item)
else:
self._falses.add(item)
return ret
def _last(self):
"""Get the last turn on which the predicate held true"""
past_l = self._past_l
future_l = self._future_l
while future_l:
past_l.append(future_l.pop())
past_r = self._past_r
future_r = self._future_r
while future_r:
past_r.append(future_r)
oper = self._oper
while past_l and past_r:
l_from, l_to, l_v = past_l[-1]
r_from, r_to, r_v = past_r[-1]
inter = intersect2((l_from, l_to), (r_from, r_to))
if not inter:
if l_from < r_from:
future_r.append(past_r.pop())
else:
future_l.append(past_l.pop())
continue
if oper(l_v, r_v):
# SQL results are exclusive on the right
if inter[1] is None:
return self._end_of_time - 1
return inter[1] - 1
def _first(self):
"""Get the first turn on which the predicate held true"""
if self._list is not None:
if not self._list:
return
return self._list[0]
oper = self._oper
for turn_from, turn_to, l_v, r_v in _yield_intersections(
chain(iter(self._past_l), reversed(self._future_l)),
chain(iter(self._past_r), reversed(self._future_r)),
until=self._end_of_time):
if oper(l_v, r_v):
return turn_from
def _yield_intersections(iter_l, iter_r, until=None):
try:
l_from, l_to, l_v = next(iter_l)
except StopIteration:
return
try:
r_from, r_to, r_v = next(iter_r)
except StopIteration:
return
while True:
if l_to in (None, (None, None)):
l_to = until
if r_to in (None, (None, None)):
r_to = until
intersection = intersect2((l_from, l_to), (r_from, r_to))
if intersection and intersection[0] != intersection[1]:
yield intersection + (l_v, r_v)
if intersection[1] is None or (isinstance(intersection[1], tuple)
and intersection[1] is None):
return
if l_to is None or r_to is None or (isinstance(
l_to, tuple) and l_to[1] is None) or (isinstance(r_to, tuple)
and r_to[1] is None):
break
elif l_to <= r_to:
try:
l_from, l_to, l_v = next(iter_l)
except StopIteration:
break
else:
try:
r_from, r_to, r_v = next(iter_r)
except StopIteration:
break
if l_to is None:
while True:
try:
r_from, r_to, r_v = next(iter_r)
except StopIteration:
if until:
yield intersect2((l_from, l_to),
(r_to, until)) + (l_v, r_v)
return
yield intersect2((l_from, l_to), (r_from, r_to)) + (l_v, r_v)
if r_to is None:
while True:
try:
l_from, l_to, l_v = next(iter_l)
except StopIteration:
if until:
yield intersect2((l_to, until),
(r_from, r_to)) + (l_v, r_v)
return
yield intersect2((l_from, l_to), (r_from, r_to)) + (l_v, r_v)
class QueryResultMidTurn(QueryResult):
def _generate(self):
spans = []
left = []
right = []
for time_from, time_to, l_v, r_v in _yield_intersections(
chain(iter(self._past_l), reversed(self._future_l)),
chain(iter(self._past_r), reversed(self._future_r)),
until=(self._end_of_time, 0)):
spans.append((time_from, time_to))
left.append(l_v)
right.append(r_v)
bools = self._oper(np.array(left), np.array(right))
trues = self._trues
_list = self._list = []
for span, buul in zip(spans, bools):
if buul:
for turn in range(span[0][0],
span[1][0] + (1 if span[1][1] else 0)):
if turn in trues:
continue
trues.add(turn)
_list.append(turn)
def __contains__(self, item):
if self._list is not None:
return item in self._trues
if item in self._trues:
return True
if item in self._falses:
return False
future_l = self._future_l
past_l = self._past_l
future_r = self._future_r
past_r = self._past_r
if not past_l:
if not future_l:
return False
past_l.append(future_l.pop())
if not past_r:
if not future_r:
return False
past_r.append(future_r.pop())
while past_l and past_l[-1][0][0] >= item:
future_l.append(past_l.pop())
while future_l and not (past_l and past_l[-1][0][0] <= item and
(past_l[-1][1][0] is None
or item <= past_l[-1][1][0])):
past_l.append(future_l.pop())
left_candidates = [past_l[-1]]
while future_l and future_l[-1][0][0] <= item and (
future_l[-1][1][0] is None or item <= future_l[-1][1][0]):
past_l.append(future_l.pop())
left_candidates.append(past_l[-1])
while past_r and past_r[-1][0][0] >= item:
future_r.append(past_r.pop())
while future_r and not (past_r and
past_r[-1][0][0] <= item <= past_r[-1][1][0]):
past_r.append(future_r.pop())
right_candidates = [past_r[-1]]
while future_r and future_r[-1][0][0] <= item and (
future_r[-1][1][0] is None or item <= future_r[-1][1][0]):
past_r.append(future_r.pop())
right_candidates.append(past_r[-1])
oper = self._oper
while left_candidates and right_candidates:
if intersect2(left_candidates[-1][:2], right_candidates[-1][:2]):
if oper(left_candidates[-1][2], right_candidates[-1][2]):
return True
if left_candidates[-1][0] < right_candidates[-1][0]:
right_candidates.pop()
else:
left_candidates.pop()
return False
def _last(self):
"""Get the last turn on which the predicate held true"""
past_l = self._past_l
future_l = self._future_l
while future_l:
past_l.append(future_l.pop())
past_r = self._past_r
future_r = self._future_r
while future_r:
past_r.append(future_r)
oper = self._oper
while past_l and past_r:
l_from, l_to, l_v = past_l[-1]
r_from, r_to, r_v = past_r[-1]
inter = intersect2((l_from, l_to), (r_from, r_to))
if not inter:
if l_from < r_from:
future_r.append(past_r.pop())
else:
future_l.append(past_l.pop())
continue
if oper(l_v, r_v):
if inter[1] == (None, None):
return self._end_of_time - 1
return inter[1][0] - (0 if inter[1][1] else 1)
def _first(self):
"""Get the first turn on which the predicate held true"""
oper = self._oper
for time_from, time_to, l_v, r_v in _yield_intersections(
chain(iter(self._past_l), reversed(self._future_l)),
chain(iter(self._past_r), reversed(self._future_r)),
until=(self._end_of_time, 0)):
if oper(l_v, r_v):
return time_from[0]
class CombinedQueryResult(QueryResult):
def __init__(self, left: QueryResult, right: QueryResult, oper):
self._left = left
self._right = right
self._oper = oper
def _genset(self):
if not hasattr(self, '_set'):
self._set = self._oper(set(self._left), set(self._right))
def __iter__(self):
self._genset()
return iter(self._set)
def __len__(self):
self._genset()
return len(self._set)
def __contains__(self, item):
if hasattr(self, '_set'):
return item in self._set
return self._oper(item in self._left, item in self._right)
class Query(object):
oper: Callable[[Any, Any], Any] = lambda x, y: NotImplemented
def __new__(cls, engine, leftside, rightside=None, **kwargs):
if rightside is None:
if not isinstance(leftside, cls):
raise TypeError("You can't make a query with only one side")
me = leftside
else:
me = super().__new__(cls)
me.leftside = leftside
me.rightside = rightside
me.engine = engine
return me
def _iter_times(self):
raise NotImplementedError
def _iter_ticks(self, turn):
raise NotImplementedError
def _iter_btts(self):
raise NotImplementedError
def __eq__(self, other):
return EqQuery(self.engine, self, other)
def __gt__(self, other):
return GtQuery(self.engine, self, other)
def __ge__(self, other):
return GeQuery(self.engine, self, other)
def __lt__(self, other):
return LtQuery(self.engine, self, other)
def __le__(self, other):
return LeQuery(self.engine, self, other)
def __ne__(self, other):
return NeQuery(self.engine, self, other)
class ComparisonQuery(Query):
oper: Callable[[Any, Any], bool] = lambda x, y: NotImplemented
def _iter_times(self):
return slow_iter_turns_eval_cmp(self, self.oper, engine=self.engine)
def _iter_btts(self):
return slow_iter_btts_eval_cmp(self, self.oper, engine=self.engine)
def __and__(self, other):
return IntersectionQuery(self.engine, self, other)
def __or__(self, other):
return UnionQuery(self.engine, self, other)
def __sub__(self, other):
return MinusQuery(self.engine, self, other)
class EqQuery(ComparisonQuery):
oper = eq
class NeQuery(ComparisonQuery):
oper = ne
class GtQuery(ComparisonQuery):
oper = gt
class LtQuery(ComparisonQuery):
oper = lt
class GeQuery(ComparisonQuery):
oper = ge
class LeQuery(ComparisonQuery):
oper = le
class CompoundQuery(Query):
oper: Callable[[Any, Any], set] = lambda x, y: NotImplemented
class UnionQuery(CompoundQuery):
oper = operator.or_
class IntersectionQuery(CompoundQuery):
oper = operator.and_
class MinusQuery(CompoundQuery):
oper = operator.sub
comparisons = {
'eq': EqQuery,
'ne': NeQuery,
'gt': GtQuery,
'lt': LtQuery,
'ge': GeQuery,
'le': LeQuery
}
class StatusAlias(EntityStatAccessor):
def __eq__(self, other):
return EqQuery(self.engine, self, other)
def __ne__(self, other):
return NeQuery(self.engine, self, other)
def __gt__(self, other):
return GtQuery(self.engine, self, other)
def __lt__(self, other):
return LtQuery(self.engine, self, other)
def __ge__(self, other):
return GeQuery(self.engine, self, other)
def __le__(self, other):
return LeQuery(self.engine, self, other)
def _mungeside(side):
if isinstance(side, Query):
return side._iter_times
elif isinstance(side, StatusAlias):
return EntityStatAccessor(side.entity, side.stat, side.engine,
side.branch, side.turn, side.tick,
side.current, side.mungers)
elif isinstance(side, EntityStatAccessor):
return side
else:
return lambda: side
def slow_iter_turns_eval_cmp(qry, oper, start_branch=None, engine=None):
"""Iterate over all turns on which a comparison holds.
This is expensive. It evaluates the query for every turn in history.
"""
leftside = _mungeside(qry.leftside)
rightside = _mungeside(qry.rightside)
engine = engine or leftside.engine or rightside.engine
for (branch, fork_turn,
fork_tick) in engine._iter_parent_btt(start_branch
or engine.branch):
if branch is None:
return
parent, turn_start, tick_start, turn_end, tick_end = engine._branches[
branch]
for turn in range(turn_start, fork_turn + 1):
if oper(leftside(branch, turn), rightside(branch, turn)):
yield branch, turn
def slow_iter_btts_eval_cmp(qry, oper, start_branch=None, engine=None):
leftside = _mungeside(qry.leftside)
rightside = _mungeside(qry.rightside)
engine = engine or leftside.engine or rightside.engine
assert engine is not None
for (branch, fork_turn,
fork_tick) in engine._iter_parent_btt(start_branch
or engine.branch):
if branch is None:
return
parent, turn_start, tick_start, turn_end, tick_end = engine._branches[
branch]
for turn in range(turn_start, fork_turn + 1):
if turn == fork_turn:
local_turn_end = fork_tick
else:
local_turn_end = engine._turn_end_plan[branch, turn]
for tick in range(0, local_turn_end + 1):
try:
val = oper(leftside(branch, turn, tick),
rightside(branch, turn, tick))
except KeyError:
continue
if val:
yield branch, turn, tick
class ConnectionHolder(query.ConnectionHolder):
def gather(self, meta):
return gather_sql(meta)
def initdb(self):
"""Set up the database schema, both for allegedb and the special
extensions for LiSE
"""
super().initdb()
init_table = self.init_table
for table in ('universals', 'rules', 'rulebooks', 'things',
'character_rulebook', 'unit_rulebook',
'character_thing_rulebook', 'character_place_rulebook',
'character_portal_rulebook', 'node_rulebook',
'portal_rulebook', 'units', 'character_rules_handled',
'unit_rules_handled', 'character_thing_rules_handled',
'character_place_rules_handled',
'character_portal_rules_handled', 'node_rules_handled',
'portal_rules_handled', 'rule_triggers',
'rule_prereqs', 'rule_actions', 'turns_completed'):
try:
init_table(table)
except OperationalError:
pass
except Exception as ex:
return ex
class QueryEngine(query.QueryEngine):
exist_edge_t = 0
path = LiSE.__path__[0]
IntegrityError = IntegrityError
OperationalError = OperationalError
holder_cls = ConnectionHolder
tables = ('global', 'branches', 'turns', 'graphs', 'keyframes',
'graph_val', 'nodes', 'node_val', 'edges', 'edge_val', 'plans',
'plan_ticks', 'universals', 'rules', 'rulebooks',
'rule_triggers', 'rule_prereqs', 'rule_actions',
'character_rulebook', 'unit_rulebook',
'character_thing_rulebook', 'character_place_rulebook',
'character_portal_rulebook', 'node_rules_handled',
'portal_rules_handled', 'things', 'node_rulebook',
'portal_rulebook', 'units', 'character_rules_handled',
'unit_rules_handled', 'character_thing_rules_handled',
'character_place_rules_handled',
'character_portal_rules_handled', 'turns_completed')
def __init__(self, dbstring, connect_args, pack=None, unpack=None):
super().__init__(dbstring,
connect_args,
pack,
unpack,
gather=gather_sql)
self._char_rules_handled = []
self._unit_rules_handled = []
self._char_thing_rules_handled = []
self._char_place_rules_handled = []
self._char_portal_rules_handled = []
self._node_rules_handled = []
self._portal_rules_handled = []
self._unitness = []
self._location = []
def flush(self):
super().flush()
put = self._inq.put
if self._unitness:
put(('silent', 'many', 'del_units_after',
[(character, graph, node, branch, turn, turn, tick)
for (character, graph, node, branch, turn, tick,
_) in self._unitness]))
put(('silent', 'many', 'units_insert', self._unitness))
self._unitness = []
if self._location:
put(('silent', 'many', 'del_things_after', [
(character, thing, branch, turn, turn, tick)
for (character, thing, branch, turn, tick, _) in self._location
]))
put(('silent', 'many', 'things_insert', self._location))
self._location = []
for (attr, cmd) in [
('_char_rules_handled', 'character_rules_handled_insert'),
('_unit_rules_handled', 'unit_rules_handled_insert'),
('_char_thing_rules_handled',
'character_thing_rules_handled_insert'),
('_char_place_rules_handled',
'character_place_rules_handled_insert'),
('_char_portal_rules_handled',
'character_portal_rules_handled_insert'),
('_node_rules_handled', '_node_rules_handled_insert'),
('_portal_rules_handled', 'portal_rules_handled_insert')
]:
if getattr(self, attr):
put(('silent', 'many', cmd, getattr(self, attr)))
setattr(self, attr, [])
assert self.echo('flushed') == 'flushed'
def universals_dump(self):
unpack = self.unpack
for key, branch, turn, tick, value in self.call_one('universals_dump'):
yield unpack(key), branch, turn, tick, unpack(value)
def rulebooks_dump(self):
unpack = self.unpack
for rulebook, branch, turn, tick, rules, prio in self.call_one(
'rulebooks_dump'):
yield unpack(rulebook), branch, turn, tick, (unpack(rules), prio)
def _rule_dump(self, typ):
unpack = self.unpack
for rule, branch, turn, tick, lst in self.call_one(
'rule_{}_dump'.format(typ)):
yield rule, branch, turn, tick, unpack(lst)
def rule_triggers_dump(self):
return self._rule_dump('triggers')
def rule_prereqs_dump(self):
return self._rule_dump('prereqs')
def rule_actions_dump(self):
return self._rule_dump('actions')
characters = characters_dump = query.QueryEngine.graphs_dump
def node_rulebook_dump(self):
unpack = self.unpack
for character, node, branch, turn, tick, rulebook in self.call_one(
'node_rulebook_dump'):
yield unpack(character), unpack(node), branch, turn, tick, unpack(
rulebook)
def portal_rulebook_dump(self):
unpack = self.unpack
for character, orig, dest, branch, turn, tick, rulebook in self.call_one(
'portal_rulebook_dump'):
yield (unpack(character), unpack(orig), unpack(dest), branch, turn,
tick, unpack(rulebook))
def _charactery_rulebook_dump(self, qry):
unpack = self.unpack
for character, branch, turn, tick, rulebook in self.call_one(
qry + '_rulebook_dump'):
yield unpack(character), branch, turn, tick, unpack(rulebook)
character_rulebook_dump = partialmethod(_charactery_rulebook_dump,
'character')
unit_rulebook_dump = partialmethod(_charactery_rulebook_dump, 'unit')
character_thing_rulebook_dump = partialmethod(_charactery_rulebook_dump,
'character_thing')
character_place_rulebook_dump = partialmethod(_charactery_rulebook_dump,
'character_place')
character_portal_rulebook_dump = partialmethod(_charactery_rulebook_dump,
'character_portal')
def character_rules_handled_dump(self):
unpack = self.unpack
for character, rulebook, rule, branch, turn, tick in self.call_one(
'character_rules_handled_dump'):
yield unpack(character), unpack(rulebook), rule, branch, turn, tick
def character_rules_changes_dump(self):
unpack = self.unpack
for (character, rulebook, rule, branch, turn, tick, handled_branch,
handled_turn) in self.call_one('character_rules_changes_dump'):
yield (unpack(character), unpack(rulebook), rule, branch, turn,
tick, handled_branch, handled_turn)
def unit_rules_handled_dump(self):
unpack = self.unpack
for character, graph, unit, rulebook, rule, branch, turn, tick in self.call_one(
'unit_rules_handled_dump'):
yield (unpack(character), unpack(graph), unpack(unit),
unpack(rulebook), rule, branch, turn, tick)
def unit_rules_changes_dump(self):
jl = self.unpack
for (character, rulebook, rule, graph, unit, branch, turn, tick,
handled_branch,
handled_turn) in self.call_one('unit_rules_changes_dump'):
yield (jl(character), jl(rulebook), rule, jl(graph), jl(unit),
branch, turn, tick, handled_branch, handled_turn)
def character_thing_rules_handled_dump(self):
unpack = self.unpack
for character, thing, rulebook, rule, branch, turn, tick in self.call_one(
'character_thing_rules_handled_dump'):
yield unpack(character), unpack(thing), unpack(
rulebook), rule, branch, turn, tick
def character_thing_rules_changes_dump(self):
jl = self.unpack
for (character, thing, rulebook, rule, branch, turn, tick,
handled_branch, handled_turn
) in self.call_one('character_thing_rules_changes_dump'):
yield (jl(character), jl(thing), jl(rulebook), rule, branch, turn,
tick, handled_branch, handled_turn)
def character_place_rules_handled_dump(self):
unpack = self.unpack
for character, place, rulebook, rule, branch, turn, tick in self.call_one(
'character_place_rules_handled_dump'):
yield unpack(character), unpack(place), unpack(
rulebook), rule, branch, turn, tick
def character_place_rules_changes_dump(self):
jl = self.unpack
for (character, rulebook, rule, place, branch, turn, tick,
handled_branch, handled_turn
) in self.call_one('character_place_rules_changes_dump'):
yield (jl(character), jl(rulebook), rule, jl(place), branch, turn,
tick, handled_branch, handled_turn)
def character_portal_rules_handled_dump(self):
unpack = self.unpack
for character, rulebook, rule, orig, dest, branch, turn, tick in self.call_one(
'character_portal_rules_handled_dump'):
yield (unpack(character), unpack(rulebook), unpack(orig),
unpack(dest), rule, branch, turn, tick)
def character_portal_rules_changes_dump(self):
jl = self.unpack
for (character, rulebook, rule, orig, dest, branch, turn, tick,
handled_branch, handled_turn
) in self.call_one('character_portal_rules_changes_dump'):
yield (jl(character), jl(rulebook), rule, jl(orig), jl(dest),
branch, turn, tick, handled_branch, handled_turn)
def node_rules_handled_dump(self):
for character, node, rulebook, rule, branch, turn, tick in self.call_one(
'node_rules_handled_dump'):
yield self.unpack(character), self.unpack(node), self.unpack(
rulebook), rule, branch, turn, tick
def node_rules_changes_dump(self):
jl = self.unpack
for (character, node, rulebook, rule, branch, turn, tick,
handled_branch,
handled_turn) in self.call_one('node_rules_changes_dump'):
yield (jl(character), jl(node), jl(rulebook), rule, branch, turn,
tick, handled_branch, handled_turn)
def portal_rules_handled_dump(self):
unpack = self.unpack
for character, orig, dest, rulebook, rule, branch, turn, tick in self.call_one(
'portal_rules_handled_dump'):
yield (unpack(character), unpack(orig), unpack(dest),
unpack(rulebook), rule, branch, turn, tick)
def portal_rules_changes_dump(self):
jl = self.unpack
for (character, orig, dest, rulebook, rule, branch, turn, tick,
handled_branch,
handled_turn) in self.call_one('portal_rules_changes_dump'):
yield (jl(character), jl(orig), jl(dest), jl(rulebook), rule,
branch, turn, tick, handled_branch, handled_turn)
def senses_dump(self):
unpack = self.unpack
for character, sense, branch, turn, tick, function in self.call_one(
'senses_dump'):
yield unpack(character), sense, branch, turn, tick, function
def things_dump(self):
unpack = self.unpack
for character, thing, branch, turn, tick, location in self.call_one(
'things_dump'):
yield (unpack(character), unpack(thing), branch, turn, tick,
unpack(location))
def load_things(self,
character,
branch,
turn_from,
tick_from,
turn_to=None,
tick_to=None):
pack = self.pack
unpack = self.unpack
if turn_to is None:
if tick_to is not None:
raise ValueError("Need both or neither of turn_to, tick_to")
for thing, turn, tick, location in self.call_one(
'load_things_tick_to_end', pack(character), branch, turn_from,
turn_from, tick_from):
yield character, unpack(thing), branch, turn, tick, unpack(
location)
else:
if tick_to is None:
raise ValueError("Need both or neither of turn_to, tick_to")
for thing, turn, tick, location in self.call_one(
'load_things_tick_to_tick', pack(character), branch, turn_from,
turn_from, tick_from, turn_to, turn_to, tick_to):
yield character, unpack(thing), branch, turn, tick, unpack(
location)
def units_dump(self):
unpack = self.unpack
for character_graph, unit_graph, unit_node, branch, turn, tick, is_av in self.call_one(
'units_dump'):
yield (unpack(character_graph), unpack(unit_graph),
unpack(unit_node), branch, turn, tick, is_av)
def universal_set(self, key, branch, turn, tick, val):
key, val = map(self.pack, (key, val))
self.call_one('universals_insert', key, branch, turn, tick, val)
def universal_del(self, key, branch, turn, tick):
key = self.pack(key)
self.call_one('universals_insert', key, branch, turn, tick, None)
def comparison(self,
entity0,
stat0,
entity1,
stat1=None,
oper='eq',
windows: list = None):
if windows is None:
windows = []
stat1 = stat1 or stat0
return comparisons[oper](leftside=entity0.status(stat0),
rightside=entity1.status(stat1),
windows=windows)
def count_all_table(self, tbl):
return self.call_one('{}_count'.format(tbl)).fetchone()[0]
def rules_dump(self):
for (name, ) in self.call_one('rules_dump'):
yield name
def _set_rule_something(self, what, rule, branch, turn, tick, flist):
flist = self.pack(flist)
return self.call_one('rule_{}_insert'.format(what), rule, branch, turn,
tick, flist)
set_rule_triggers = partialmethod(_set_rule_something, 'triggers')
set_rule_prereqs = partialmethod(_set_rule_something, 'prereqs')
set_rule_actions = partialmethod(_set_rule_something, 'actions')
def set_rule(self,
rule,
branch,
turn,
tick,
triggers=None,
prereqs=None,
actions=None):
try:
self.call_one('rules_insert', rule)
except IntegrityError:
pass
self.set_rule_triggers(rule, branch, turn, tick, triggers or [])
self.set_rule_prereqs(rule, branch, turn, tick, prereqs or [])
self.set_rule_actions(rule, branch, turn, tick, actions or [])
def set_rulebook(self, name, branch, turn, tick, rules=None, prio=0.0):
name, rules = map(self.pack, (name, rules or []))
self.call_one('rulebooks_insert', name, branch, turn, tick, rules,
float(prio))
def _set_rulebook_on_character(self, rbtyp, char, branch, turn, tick, rb):
char, rb = map(self.pack, (char, rb))
self.call_one(rbtyp + '_rulebook_insert', char, branch, turn, tick, rb)
set_character_rulebook = partialmethod(_set_rulebook_on_character,
'character')
set_unit_rulebook = partialmethod(_set_rulebook_on_character, 'unit')
set_character_thing_rulebook = partialmethod(_set_rulebook_on_character,
'character_thing')
set_character_place_rulebook = partialmethod(_set_rulebook_on_character,
'character_place')
set_character_portal_rulebook = partialmethod(_set_rulebook_on_character,
'character_portal')
def rulebooks(self):
for book in self.call_one('rulebooks'):
yield self.unpack(book)
def exist_node(self, character, node, branch, turn, tick, extant):
super().exist_node(character, node, branch, turn, tick, extant)
def exist_edge(self,
character,
orig,
dest,
idx,
branch,
turn,
tick,
extant=None):
start = monotonic()
if extant is None:
branch, turn, tick, extant = idx, branch, turn, tick
idx = 0
super().exist_edge(character, orig, dest, idx, branch, turn, tick,
extant)
QueryEngine.exist_edge_t += monotonic() - start
def set_node_rulebook(self, character, node, branch, turn, tick, rulebook):
(character, node, rulebook) = map(self.pack,
(character, node, rulebook))
return self.call_one('node_rulebook_insert', character, node, branch,
turn, tick, rulebook)
def set_portal_rulebook(self, character, orig, dest, branch, turn, tick,
rulebook):
(character, orig, dest,
rulebook) = map(self.pack, (character, orig, dest, rulebook))
return self.call_one('portal_rulebook_insert', character, orig, dest,
branch, turn, tick, rulebook)
def handled_character_rule(self, character, rulebook, rule, branch, turn,
tick):
(character, rulebook) = map(self.pack, (character, rulebook))
self._char_rules_handled.append(
(character, rulebook, rule, branch, turn, tick))
def _flush_char_rules_handled(self):
if not self._char_rules_handled:
return
self.call_many('character_rules_handled_insert',
self._char_rules_handled)
self._char_rules_handled = []
def handled_unit_rule(self, character, rulebook, rule, graph, unit, branch,
turn, tick):
character, graph, unit, rulebook = map(
self.pack, (character, graph, unit, rulebook))
self._unit_rules_handled.append(
(character, graph, unit, rulebook, rule, branch, turn, tick))
def _flush_unit_rules_handled(self):
if not self._unit_rules_handled:
return
self.call_many('unit_rules_handled_insert', self._unit_rules_handled)
self._unit_rules_handled = []
def handled_character_thing_rule(self, character, rulebook, rule, thing,
branch, turn, tick):
character, thing, rulebook = map(self.pack,
(character, thing, rulebook))
self._char_thing_rules_handled.append(
(character, thing, rulebook, rule, branch, turn, tick))
def _flush_char_thing_rules_handled(self):
if not self._char_thing_rules_handled:
return
self.call_many('character_thing_rules_handled_insert',
self._char_thing_rules_handled)
self._char_thing_rules_handled = []
def handled_character_place_rule(self, character, rulebook, rule, place,
branch, turn, tick):
character, rulebook, place = map(self.pack,
(character, rulebook, place))
self._char_place_rules_handled.append(
(character, place, rulebook, rule, branch, turn, tick))
def _flush_char_place_rules_handled(self):
if not self._char_place_rules_handled:
return
self.call_many('character_place_rules_handled_insert',
self._char_place_rules_handled)
self._char_place_rules_handled = []
def handled_character_portal_rule(self, character, rulebook, rule, orig,
dest, branch, turn, tick):
character, rulebook, orig, dest = map(
self.pack, (character, rulebook, orig, dest))
self._char_portal_rules_handled.append(
(character, orig, dest, rulebook, rule, branch, turn, tick))
def _flush_char_portal_rules_handled(self):
if not self._char_portal_rules_handled:
return
self.call_many('character_portal_rules_handled_insert',
self._char_portal_rules_handled)
self._char_portal_rules_handled = []
def handled_node_rule(self, character, node, rulebook, rule, branch, turn,
tick):
(character, node, rulebook) = map(self.pack,
(character, node, rulebook))
self._node_rules_handled.append(
(character, node, rulebook, rule, branch, turn, tick))
def _flush_node_rules_handled(self):
if not self._node_rules_handled:
return
self.call_many('node_rules_handled_insert', self._node_rules_handled)
self._node_rules_handled = []
def handled_portal_rule(self, character, orig, dest, rulebook, rule,
branch, turn, tick):
(character, orig, dest,
rulebook) = map(self.pack, (character, orig, dest, rulebook))
self._portal_rules_handled.append(
(character, orig, dest, rulebook, rule, branch, turn, tick))
def _flush_portal_rules_handled(self):
if not self._portal_rules_handled:
return
self.call_many('portal_rules_handled_insert',
self._portal_rules_handled)
self._portal_rules_handled = []
def get_rulebook_char(self, rulemap, character):
character = self.pack(character)
for (book, ) in self.call_one('rulebook_get_{}'.format(rulemap),
character):
return self.unpack(book)
raise KeyError("No rulebook")
def set_thing_loc(self, character, thing, branch, turn, tick, loc):
(character, thing) = map(self.pack, (character, thing))
loc = self.pack(loc)
self._location.append((character, thing, branch, turn, tick, loc))
def unit_set(self, character, graph, node, branch, turn, tick, isav):
(character, graph, node) = map(self.pack, (character, graph, node))
self._unitness.append(
(character, graph, node, branch, turn, tick, isav))
def rulebooks_rules(self):
for (rulebook, rule) in self.call_one('rulebooks_rules'):
yield map(self.unpack, (rulebook, rule))
def rulebook_get(self, rulebook, idx):
return self.unpack(
self.call_one('rulebook_get', self.pack(rulebook),
idx).fetchone()[0])
def rulebook_set(self, rulebook, branch, turn, tick, rules):
# what if the rulebook has other values set afterward? wipe them out, right?
# should that happen in the query engine or elsewhere?
rulebook, rules = map(self.pack, (rulebook, rules))
try:
self.call_one('rulebooks_insert', rulebook, branch, turn, tick,
rules)
except IntegrityError:
self.call_one('rulebooks_update', rules, rulebook, branch, turn,
tick)
def rulebook_del_time(self, branch, turn, tick):
self.call_one('rulebooks_del_time', branch, turn, tick)
def branch_descendants(self, branch):
for child in self.call_one('branch_children', branch):
yield child
yield from self.branch_descendants(child)
def turns_completed_dump(self):
return self.call_one('turns_completed_dump')
def complete_turn(self, branch, turn, discard_rules=False):
try:
self.call_one('turns_completed_insert', branch, turn)
except IntegrityError:
self.call_one('turns_completed_update', turn, branch)
if discard_rules:
self._char_rules_handled = []
self._unit_rules_handled = []
self._char_thing_rules_handled = []
self._char_place_rules_handled = []
self._char_portal_rules_handled = []
self._node_rules_handled = []
self._portal_rules_handled = []
class QueryEngineProxy:
def __init__(self,
dbstring,
connect_args,
alchemy,
pack=None,
unpack=None):
self._inq = Queue()
self._outq = Queue()
self._dbstring = dbstring
self._connect_args = connect_args
self._alchemy = alchemy
self._pack = pack
self._unpack = unpack
self.globl = self.GlobalsProxy(self._inq, self._outq)
self._thread = Thread(target=self._subthread, daemon=True)
self._thread.start()
def _subthread(self):
real = QueryEngine(self._dbstring,
self._connect_args,
self._alchemy,
pack=self._pack,
unpack=self._unpack)
while True:
func, args, kwargs = self._inq.get()
if func == 'get_global':
if args not in real.globl:
output = KeyError()
else:
output = real.globl[args]
elif func == 'set_global':
k, v = args
real.globl[k] = v
continue
elif func == 'list_globals':
output = list(real.globl)
elif func == 'len_globals':
output = len(real.globl)
elif func == 'del_global':
if args in real.globl:
del real.globl[args]
output = None
else:
output = KeyError()
else:
try:
output = getattr(real, func)(*args, **kwargs)
except Exception as ex:
output = ex
if hasattr(output, '__next__'):
output = list(output)
self._outq.put(output)
if func == 'close':
return
class Caller:
def __init__(self, func, inq, outq):
self._func = func
self._inq = inq
self._outq = outq
def __call__(self, *args, **kwargs):
self._inq.put((self._func, args, kwargs))
ret = self._outq.get()
if isinstance(ret, Exception):
raise ret
return ret
class GlobalsProxy(MutableMapping):
def __init__(self, inq, outq):
self._inq = inq
self._outq = outq
def __iter__(self):
self._inq.put(('list_globals', None, None))
return iter(self._outq.get())
def __len__(self):
self._inq.put(('len_globals', None, None))
return self._outq.get()
def __getitem__(self, item):
self._inq.put(('get_global', item, None))
ret = self._outq.get()
if isinstance(ret, Exception):
raise ret
return ret
def __setitem__(self, key, value):
self._inq.put(('set_global', (key, value), None))
def __delitem__(self, key):
self._inq.put(('del_global', key, None))
ret = self._outq.get()
if isinstance(ret, Exception):
raise ret
def __getattr__(self, item):
if hasattr(QueryEngine, item) and callable(getattr(QueryEngine, item)):
return self.Caller(item, self._inq, self._outq)