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