# 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, 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 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/>.
"""The query engine provides Pythonic methods to access the database.
This module also contains a notably unfinished implementation of a query
language specific to LiSE. Access some stats using entities' method
``historical``, and do comparisons on those, and instead of a boolean
result you'll get a callable object that will return an iterator over
turn numbers in which the comparison evaluated to ``True``.
"""
from operator import gt, lt, eq, ne, le, ge
from functools import partialmethod
import allegedb.query
from .exc import (
IntegrityError,
OperationalError
)
from .util import EntityStatAccessor
import LiSE
[docs]def windows_union(windows):
"""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
[docs]def windows_intersection(windows):
"""Given a list of (beginning, ending), return another describing where they overlap.
:rtype: list
"""
def intersect2(left, right):
if left == right:
return left
elif left is (None, None):
return right
elif right is (None, None):
return left
elif left[0] is None:
if right[0] is None:
return None, min((left[1], right[1]))
elif right[1] is 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:
if right[0] is None:
return left[0], right[1]
else:
return right # assumes left[0] <= right[0]
# None not in left
elif right[0] is None:
return left[0], min((left[1], right[1]))
elif right[1] is None:
if left[1] >= right[0]:
return right[0], left[1]
else:
return None
assert None not in left and None not in right and left[0] < right[1]
if left[1] >= right[0]:
if right[1] > left[1]:
return right[0], left[1]
else:
return right
return None
if len(windows) == 1:
return windows
left_none = []
right_none = []
otherwise = []
for window in windows:
assert window is not None, None
if window[0] is None:
left_none.append(window)
elif window[1] is None:
right_none.append(window)
else:
otherwise.append(window)
done = []
todo = left_none + sorted(otherwise)
for window in todo:
if not done:
done.append(window)
continue
res = intersect2(done.pop(), window)
if res:
done.append(res)
return done
class Query(object):
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
me.windows = kwargs.get('windows', [])
return me
def iter_turns(self):
raise NotImplementedError
def iter_ticks(self, turn):
raise NotImplementedError
def __eq__(self, other):
return EqQuery(self.engine, self, self.engine.entityfy(other))
def __gt__(self, other):
return GtQuery(self.engine, self, self.engine.entityfy(other))
def __ge__(self, other):
return GeQuery(self.engine, self, self.engine.entityfy(other))
def __lt__(self, other):
return LtQuery(self.engine, self, self.engine.entityfy(other))
def __le__(self, other):
return LeQuery(self.engine, self, self.engine.entityfy(other))
def __ne__(self, other):
return NeQuery(self.engine, self, self.engine.entityfy(other))
def and_before(self, end):
if self.windows:
new_windows = windows_intersection(
sorted(self.windows + [(None, end)])
)
else:
new_windows = [(0, end)]
return type(self)(self.leftside, self.rightside, windows=new_windows)
before = and_before
def or_before(self, end):
if self.windows:
new_windows = windows_union(self.windows + [(None, end)])
else:
new_windows = [(None, end)]
return type(self)(self.leftside, self.rightside, windows=new_windows)
def and_after(self, start):
if self.windows:
new_windows = windows_intersection(self.windows + [(start, None)])
else:
new_windows = [(start, None)]
return type(self)(self.leftside, self.rightside, windows=new_windows)
after = and_after
def or_between(self, start, end):
if self.windows:
new_windows = windows_union(self.windows + [(start, end)])
else:
new_windows = [(start, end)]
return type(self)(self.leftside, self.rightside, windows=new_windows)
def and_between(self, start, end):
if self.windows:
new_windows = windows_intersection(self.windows + [(start, end)])
else:
new_windows = [(start, end)]
return type(self)(self.leftside, self.rightside, windows=new_windows)
between = and_between
def or_during(self, tick):
return self.or_between(tick, tick)
def and_during(self, tick):
return self.and_between(tick, tick)
during = and_during
class Union(Query):
pass
class ComparisonQuery(Query):
oper = lambda x, y: NotImplemented
def iter_turns(self):
return slow_iter_turns_eval_cmp(self, self.oper, engine=self.engine)
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
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)
[docs]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.
"""
def mungeside(side):
if isinstance(side, Query):
return side.iter_turns
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
leftside = mungeside(qry.leftside)
rightside = mungeside(qry.rightside)
engine = engine or leftside.engine or rightside.engine
for (branch, _, _) 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, engine.turn + 1):
if oper(leftside(branch, turn), rightside(branch, turn)):
yield branch, turn
[docs]class QueryEngine(allegedb.query.QueryEngine):
path = LiSE.__path__[0]
IntegrityError = IntegrityError
OperationalError = OperationalError
def universals_dump(self):
for key, branch, turn, tick, value in self.sql('universals_dump'):
yield self.unpack(key), branch, turn, tick, self.unpack(value)
def rulebooks_dump(self):
for rulebook, branch, turn, tick, rules in self.sql('rulebooks_dump'):
yield self.unpack(rulebook), branch, turn, tick, self.unpack(rules)
def _rule_dump(self, typ):
for rule, branch, turn, tick, lst in self.sql('rule_{}_dump'.format(typ)):
yield rule, branch, turn, tick, self.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')
def characters_dump(self):
for graph, typ in self.sql('graphs_dump'):
if typ == 'DiGraph':
yield self.unpack(graph)
characters = characters_dump
def node_rulebook_dump(self):
for character, node, branch, turn, tick, rulebook in self.sql('node_rulebook_dump'):
yield self.unpack(character), self.unpack(node), branch, turn, tick, self.unpack(rulebook)
def portal_rulebook_dump(self):
for character, orig, dest, branch, turn, tick, rulebook in self.sql('portal_rulebook_dump'):
yield (
self.unpack(character), self.unpack(orig), self.unpack(dest),
branch, turn, tick, self.unpack(rulebook)
)
def _charactery_rulebook_dump(self, qry):
for character, branch, turn, tick, rulebook in self.sql(qry+'_rulebook_dump'):
yield self.unpack(character), branch, turn, tick, self.unpack(rulebook)
character_rulebook_dump = partialmethod(_charactery_rulebook_dump, 'character')
avatar_rulebook_dump = partialmethod(_charactery_rulebook_dump, 'avatar')
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):
for character, rulebook, rule, branch, turn, tick in self.sql('character_rules_handled_dump'):
yield self.unpack(character), self.unpack(rulebook), rule, branch, turn, tick
def character_rules_changes_dump(self):
for (
character, rulebook, rule, branch, turn, tick, handled_branch, handled_turn
) in self.sql('character_rules_changes_dump'):
yield (
self.unpack(character), self.unpack(rulebook),
rule, branch, turn, tick, handled_branch, handled_turn
)
def avatar_rules_handled_dump(self):
for character, rulebook, rule, graph, avatar, branch, turn, tick in self.sql('avatar_rules_handled_dump'):
yield (
self.unpack(character), self.unpack(rulebook), rule,
self.unpack(graph), self.unpack(avatar), branch, turn, tick
)
def avatar_rules_changes_dump(self):
jl = self.unpack
for (
character, rulebook, rule, graph, avatar, branch, turn, tick, handled_branch, handled_turn
) in self.sql('avatar_rules_changes_dump'):
yield (
jl(character), jl(rulebook), rule, jl(graph), jl(avatar),
branch, turn, tick, handled_branch, handled_turn
)
def character_thing_rules_handled_dump(self):
for character, rulebook, rule, thing, branch, turn, tick in self.sql('character_thing_rules_handled_dump'):
yield self.unpack(character), self.unpack(rulebook), rule, self.unpack(thing), branch, turn, tick
def character_thing_rules_changes_dump(self):
jl = self.unpack
for (
character, rulebook, rule, thing, branch, turn, tick, handled_branch, handled_turn
) in self.sql('character_thing_rules_changes_dump'):
yield (
jl(character), jl(rulebook), rule, jl(thing),
branch, turn, tick, handled_branch, handled_turn
)
def character_place_rules_handled_dump(self):
for character, rulebook, rule, place, branch, turn, tick in self.sql('character_place_rules_handled_dump'):
yield self.unpack(character), self.unpack(rulebook), rule, self.unpack(place), 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.sql('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):
for character, rulebook, rule, orig, dest, branch, turn, tick in self.sql('character_portal_rules_handled_dump'):
yield (
self.unpack(character), self.unpack(rulebook), rule, self.unpack(orig), self.unpack(dest),
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.sql('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.sql('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.sql('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):
for character, orig, dest, rulebook, rule, branch, turn, tick in self.sql('portal_rules_handled_dump'):
yield (
self.unpack(character), self.unpack(orig), self.unpack(dest),
self.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.sql('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):
for character, sense, branch, turn, tick, function in self.sql('senses_dump'):
yield self.unpack(character), sense, branch, turn, tick, function
def things_dump(self):
for character, thing, branch, turn, tick, location in self.sql('things_dump'):
yield (
self.unpack(character), self.unpack(thing), branch, turn, tick,
self.unpack(location)
)
def avatars_dump(self):
for character_graph, avatar_graph, avatar_node, branch, turn, tick, is_av in self.sql('avatars_dump'):
yield (
self.unpack(character_graph), self.unpack(avatar_graph),
self.unpack(avatar_node), branch, turn, tick, is_av
)
def universal_set(self, key, branch, turn, tick, val):
key, val = map(self.pack, (key, val))
self.sql('universals_insert', key, branch, turn, tick, val)
def universal_del(self, key, branch, turn, tick):
key = self.pack(key)
self.sql('universals_insert', key, branch, turn, tick, None)
def comparison(
self, entity0, stat0, entity1,
stat1=None, oper='eq', 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.sql('{}_count'.format(tbl)).fetchone()[0]
def init_table(self, tbl):
try:
return self.sql('create_{}'.format(tbl))
except OperationalError:
pass
def rules_dump(self):
for (name,) in self.sql('rules_dump'):
yield name
def _set_rule_something(self, what, rule, branch, turn, tick, flist):
flist = self.pack(flist)
return self.sql('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):
self.sql('rules_insert', rule)
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):
name, rules = map(self.pack, (name, rules or []))
self.sql('rulebooks_insert', name, branch, turn, tick, rules)
def _set_rulebook_on_character(self, rbtyp, char, branch, turn, tick, rb):
char, rb = map(self.pack, (char, rb))
self.sql(rbtyp + '_rulebook_insert', char, branch, turn, tick, rb)
set_character_rulebook = partialmethod(_set_rulebook_on_character, 'character')
set_avatar_rulebook = partialmethod(_set_rulebook_on_character, 'avatar')
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.sql('rulebooks'):
yield self.unpack(book)
[docs] def exist_node(self, character, node, branch, turn, tick, extant):
super().exist_node(character, node, branch, turn, tick, extant)
[docs] def exist_edge(self, character, orig, dest, idx, branch, turn, tick, extant=None):
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)
def set_node_rulebook(self, character, node, branch, turn, tick, rulebook):
(character, node, rulebook) = map(
self.pack, (character, node, rulebook)
)
return self.sql('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.sql(
'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)
)
return self.sql(
'character_rules_handled_insert',
character,
rulebook,
rule,
branch,
turn,
tick,
)
def handled_avatar_rule(self, character, rulebook, rule, graph, av, branch, turn, tick):
character, graph, av, rulebook = map(
self.pack, (character, graph, av, rulebook)
)
return self.sql(
'avatar_rules_handled_insert',
character,
rulebook,
rule,
graph,
av,
branch,
turn,
tick
)
def handled_character_thing_rule(self, character, rulebook, rule, thing, branch, turn, tick):
character, thing, rulebook = map(
self.pack, (character, thing, rulebook)
)
return self.sql(
'character_thing_rules_handled_insert',
character,
rulebook,
rule,
thing,
branch,
turn,
tick
)
def handled_character_place_rule(self, character, rulebook, rule, place, branch, turn, tick):
character, rulebook, place = map(
self.pack, (character, rulebook, place)
)
return self.sql(
'character_place_rules_handled_insert',
character,
rulebook,
rule,
place,
branch,
turn,
tick
)
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)
)
return self.sql(
'character_portal_rules_handled_insert',
character,
rulebook,
rule,
orig,
dest,
branch,
turn,
tick
)
def handled_node_rule(
self, character, node, rulebook, rule, branch, turn, tick
):
(character, node, rulebook) = map(
self.pack, (character, node, rulebook)
)
return self.sql(
'node_rules_handled_insert',
character,
node,
rulebook,
rule,
branch,
turn,
tick
)
def handled_portal_rule(
self, character, orig, dest, rulebook, rule, branch, turn, tick
):
(character, orig, dest, rulebook) = map(
self.pack, (character, orig, dest, rulebook)
)
return self.sql(
'portal_rules_handled_insert',
character,
orig,
dest,
rulebook,
rule,
branch,
turn,
tick
)
def get_rulebook_char(self, rulemap, character):
character = self.pack(character)
for (book,) in self.sql(
'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.sql('del_things_after', character, thing, branch, turn, turn, tick)
self.sql(
'things_insert',
character,
thing,
branch,
turn,
tick,
loc
)
def avatar_set(self, character, graph, node, branch, turn, tick, isav):
(character, graph, node) = map(
self.pack, (character, graph, node)
)
self.sql(
'del_avatars_after',
character, graph, node, branch, turn, turn, tick
)
self.sql(
'avatars_insert', character, graph, node, branch, turn, tick, isav
)
def rulebooks_rules(self):
for (rulebook, rule) in self.sql('rulebooks_rules'):
yield map(self.unpack, (rulebook, rule))
def rulebook_get(self, rulebook, idx):
return self.unpack(
self.sql(
'rulebook_get', self.pack(rulebook), idx
).fetchone()[0]
)
def branch_descendants(self, branch):
for child in self.sql('branch_children', branch):
yield child
yield from self.branch_descendants(child)
def turns_completed_dump(self):
return self.sql('turns_completed_dump')
def complete_turn(self, branch, turn):
try:
self.sql('turns_completed_insert', branch, turn)
except IntegrityError:
self.sql('turns_completed_update', turn, branch)
[docs] def initdb(self):
"""Set up the database schema, both for allegedb and the special
extensions for LiSE
"""
super().initdb()
for table in (
'universals',
'rules',
'rulebooks',
'senses',
'things',
'character_rulebook',
'avatar_rulebook',
'character_thing_rulebook',
'character_place_rulebook',
'character_portal_rulebook',
'node_rulebook',
'portal_rulebook',
'avatars',
'character_rules_handled',
'avatar_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'
):
self.init_table(table)