home · contact · privacy
Add ever-decreasing health to animate things, and death.
[plomrogue2-experiments] / new / plomrogue / game.py
1 from plomrogue.tasks import Task_WAIT, Task_MOVE, Task_PICKUP, Task_DROP
2 from plomrogue.errors import ArgError, GameError
3 from plomrogue.commands import (cmd_GEN_WORLD, cmd_GET_GAMESTATE,
4                                 cmd_MAP, cmd_MAP, cmd_THING_TYPE,
5                                 cmd_THING_POS, cmd_THING_INVENTORY,
6                                 cmd_THING_HEALTH,
7                                 cmd_GET_PICKABLE_ITEMS,
8                                 cmd_TERRAIN_LINE, cmd_PLAYER_ID,
9                                 cmd_TURN, cmd_SWITCH_PLAYER, cmd_SAVE)
10 from plomrogue.mapping import MapHex
11 from plomrogue.parser import Parser
12 from plomrogue.io import GameIO
13 from plomrogue.misc import quote, stringify_yx
14 from plomrogue.things import Thing, ThingMonster, ThingHuman, ThingItem
15
16
17
18 class WorldBase:
19
20     def __init__(self, game):
21         self.turn = 0
22         self.things = []
23         self.game = game
24
25     def get_thing(self, id_, create_unfound=True):
26         for thing in self.things:
27             if id_ == thing.id_:
28                 return thing
29         if create_unfound:
30             t = self.game.thing_type(self, id_)
31             self.things += [t]
32             return t
33         return None
34
35
36
37 class World(WorldBase):
38
39     def __init__(self, *args, **kwargs):
40         super().__init__(*args, **kwargs)
41         self.player_id = 0
42         self.player_is_alive = True
43
44     @property
45     def player(self):
46         return self.get_thing(self.player_id)
47
48     def new_thing_id(self):
49         if len(self.things) == 0:
50             return 0
51         return self.things[-1].id_ + 1
52
53     def new_map(self, yx):
54         self.map_ = self.game.map_type(yx)
55
56     def proceed_to_next_player_turn(self):
57         """Run game world turns until player can decide their next step.
58
59         Iterates through all non-player things, on each step
60         furthering them in their tasks (and letting them decide new
61         ones if they finish). The iteration order is: first all things
62         that come after the player in the world things list, then
63         (after incrementing the world turn) all that come before the
64         player; then the player's .proceed() is run, and if it does
65         not finish his task, the loop starts at the beginning. Once
66         the player's task is finished, or the player is dead, the loop
67         breaks.
68
69         """
70         while True:
71             player_i = self.things.index(self.player)
72             for thing in self.things[player_i+1:]:
73                 thing.proceed()
74             self.turn += 1
75             for thing in self.things[:player_i]:
76                 thing.proceed()
77             self.player.proceed(is_AI=False)
78             if self.player.task is None or not self.player_is_alive:
79                 break
80
81     def make_new(self, yx, seed):
82         import random
83
84         def add_thing(type_):
85             t = self.game.thing_types[type_](self)
86             t.position = (random.randint(0, yx[0] -1),
87                           random.randint(0, yx[1] - 1))
88             self.things += [t]
89             return t
90
91         self.things = []
92         random.seed(seed)
93         self.turn = 0
94         self.new_map(yx)
95         for pos in self.map_:
96             if 0 in pos or (yx[0] - 1) == pos[0] or (yx[1] - 1) == pos[1]:
97                 self.map_[pos] = '#'
98                 continue
99             self.map_[pos] = random.choice(('.', '.', '.', '.', 'x'))
100
101         player = add_thing('human')
102         self.player_id = player.id_
103         add_thing('monster')
104         add_thing('monster')
105         add_thing('item')
106         add_thing('item')
107         return 'success'
108
109
110
111 class Game:
112
113     def __init__(self, game_file_name):
114         self.io = GameIO(game_file_name, self)
115         self.map_type = MapHex
116         self.tasks = {'WAIT': Task_WAIT,
117                       'MOVE': Task_MOVE,
118                       'PICKUP': Task_PICKUP,
119                       'DROP': Task_DROP}
120         self.commands = {'GEN_WORLD': cmd_GEN_WORLD,
121                          'GET_GAMESTATE': cmd_GET_GAMESTATE,
122                          'MAP': cmd_MAP,
123                          'THING_TYPE': cmd_THING_TYPE,
124                          'THING_POS': cmd_THING_POS,
125                          'THING_HEALTH': cmd_THING_HEALTH,
126                          'THING_INVENTORY': cmd_THING_INVENTORY,
127                          'TERRAIN_LINE': cmd_TERRAIN_LINE,
128                          'GET_PICKABLE_ITEMS': cmd_GET_PICKABLE_ITEMS,
129                          'PLAYER_ID': cmd_PLAYER_ID,
130                          'TURN': cmd_TURN,
131                          'SWITCH_PLAYER': cmd_SWITCH_PLAYER,
132                          'SAVE': cmd_SAVE}
133         self.world_type = World
134         self.world = self.world_type(self)
135         self.thing_type = Thing
136         self.thing_types = {'human': ThingHuman,
137                             'monster': ThingMonster,
138                             'item': ThingItem}
139
140     def get_string_options(self, string_option_type):
141         if string_option_type == 'direction':
142             return self.world.map_.get_directions()
143         elif string_option_type == 'thingtype':
144             return list(self.thing_types.keys())
145         return None
146
147     def send_gamestate(self, connection_id=None):
148         """Send out game state data relevant to clients."""
149
150         self.io.send('TURN ' + str(self.world.turn))
151         self.io.send('MAP ' + stringify_yx(self.world.map_.size))
152         visible_map = self.world.player.get_visible_map()
153         for y, line in visible_map.lines():
154             self.io.send('VISIBLE_MAP_LINE %5s %s' % (y, quote(line)))
155         visible_things = self.world.player.get_visible_things()
156         for thing in visible_things:
157             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
158             self.io.send('THING_POS %s %s' % (thing.id_,
159                                               stringify_yx(thing.position)))
160             if hasattr(thing, 'health'):
161                 self.io.send('THING_HEALTH %s %s' % (thing.id_,
162                                                      thing.health))
163         if len(self.world.player.inventory) > 0:
164             self.io.send('PLAYER_INVENTORY %s' %
165                          ','.join([str(i) for i in self.world.player.inventory]))
166         else:
167             self.io.send('PLAYER_INVENTORY ,')
168         for id_ in self.world.player.inventory:
169             thing = self.world.get_thing(id_)
170             self.io.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
171             self.io.send('THING_POS %s %s' % (thing.id_,
172                                               stringify_yx(thing.position)))
173         self.io.send('GAME_STATE_COMPLETE')
174
175     def proceed(self):
176         """Send turn finish signal, run game world, send new world data.
177
178         First sends 'TURN_FINISHED' message, then runs game world
179         until new player input is needed, then sends game state.
180         """
181         self.io.send('TURN_FINISHED ' + str(self.world.turn))
182         self.world.proceed_to_next_player_turn()
183         msg = str(self.world.player._last_task_result)
184         self.io.send('LAST_PLAYER_TASK_RESULT ' + quote(msg))
185         self.send_gamestate()
186
187     def get_command(self, command_name):
188
189         def partial_with_attrs(f, *args, **kwargs):
190             from functools import partial
191             p = partial(f, *args, **kwargs)
192             p.__dict__.update(f.__dict__)
193             return p
194
195         def cmd_TASK_colon(task_name, game, *args):
196             if not game.world.player_is_alive:
197                 raise GameError('You are dead.')
198             game.world.player.set_task(task_name, args)
199             game.proceed()
200
201         def cmd_SET_TASK_colon(task_name, game, thing_id, todo, *args):
202             t = game.world.get_thing(thing_id, False)
203             if t is None:
204                 raise ArgError('No such Thing.')
205             task_class = game.tasks[task_name]
206             t.task = task_class(t, args)
207             t.task.todo = todo
208
209         def task_prefixed(command_name, task_prefix, task_command,
210                           argtypes_prefix=None):
211             if command_name[:len(task_prefix)] == task_prefix:
212                 task_name = command_name[len(task_prefix):]
213                 if task_name in self.tasks:
214                     f = partial_with_attrs(task_command, task_name, self)
215                     task = self.tasks[task_name]
216                     if argtypes_prefix:
217                         f.argtypes = argtypes_prefix + ' ' + task.argtypes
218                     else:
219                         f.argtypes = task.argtypes
220                     return f
221             return None
222
223         command = task_prefixed(command_name, 'TASK:', cmd_TASK_colon)
224         if command:
225             return command
226         command = task_prefixed(command_name, 'SET_TASK:', cmd_SET_TASK_colon,
227                                 'int:nonneg int:nonneg ')
228         if command:
229             return command
230         if command_name in self.commands:
231             f = partial_with_attrs(self.commands[command_name], self)
232             return f
233         return None