/
environment.py
169 lines (144 loc) · 6.65 KB
/
environment.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
"""
Environment describes an abstract set of variables that are known in a given context.
The environment class tracks *nested generation contexts*, e.g. enabling a generation chain as:
0. Init generator & environment
1. Generate Region
2. Generate Structure (using Region env. data)
3. Generate Quests (using Region & Structure env. data)
...
"""
from copy import deepcopy
from numbers import Number
from typing import Callable, List
from gnomebrew.game.objects.data_object import DataObject
class Environment(DataObject):
"""
Wraps environment data.
"""
_update_strategies_by_name = dict()
def __init__(self, data: dict):
DataObject.__init__(self, data)
@classmethod
def empty(cls) -> 'Environment':
"""
Generates an empty environment object.
:return: The generated, empty environment
"""
return Environment({
'variables': {},
'stack': [[]]
})
def create_copy(self):
"""
Creates a new environment with identical properties to this one.
:return: A new environment with identical properties to this one.
"""
copy = Environment(deepcopy(self._data))
return copy
def has_variable(self, varname) -> bool:
"""
Checks if a variable is set in this environment.
:param varname: Variable name to check.
:return: `True`, if variable is set in this environment. Otherwise `False`.
"""
return varname in self._data['variables'] and self._data['variables'][varname]
def get(self, varname, **kwargs):
"""
Retrieves a variable from the environment.
:param varname: Variable Name
:keyword default: Default in case variable doesn't exist
:return: Variable value. If variable is not set and `default` provided, will return
the given default value.
"""
if varname not in self._data['variables'] or not self._data['variables'][varname]:
if 'default' in kwargs:
return kwargs['default']
else:
raise Exception(f"Unknown variable ({varname}) requested without default.")
else:
return self._data['variables'][varname][-1]
def get_variables(self):
return self._data['variables']
def update(self, gen: 'Generator', varname: str, value, stack_offset: int = 0):
"""
Updates an environment variable with given details.
If the variable has not been set, it will be set. If it already exists and there is a more sophisticated
strategy available, the both values will be combined accordingly; if not, the value will
be set hard to the new input.
:param gen: The active generator of this environment
:param varname: The variable to update.
:param value: The update value.
"""
if stack_offset < 0:
raise Exception(f"Negative offset is not allowed: {stack_offset}")
# STEP 1: Add the variable to the data
if varname not in self._data['variables']:
# No entry yet. Add an empty 'editing history' list with the given value
self._data['variables'][varname] = [value]
else:
# Variable is already defined. Pick the most appropriate update strategy
if varname in Environment._update_strategies_by_name:
# There's a strategy specific to this variable name. Use it
strategy = Environment._update_strategies_by_name[varname]
else:
# There is no dedicated strategy for this variable name. Instead, pick a fitting strategy from the
# Default strategies
strategy = None
if not strategy:
# no match, hard set
self._data['variables'][varname].append(value)
else:
self._data['variables'][varname].append(strategy(gen, self._data['variables'][varname], value))
# STEP 2: Update the stack information
self._data['stack'][-1 - stack_offset].append(varname)
def increase_stacklevel(self):
"""
Increases this environment\'s stack level.
All variables added before this call are safe from the next `decrease_stacklevel`.
"""
# Append a new stack list at the end of the list
self._data['stack'].append([])
def decrease_stacklevel(self):
"""
Decreases this environment\'s stack level.
Removes all variable updates since the last `increase_stacklevel`.
"""
# Pop the latest stack element: The list of changed variable names
level_variables: List[str] = self._data['stack'].pop()
# Remove all variables from latest level
for variable_to_remove in level_variables:
self._data['variables'][variable_to_remove].pop()
def incorporate_env(self, other):
"""
Incorporates the data from another environment.
:param other: Another environment.
"""
self.incorporate_rule_dict(other.variables)
def incorporate_rule_dict(self, gen: 'Generator', rules: dict):
"""
Incorporates a dictionary of environment variables. After this function has been called, this environment will
* Contain all environment variables that have not been set yet as they appear in `rules`.
* call `update` on all variables in `rules` that already exist in this environment
:param gen: A generator to use for eventual RNG
:param rules: A `dict` of values to incorporate
"""
intersect = set(self._data['variables'].keys()) & set(rules.keys())
# Directly insert whatever value from other we don't have set in self
self._data['variables'].update({k: v for k, v in rules.items() if k not in intersect})
for key in intersect:
# Properly configure updates for variables already set
self.update(gen, key, rules[key])
@staticmethod
def update_rule(var_name: str):
"""
Annotation method to signify a special rule for how to update a particular environment variable.
:param var_name: The name of the environment variable for which this rule is to be applied.
:return Expects a function with two parameters (old & new) that returns the result of the applied
update rule.
"""
def wrapper(fun: Callable):
Environment._update_strategies_by_name[var_name] = fun
return fun
return wrapper
# Environment Data Validation
Environment.validation_parameters(('variables', dict), ('stack', list))