home · contact · privacy
Refactor.
[plomrogue2-experiments] / server_ / game.py
1 import sys
2 sys.path.append('../')
3 import game_common
4 import parser
5
6
7 class GameError(Exception):
8     pass
9
10
11 def move_pos(direction, pos_yx):
12     if direction == 'UP':
13         pos_yx[0] -= 1
14     elif direction == 'DOWN':
15         pos_yx[0] += 1
16     elif direction == 'RIGHT':
17         pos_yx[1] += 1
18     elif direction == 'LEFT':
19         pos_yx[1] -= 1
20
21
22 class Map(game_common.Map):
23
24     def get_line(self, y):
25         width = self.size[1]
26         return self.terrain[y * width:(y + 1) * width]
27
28
29 class World(game_common.World):
30
31     def __init__(self):
32         super().__init__()
33         self.Thing = Thing  # use local Thing class instead of game_common's
34         self.map_ = Map()  # use extended child class
35         self.player_id = 0
36
37     def proceed_to_next_player_turn(self):
38         """Run game world turns until player can decide their next step.
39
40         Iterates through all non-player things, on each step
41         furthering them in their tasks (and letting them decide new
42         ones if they finish). The iteration order is: first all things
43         that come after the player in the world things list, then
44         (after incrementing the world turn) all that come before the
45         player; then the player's .proceed() is run, and if it does
46         not finish his task, the loop starts at the beginning. Once
47         the player's task is finished, the loop breaks.
48         """
49         while True:
50             player = self.get_player()
51             player_i = self.things.index(player)
52             for thing in self.things[player_i+1:]:
53                 thing.proceed()
54             self.turn += 1
55             for thing in self.things[:player_i]:
56                 thing.proceed()
57             player.proceed(is_AI=False)
58             if player.task is None:
59                 break
60
61     def get_player(self):
62         return self.get_thing(self.player_id)
63
64
65 class Task:
66
67     def __init__(self, thing, name, args=(), kwargs={}):
68         self.name = name
69         self.thing = thing
70         self.args = args
71         self.kwargs = kwargs
72         self.todo = 3
73
74     def check(self):
75         if self.name == 'move':
76             if len(self.args) > 0:
77                 direction = self.args[0]
78             else:
79                 direction = self.kwargs['direction']
80             test_pos = self.thing.position[:]
81             move_pos(direction, test_pos)
82             if test_pos[0] < 0 or test_pos[1] < 0 or \
83                test_pos[0] >= self.thing.world.map_.size[0] or \
84                test_pos[1] >= self.thing.world.map_.size[1]:
85                 raise GameError('would move outside map bounds')
86             pos_i = test_pos[0] * self.thing.world.map_.size[1] + test_pos[1]
87             map_tile = self.thing.world.map_.terrain[pos_i]
88             if map_tile != '.':
89                 raise GameError('would move into illegal terrain')
90             for t in self.thing.world.things:
91                 if t.position == test_pos:
92                     raise GameError('would move into other thing')
93
94
95 class Thing(game_common.Thing):
96
97     def __init__(self, *args, **kwargs):
98         super().__init__(*args, **kwargs)
99         self.task = Task(self, 'wait')
100         self.last_task_result = None
101         self._stencil = None
102
103     def task_wait(self):
104         return 'success'
105
106     def task_move(self, direction):
107         move_pos(direction, self.position)
108         return 'success'
109
110     def decide_task(self):
111         if self.position[1] > 1:
112             self.set_task('move', 'LEFT')
113         elif self.position[1] < 3:
114             self.set_task('move', 'RIGHT')
115         else:
116             self.set_task('wait')
117
118     def set_task(self, task_name, *args, **kwargs):
119         self.task = Task(self, task_name, args, kwargs)
120         self.task.check()
121
122     def proceed(self, is_AI=True):
123         """Further the thing in its tasks.
124
125         Decrements .task.todo; if it thus falls to <= 0, enacts method
126         whose name is 'task_' + self.task.name and sets .task =
127         None. If is_AI, calls .decide_task to decide a self.task.
128
129         Before doing anything, ensures an empty map visibility stencil
130         and checks that task is still possible, and aborts it
131         otherwise (for AI things, decides a new task).
132
133         """
134         self._stencil = None
135         try:
136             self.task.check()
137         except GameError as e:
138             self.task = None
139             self.last_task_result = e
140             if is_AI:
141                 self.decide_task()
142             return
143         self.task.todo -= 1
144         if self.task.todo <= 0:
145             task = getattr(self, 'task_' + self.task.name)
146             self.last_task_result = task(*self.task.args, **self.task.kwargs)
147             self.task = None
148         if is_AI and self.task is None:
149             self.decide_task()
150
151     def get_stencil(self):
152         if self._stencil is not None:
153             return self._stencil
154         size = self.world.map_.size
155         m = Map(self.world.map_.size, '?'*size[0]*size[1])
156         y_me = self.position[0]
157         x_me = self.position[1]
158         for y in range(m.size[0]):
159             if y in (y_me - 1, y_me, y_me + 1):
160                 for x in range(m.size[1]):
161                     if x in (x_me - 1, x_me, x_me + 1):
162                         pos = y * size[1] + x
163                         m.terrain = m.terrain[:pos] + '.' + m.terrain[pos+1:]
164         self._stencil = m
165         return self._stencil
166
167     def get_visible_map(self):
168         stencil = self.get_stencil()
169         size = self.world.map_.size
170         size_i = self.world.map_.size[0] * self.world.map_.size[1]
171         m = Map(size, ' '*size_i)
172         for i in range(size_i):
173             if stencil.terrain[i] == '.':
174                 c = self.world.map_.terrain[i]
175                 m.terrain = m.terrain[:i] + c + m.terrain[i+1:]
176         return m
177
178     def get_visible_things(self):
179         stencil = self.get_stencil()
180         visible_things = []
181         for thing in self.world.things:
182             width = self.world.map_.size[1]
183             pos_i = thing.position[0] * width + thing.position[1]
184             if stencil.terrain[pos_i] == '.':
185                 visible_things += [thing]
186         return visible_things
187
188
189 class Commander(game_common.Commander):
190
191     def cmd_MOVE(self, direction):
192         """Set player task to 'move' with direction arg, finish player turn."""
193         if direction not in {'UP', 'DOWN', 'RIGHT', 'LEFT'}:
194             raise parser.ArgError('Move argument must be one of: '
195                                   'UP, DOWN, RIGHT, LEFT')
196         self.world.get_player().set_task('move', direction=direction)
197         self.proceed()
198     cmd_MOVE.argtypes = 'string'
199
200     def cmd_WAIT(self):
201         """Set player task to 'wait', finish player turn."""
202         self.world.get_player().set_task('wait')
203         self.proceed()
204
205     def cmd_GET_GAMESTATE(self, connection_id):
206         """Send game state jto caller."""
207         self.send_gamestate(connection_id)
208
209     def cmd_ECHO(self, msg, connection_id):
210         """Send msg to caller."""
211         self.send(msg, connection_id)
212     cmd_ECHO.argtypes = 'string'
213
214     def cmd_ALL(self, msg, connection_id):
215         """Send msg to all clients."""
216         self.send(msg)
217     cmd_ALL.argtypes = 'string'
218
219     def cmd_TERRAIN_LINE(self, y, terrain_line):
220         self.world.map_.set_line(y, terrain_line)
221     cmd_TERRAIN_LINE.argtypes = 'int:nonneg string'