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 return self.things[-1].id_ + 1
209 def get_map(self, map_pos):
210 if not (map_pos in self.maps and
211 self.maps[map_pos].size == self.map_size):
212 self.maps[map_pos] = MapChunk(self.map_size)
213 self.maps[map_pos].awake = self.max_map_awakeness
214 for pos in self.maps[map_pos]:
215 self.maps[map_pos][pos] = '.'
216 return self.maps[map_pos]
218 def proceed_to_next_player_turn(self):
219 """Run game world turns until player can decide their next step.
221 All things and processes inside the player's reality bubble
222 are worked through. Things are furthered in their tasks and,
223 if finished, decide new ones. The iteration order is: first
224 all things that come after the player in the world things
225 list, then (after incrementing the world turn) all that come
226 before the player; then the player's .proceed() is run.
228 Next, parts of the game world are put to sleep or woken up
229 based on how close they are to the player's position, or how
230 short ago the player visited them.
232 If the player's last task is finished at the end of the loop,
233 it breaks; otherwise it starts again.
238 for thing in self.things[player_i+1:]:
241 for map_pos in self.maps:
242 if self.maps[map_pos].awake:
243 for pos in self.maps[map_pos]:
244 if self.rand.random() > 0.999 and \
245 self.maps[map_pos][pos] == '.' and \
246 len(self.things_at_pos((map_pos, pos))) == 0:
247 self.add_thing_at('food', (map_pos, pos))
248 for thing in self.things[:player_i]:
250 self.player.proceed(is_AI=False)
252 def reality_bubble():
254 def regenerate_chunk_from_map_stats(map_):
256 max_stat = self.max_map_awakeness
257 for t_type in map_.stats:
258 stat = map_.stats[t_type]
259 to_create = stat['population'] // max_stat
260 mod_created = int(self.rand.randint(0, max_stat - 1) <
261 (stat['population'] % max_stat))
262 to_create = (stat['population'] // max_stat) + mod_created
265 average_health = None
266 if stat['health'] > 0:
267 average_health = math.ceil(stat['health'] /
269 for i in range(to_create):
270 t = self.add_thing_at_random(map_pos, t_type)
272 t.health = average_health
273 #if hasattr(t, 'health'):
274 # print('DEBUG create', t.type_, t.health)
276 for map_pos in self.maps:
277 m = self.maps[map_pos]
278 if map_pos in self.player.close_maps:
280 # Newly inside chunks are regenerated from .stats.
282 #print('DEBUG regen stats', map_pos, m.stats)
283 regenerate_chunk_from_map_stats(m)
285 # Inside chunks are set to max .awake and don't collect
287 m.awake = self.max_map_awakeness
290 # Outside chunks grow distant through .awake decremention.
291 # They collect .stats until they fall asleep – then any things
292 # inside are disappeared.
295 for t in self.things:
296 if t.position[0] == map_pos:
297 if not t.type_ in m.stats:
298 m.stats[t.type_] = {'population': 0,
300 m.stats[t.type_]['population'] += 1
301 if isinstance(t, ThingAnimate):
302 m.stats[t.type_]['health'] += t.health
304 del self.things[self.things.index(t)]
306 # print('DEBUG sleep stats', map_pos, m.stats)
309 player_i = self.things.index(self.player)
312 if self.player.task is None or not self.player_is_alive:
315 def add_thing_at(self, type_, pos):
316 t = self.thing_types[type_](self)
321 def add_thing_at_random(self, big_yx, type_):
324 YX(self.rand.randint(0, self.map_size.y - 1),
325 self.rand.randint(0, self.map_size.x - 1)))
326 if self.maps[new_pos[0]][new_pos[1]] != '.':
328 if len(self.things_at_pos(new_pos)) > 0:
330 return self.add_thing_at(type_, new_pos)
332 def make_map_chunk(self, big_yx):
333 map_ = self.get_map(big_yx)
335 map_[pos] = self.rand.choice(('.', '.', '.', '~', 'x'))
336 self.add_thing_at_random(big_yx, 'monster')
337 self.add_thing_at_random(big_yx, 'monster')
338 self.add_thing_at_random(big_yx, 'monster')
339 self.add_thing_at_random(big_yx, 'monster')
340 self.add_thing_at_random(big_yx, 'monster')
341 self.add_thing_at_random(big_yx, 'monster')
342 self.add_thing_at_random(big_yx, 'monster')
343 self.add_thing_at_random(big_yx, 'monster')
344 self.add_thing_at_random(big_yx, 'food')
345 self.add_thing_at_random(big_yx, 'food')
346 self.add_thing_at_random(big_yx, 'food')
347 self.add_thing_at_random(big_yx, 'food')
349 def make_new_world(self, size, seed):
355 self.make_map_chunk(YX(0,0))
356 player = self.add_thing_at_random(YX(0,0), 'human')
357 player.surroundings # To help initializing reality bubble, see
358 # comment on ThingAnimate._position_set
359 self.player_id = player.id_