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