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=True):
45 for thing in self.things:
49 t = self.thing_type(self, id_)
54 def things_at_pos(self, pos):
65 def __init__(self, game_file_name, *args, **kwargs):
66 super().__init__(*args, **kwargs)
67 self.io = GameIO(game_file_name, self)
69 self.map_geometry = MapGeometryHex()
70 self.tasks = {'WAIT': Task_WAIT,
72 'PICKUP': Task_PICKUP,
75 self.commands = {'GEN_WORLD': cmd_GEN_WORLD,
76 'GET_GAMESTATE': cmd_GET_GAMESTATE,
78 'MAP_SIZE': cmd_MAP_SIZE,
80 'THING_TYPE': cmd_THING_TYPE,
81 'THING_POS': cmd_THING_POS,
82 'THING_HEALTH': cmd_THING_HEALTH,
83 'THING_INVENTORY': cmd_THING_INVENTORY,
84 'TERRAIN_LINE': cmd_TERRAIN_LINE,
85 'GET_PICKABLE_ITEMS': cmd_GET_PICKABLE_ITEMS,
86 'PLAYER_ID': cmd_PLAYER_ID,
88 'SWITCH_PLAYER': cmd_SWITCH_PLAYER,
90 self.thing_type = Thing
91 self.thing_types = {'human': ThingHuman,
92 'monster': ThingMonster,
95 self.player_is_alive = True
97 self.max_map_awakeness = 100
100 def get_string_options(self, string_option_type):
101 if string_option_type == 'direction':
102 return self.map_geometry.get_directions()
103 elif string_option_type == 'thingtype':
104 return list(self.thing_types.keys())
107 def send_gamestate(self, connection_id=None):
108 """Send out game state data relevant to clients."""
110 def send_thing(thing):
111 view_pos = self.map_geometry.pos_in_view(thing.position,
112 self.player.view_offset,
114 self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
115 self.io.send('THING_POS %s %s' % (thing.id_, view_pos))
117 self.io.send('PLAYER_ID ' + str(self.player_id))
118 self.io.send('TURN ' + str(self.turn))
119 visible_map = self.player.get_visible_map()
120 self.io.send('VISIBLE_MAP %s %s' % (visible_map.size,
121 visible_map.start_indented))
122 for y, line in visible_map.lines():
123 self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, quote(line)))
124 visible_things = self.player.get_visible_things()
125 for thing in visible_things:
127 if hasattr(thing, 'health'):
128 self.io.send('THING_HEALTH %s %s' % (thing.id_,
130 if len(self.player.inventory) > 0:
131 self.io.send('PLAYER_INVENTORY %s' %
132 ','.join([str(i) for i in self.player.inventory]))
134 self.io.send('PLAYER_INVENTORY ,')
135 for id_ in self.player.inventory:
136 thing = self.get_thing(id_)
138 self.io.send('GAME_STATE_COMPLETE')
141 """Send turn finish signal, run game world, send new world data.
143 First sends 'TURN_FINISHED' message, then runs game world
144 until new player input is needed, then sends game state.
146 self.io.send('TURN_FINISHED ' + str(self.turn))
147 self.proceed_to_next_player_turn()
148 msg = str(self.player._last_task_result)
149 self.io.send('LAST_PLAYER_TASK_RESULT ' + quote(msg))
150 self.send_gamestate()
152 def get_command(self, command_name):
154 def partial_with_attrs(f, *args, **kwargs):
155 from functools import partial
156 p = partial(f, *args, **kwargs)
157 p.__dict__.update(f.__dict__)
160 def cmd_TASK_colon(task_name, game, *args):
161 if not game.player_is_alive:
162 raise GameError('You are dead.')
163 game.player.set_task(task_name, args)
166 def cmd_SET_TASK_colon(task_name, game, thing_id, todo, *args):
167 t = game.get_thing(thing_id, False)
169 raise ArgError('No such Thing.')
170 task_class = game.tasks[task_name]
171 t.task = task_class(t, args)
174 def task_prefixed(command_name, task_prefix, task_command,
175 argtypes_prefix=None):
176 if command_name[:len(task_prefix)] == task_prefix:
177 task_name = command_name[len(task_prefix):]
178 if task_name in self.tasks:
179 f = partial_with_attrs(task_command, task_name, self)
180 task = self.tasks[task_name]
182 f.argtypes = argtypes_prefix + ' ' + task.argtypes
184 f.argtypes = task.argtypes
188 command = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
191 command = task_prefixed(command_name, 'SET_TASK:', cmd_SET_TASK_colon,
192 'int:nonneg int:nonneg ')
195 if command_name in self.commands:
196 f = partial_with_attrs(self.commands[command_name], self)
202 return self.get_thing(self.player_id)
204 def new_thing_id(self):
205 if len(self.things) == 0:
207 # DANGEROUS – if anywhere we append a thing to the list of lower
208 # ID than the highest-value ID, this might lead to re-using an
209 # already active ID. This should not happen anywhere in the
210 # code, but a break here might be more visible.
211 return self.things[-1].id_ + 1
213 def get_map(self, map_pos):
214 if not (map_pos in self.maps and
215 self.maps[map_pos].size == self.map_size):
216 self.maps[map_pos] = MapChunk(self.map_size)
217 self.maps[map_pos].awake = self.max_map_awakeness
218 for pos in self.maps[map_pos]:
219 self.maps[map_pos][pos] = '.'
220 return self.maps[map_pos]
222 def proceed_to_next_player_turn(self):
223 """Run game world turns until player can decide their next step.
225 All things and processes inside the player's reality bubble
226 are worked through. Things are furthered in their tasks and,
227 if finished, decide new ones. The iteration order is: first
228 all things that come after the player in the world things
229 list, then (after incrementing the world turn) all that come
230 before the player; then the player's .proceed() is run.
232 Next, parts of the game world are put to sleep or woken up
233 based on how close they are to the player's position, or how
234 short ago the player visited them.
236 If the player's last task is finished at the end of the loop,
237 it breaks; otherwise it starts again.
242 for thing in self.things[player_i+1:]:
245 for map_pos in self.maps:
246 if self.maps[map_pos].awake:
247 for pos in self.maps[map_pos]:
248 if self.rand.random() > 0.999 and \
249 self.maps[map_pos][pos] == '.' and \
250 len(self.things_at_pos((map_pos, pos))) == 0:
251 self.add_thing_at('food', (map_pos, pos))
252 for thing in self.things[:player_i]:
254 self.player.proceed(is_AI=False)
256 def reality_bubble():
258 def regenerate_chunk_from_map_stats(map_):
260 max_stat = self.max_map_awakeness
261 for t_type in map_.stats:
262 stat = map_.stats[t_type]
263 to_create = stat['population'] // max_stat
264 mod_created = int(self.rand.randint(0, max_stat - 1) <
265 (stat['population'] % max_stat))
266 to_create = (stat['population'] // max_stat) + mod_created
269 average_health = None
270 if stat['health'] > 0:
271 average_health = math.ceil(stat['health'] /
273 for i in range(to_create):
274 t = self.add_thing_at_random(map_pos, t_type)
276 t.health = average_health
277 #if hasattr(t, 'health'):
278 # print('DEBUG create', t.type_, t.health)
280 for map_pos in self.maps:
281 m = self.maps[map_pos]
282 if map_pos in self.player.close_maps:
284 # Newly inside chunks are regenerated from .stats.
286 #print('DEBUG regen stats', map_pos, m.stats)
287 regenerate_chunk_from_map_stats(m)
289 # Inside chunks are set to max .awake and don't collect
291 m.awake = self.max_map_awakeness
294 # Outside chunks grow distant through .awake decremention.
295 # They collect .stats until they fall asleep – then any things
296 # inside are disappeared.
299 # We iterate over a list comprehension of self.things,
300 # since we might delete elements of self.things.
301 for t in [t for t in self.things]:
302 if t.position[0] == map_pos:
303 if not t.type_ in m.stats:
304 m.stats[t.type_] = {'population': 0,
306 m.stats[t.type_]['population'] += 1
307 if isinstance(t, ThingAnimate):
308 m.stats[t.type_]['health'] += t.health
310 # TODO: Handle inventory.
311 del self.things[self.things.index(t)]
313 # print('DEBUG sleep stats', map_pos, m.stats)
316 player_i = self.things.index(self.player)
319 if self.player.task is None or not self.player_is_alive:
322 def add_thing_at(self, type_, pos):
323 t = self.thing_types[type_](self)
328 def add_thing_at_random(self, big_yx, type_):
331 YX(self.rand.randint(0, self.map_size.y - 1),
332 self.rand.randint(0, self.map_size.x - 1)))
333 if self.maps[new_pos[0]][new_pos[1]] != '.':
335 if len(self.things_at_pos(new_pos)) > 0:
337 return self.add_thing_at(type_, new_pos)
339 def make_map_chunk(self, big_yx):
340 map_ = self.get_map(big_yx)
342 map_[pos] = self.rand.choice(('.', '.', '.', '~', 'x'))
343 self.add_thing_at_random(big_yx, 'monster')
344 self.add_thing_at_random(big_yx, 'monster')
345 self.add_thing_at_random(big_yx, 'monster')
346 self.add_thing_at_random(big_yx, 'monster')
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, 'food')
352 self.add_thing_at_random(big_yx, 'food')
353 self.add_thing_at_random(big_yx, 'food')
354 self.add_thing_at_random(big_yx, 'food')
356 def make_new_world(self, size, seed):
362 self.make_map_chunk(YX(0,0))
363 player = self.add_thing_at_random(YX(0,0), 'human')
364 player.surroundings # To help initializing reality bubble, see
365 # comment on ThingAnimate._position_set
366 self.player_id = player.id_