1 from plomrogue.tasks import (Task_WAIT, Task_MOVE, Task_PICKUP,
3 from plomrogue.errors import ArgError, GameError
4 from plomrogue.commands import (cmd_GEN_WORLD, cmd_GET_GAMESTATE,
5 cmd_MAP, cmd_MAP, cmd_THING_TYPE,
6 cmd_THING_POS, cmd_THING_INVENTORY,
7 cmd_THING_HEALTH, cmd_SEED,
8 cmd_GET_PICKABLE_ITEMS, cmd_MAP_SIZE,
9 cmd_TERRAIN_LINE, cmd_PLAYER_ID,
10 cmd_TURN, cmd_SWITCH_PLAYER, cmd_SAVE)
11 from plomrogue.mapping import MapGeometryHex, MapChunk, YX
12 from plomrogue.parser import Parser
13 from plomrogue.io import GameIO
14 from plomrogue.misc import quote
15 from plomrogue.things import (Thing, ThingMonster, ThingHuman, ThingFood,
21 class PRNGod(random.Random):
24 self.prngod_seed = seed
27 return self.prngod_seed
33 self.prngod_seed = ((self.prngod_seed * 1103515245) + 12345) % 2**32
34 return (self.prngod_seed >> 16) / (2**16 - 1)
44 def get_thing(self, id_, create_unfound):
45 # No default for create_unfound because every call to get_thing
46 # should be accompanied by serious consideration whether to use it.
47 for thing in self.things:
51 t = self.thing_type(self, id_)
56 def things_at_pos(self, pos):
67 def __init__(self, game_file_name, *args, **kwargs):
68 super().__init__(*args, **kwargs)
69 self.io = GameIO(game_file_name, self)
71 self.map_geometry = MapGeometryHex()
72 self.tasks = {'WAIT': Task_WAIT,
74 'PICKUP': Task_PICKUP,
77 self.commands = {'GEN_WORLD': cmd_GEN_WORLD,
78 'GET_GAMESTATE': cmd_GET_GAMESTATE,
80 'MAP_SIZE': cmd_MAP_SIZE,
82 'THING_TYPE': cmd_THING_TYPE,
83 'THING_POS': cmd_THING_POS,
84 'THING_HEALTH': cmd_THING_HEALTH,
85 'THING_INVENTORY': cmd_THING_INVENTORY,
86 'TERRAIN_LINE': cmd_TERRAIN_LINE,
87 'GET_PICKABLE_ITEMS': cmd_GET_PICKABLE_ITEMS,
88 'PLAYER_ID': cmd_PLAYER_ID,
90 'SWITCH_PLAYER': cmd_SWITCH_PLAYER,
92 self.thing_type = Thing
93 self.thing_types = {'human': ThingHuman,
94 'monster': ThingMonster,
97 self.player_is_alive = True
99 self.max_map_awakeness = 100
100 self.rand = PRNGod(0)
102 def get_string_options(self, string_option_type):
103 if string_option_type == 'direction':
104 return self.map_geometry.get_directions()
105 elif string_option_type == 'thingtype':
106 return list(self.thing_types.keys())
109 def send_gamestate(self, connection_id=None):
110 """Send out game state data relevant to clients."""
112 def send_thing(thing):
113 view_pos = self.map_geometry.pos_in_view(thing.position,
114 self.player.view_offset,
116 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
117 self.io.send('THING_POS %s %s' % (thing.id_, view_pos))
119 self.io.send('PLAYER_ID ' + str(self.player_id))
120 self.io.send('TURN ' + str(self.turn))
121 visible_map = self.player.get_visible_map()
122 self.io.send('VISIBLE_MAP %s %s' % (visible_map.size,
123 visible_map.start_indented))
124 for y, line in visible_map.lines():
125 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, quote(line)))
126 visible_things = self.player.get_visible_things()
127 for thing in visible_things:
129 if hasattr(thing, 'health'):
130 self.io.send('THING_HEALTH %s %s' % (thing.id_,
132 if len(self.player.inventory) > 0:
133 self.io.send('PLAYER_INVENTORY %s' %
134 ','.join([str(i) for i in self.player.inventory]))
136 self.io.send('PLAYER_INVENTORY ,')
137 for id_ in self.player.inventory:
138 thing = self.get_thing(id_, create_unfound=False)
140 self.io.send('GAME_STATE_COMPLETE')
143 """Send turn finish signal, run game world, send new world data.
145 First sends 'TURN_FINISHED' message, then runs game world
146 until new player input is needed, then sends game state.
148 self.io.send('TURN_FINISHED ' + str(self.turn))
149 self.proceed_to_next_player_turn()
150 msg = str(self.player._last_task_result)
151 self.io.send('LAST_PLAYER_TASK_RESULT ' + quote(msg))
152 self.send_gamestate()
154 def get_command(self, command_name):
156 def partial_with_attrs(f, *args, **kwargs):
157 from functools import partial
158 p = partial(f, *args, **kwargs)
159 p.__dict__.update(f.__dict__)
162 def cmd_TASK_colon(task_name, game, *args):
163 if not game.player_is_alive:
164 raise GameError('You are dead.')
165 game.player.set_task(task_name, args)
168 def cmd_SET_TASK_colon(task_name, game, thing_id, todo, *args):
169 t = game.get_thing(thing_id, False)
171 raise ArgError('No such Thing.')
172 task_class = game.tasks[task_name]
173 t.task = task_class(t, args)
176 def task_prefixed(command_name, task_prefix, task_command,
177 argtypes_prefix=None):
178 if command_name.startswith(task_prefix):
179 task_name = command_name[len(task_prefix):]
180 if task_name in self.tasks:
181 f = partial_with_attrs(task_command, task_name, self)
182 task = self.tasks[task_name]
184 f.argtypes = argtypes_prefix + ' ' + task.argtypes
186 f.argtypes = task.argtypes
190 command = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
193 command = task_prefixed(command_name, 'SET_TASK:', cmd_SET_TASK_colon,
194 'int:nonneg int:nonneg ')
197 if command_name in self.commands:
198 f = partial_with_attrs(self.commands[command_name], self)
204 return self.get_thing(self.player_id, create_unfound=False)
206 def new_thing_id(self):
207 if len(self.things) == 0:
209 # DANGEROUS – if anywhere we append a thing to the list of lower
210 # ID than the highest-value ID, this might lead to re-using an
211 # already active ID. This condition /should/ not be fulfilled
212 # anywhere in the code, but if it does, trouble here is one of
213 # the more obvious indicators that it does – that's why there's
214 # no safeguard here against this.
215 return self.things[-1].id_ + 1
217 def get_map(self, map_pos):
218 if not (map_pos in self.maps and
219 self.maps[map_pos].size == self.map_size):
220 self.maps[map_pos] = MapChunk(self.map_size)
221 self.maps[map_pos].awake = self.max_map_awakeness
222 for pos in self.maps[map_pos]:
223 self.maps[map_pos][pos] = '.'
224 return self.maps[map_pos]
226 def proceed_to_next_player_turn(self):
227 """Run game world turns until player can decide their next step.
229 All things and processes inside the player's reality bubble
230 are worked through. Things are furthered in their tasks and,
231 if finished, decide new ones. The iteration order is: first
232 all things that come after the player in the world things
233 list, then (after incrementing the world turn) all that come
234 before the player; then the player's .proceed() is run.
236 Next, parts of the game world are put to sleep or woken up
237 based on how close they are to the player's position, or how
238 short ago the player visited them.
240 If the player's last task is finished at the end of the loop,
241 it breaks; otherwise it starts again.
246 for thing in self.things[player_i+1:]:
249 for map_pos in self.maps:
250 if self.maps[map_pos].awake:
251 for pos in self.maps[map_pos]:
252 if self.rand.random() > 0.999 and \
253 self.maps[map_pos][pos] == '.' and \
254 len(self.things_at_pos((map_pos, pos))) == 0:
255 self.add_thing_at('food', (map_pos, pos))
256 for thing in self.things[:player_i]:
258 self.player.proceed(is_AI=False)
260 def reality_bubble():
262 def regenerate_chunk_from_map_stats(map_):
264 max_stat = self.max_map_awakeness
265 for t_type in map_.stats:
266 stat = map_.stats[t_type]
267 to_create = stat['population'] // max_stat
268 mod_created = int(self.rand.randint(0, max_stat - 1) <
269 (stat['population'] % max_stat))
270 to_create = (stat['population'] // max_stat) + mod_created
273 average_health = None
274 if stat['health'] > 0:
275 average_health = math.ceil(stat['health'] /
277 for i in range(to_create):
278 t = self.add_thing_at_random(map_pos, t_type)
280 t.health = average_health
281 #if hasattr(t, 'health'):
282 # print('DEBUG create', t.type_, t.health)
284 for map_pos in self.maps:
285 m = self.maps[map_pos]
286 if map_pos in self.player.close_maps:
288 # Newly inside chunks are regenerated from .stats.
290 #print('DEBUG regen stats', map_pos, m.stats)
291 regenerate_chunk_from_map_stats(m)
293 # Inside chunks are set to max .awake and don't collect
295 m.awake = self.max_map_awakeness
298 # Outside chunks grow distant through .awake decremention.
299 # They collect .stats until they fall asleep – then any things
300 # inside are disappeared.
303 # We iterate over a list comprehension of self.things,
304 # since we might delete elements of self.things.
305 for t in [t for t in self.things]:
306 if t.position[0] == map_pos:
307 if not t.type_ in m.stats:
308 m.stats[t.type_] = {'population': 0,
310 m.stats[t.type_]['population'] += 1
311 if isinstance(t, ThingAnimate):
312 m.stats[t.type_]['health'] += t.health
314 # TODO: Handle inventory.
315 del self.things[self.things.index(t)]
317 # print('DEBUG sleep stats', map_pos, m.stats)
320 player_i = self.things.index(self.player)
323 if self.player.task is None or not self.player_is_alive:
326 def add_thing_at(self, type_, pos):
327 t = self.thing_types[type_](self)
332 def add_thing_at_random(self, big_yx, type_):
335 YX(self.rand.randint(0, self.map_size.y - 1),
336 self.rand.randint(0, self.map_size.x - 1)))
337 if self.maps[new_pos[0]][new_pos[1]] != '.':
339 if len(self.things_at_pos(new_pos)) > 0:
341 return self.add_thing_at(type_, new_pos)
343 def make_map_chunk(self, big_yx):
344 map_ = self.get_map(big_yx)
346 map_[pos] = self.rand.choice(('.', '.', '.', '~', 'x'))
347 self.add_thing_at_random(big_yx, 'monster')
348 self.add_thing_at_random(big_yx, 'monster')
349 self.add_thing_at_random(big_yx, 'monster')
350 self.add_thing_at_random(big_yx, 'monster')
351 self.add_thing_at_random(big_yx, 'monster')
352 self.add_thing_at_random(big_yx, 'monster')
353 self.add_thing_at_random(big_yx, 'monster')
354 self.add_thing_at_random(big_yx, 'monster')
355 self.add_thing_at_random(big_yx, 'food')
356 self.add_thing_at_random(big_yx, 'food')
357 self.add_thing_at_random(big_yx, 'food')
358 self.add_thing_at_random(big_yx, 'food')
360 def make_new_world(self, size, seed):
366 self.make_map_chunk(YX(0,0))
367 player = self.add_thing_at_random(YX(0,0), 'human')
368 player.surroundings # To help initializing reality bubble, see
369 # comment on ThingAnimate._position_set
370 self.player_id = player.id_