home · contact · privacy
Refactor.
[plomrogue2-experiments] / server.py
1 #!/usr/bin/env python3
2 import sys
3 import os
4 import parser
5 import server_.game
6 import server_.io
7 import game_common
8
9
10 def fib(n):
11     """Calculate n-th Fibonacci number. Very inefficiently."""
12     if n in (1, 2):
13         return 1
14     else:
15         return fib(n-1) + fib(n-2)
16
17
18 class CommandHandler(server_.game.Commander):
19
20     def __init__(self, game_file_name):
21         self.queues_out = {}
22         self.world = server_.game.World()
23         self.parser = parser.Parser(self)
24         self.game_file_name = game_file_name
25         # self.pool and self.pool_result are currently only needed by the FIB
26         # command and the demo of a parallelized game loop in cmd_inc_p.
27         from multiprocessing import Pool
28         self.pool = Pool()
29         self.pool_result = None
30
31     def quote(self, string):
32         """Quote & escape string so client interprets it as single token."""
33         quoted = []
34         quoted += ['"']
35         for c in string:
36             if c in {'"', '\\'}:
37                 quoted += ['\\']
38             quoted += [c]
39         quoted += ['"']
40         return ''.join(quoted)
41
42     def handle_input(self, input_, connection_id=None, store=True):
43         """Process input_ to command grammar, call command handler if found."""
44         from inspect import signature
45
46         def answer(connection_id, msg):
47             if connection_id:
48                 self.send(msg, connection_id)
49             else:
50                 print(msg)
51
52         try:
53             command = self.parser.parse(input_)
54             if command is None:
55                 answer(connection_id, 'UNHANDLED_INPUT')
56             else:
57                 if 'connection_id' in list(signature(command).parameters):
58                     command(connection_id=connection_id)
59                 else:
60                     command()
61                     if store:
62                         with open(self.game_file_name, 'a') as f:
63                             f.write(input_ + '\n')
64         except parser.ArgError as e:
65             answer(connection_id, 'ARGUMENT_ERROR ' + self.quote(str(e)))
66         except server_.game.GameError as e:
67             answer(connection_id, 'GAME_ERROR ' + self.quote(str(e)))
68
69     def send(self, msg, connection_id=None):
70         if connection_id:
71             self.queues_out[connection_id].put(msg)
72         else:
73             for connection_id in self.queues_out:
74                 self.queues_out[connection_id].put(msg)
75
76     def send_gamestate(self, connection_id=None):
77         """Send out game state data relevant to clients."""
78
79         def stringify_yx(tuple_):
80             """Transform tuple (y,x) into string 'Y:'+str(y)+',X:'+str(x)."""
81             return 'Y:' + str(tuple_[0]) + ',X:' + str(tuple_[1])
82
83         self.send('NEW_TURN ' + str(self.world.turn))
84         self.send('MAP_SIZE ' + stringify_yx(self.world.map_.size))
85         visible_map = self.world.get_player().get_visible_map()
86         for y in range(self.world.map_.size[0]):
87             self.send('VISIBLE_MAP_LINE %5s %s' %
88                       (y, self.quote(visible_map.get_line(y))))
89         visible_things = self.world.get_player().get_visible_things()
90         for thing in visible_things:
91             self.send('THING_TYPE %s %s' % (thing.id_, thing.type_))
92             self.send('THING_POS %s %s' % (thing.id_,
93                                            stringify_yx(thing.position)))
94
95     def proceed(self):
96         """Send turn finish signal, run game world, send new world data.
97
98         First sends 'TURN_FINISHED' message, then runs game world
99         until new player input is needed, then sends game state.
100         """
101         self.send('TURN_FINISHED ' + str(self.world.turn))
102         self.world.proceed_to_next_player_turn()
103         msg = str(self.world.get_player().last_task_result)
104         self.send('LAST_PLAYER_TASK_RESULT ' + self.quote(msg))
105         self.send_gamestate()
106
107     def cmd_FIB(self, numbers, connection_id):
108         """Reply with n-th Fibonacci numbers, n taken from tokens[1:].
109
110         Numbers are calculated in parallel as far as possible, using fib().
111         A 'CALCULATING …' message is sent to caller before the result.
112         """
113         self.send('CALCULATING …', connection_id)
114         results = self.pool.map(fib, numbers)
115         reply = ' '.join([str(r) for r in results])
116         self.send(reply, connection_id)
117     cmd_FIB.argtypes = 'seq:int:nonneg'
118
119     def cmd_INC_P(self, connection_id):
120         """Increment world.turn, send game turn data to everyone.
121
122         To simulate game processing waiting times, a one second delay between
123         TURN_FINISHED and NEW_TURN occurs; after NEW_TURN, some expensive
124         calculations are started as pool processes that need to be finished
125         until a further INC finishes the turn.
126
127         This is just a demo structure for how the game loop could work when
128         parallelized. One might imagine a two-step game turn, with a non-action
129         step determining actor tasks (the AI determinations would take the
130         place of the fib calculations here), and an action step wherein these
131         tasks are performed (where now sleep(1) is).
132         """
133         from time import sleep
134         if self.pool_result is not None:
135             self.pool_result.wait()
136         self.send('TURN_FINISHED ' + str(self.world.turn))
137         sleep(1)
138         self.world.turn += 1
139         self.send_gamestate()
140         self.pool_result = self.pool.map_async(fib, (35, 35))
141
142
143 if len(sys.argv) != 2:
144     print('wrong number of arguments, expected one (game file)')
145     exit(1)
146 game_file_name = sys.argv[1]
147 command_handler = CommandHandler(game_file_name)
148 if os.path.exists(game_file_name):
149     if not os.path.isfile(game_file_name):
150         print('game file name does not refer to a valid game file')
151     else:
152         with open(game_file_name, 'r') as f:
153             lines = f.readlines()
154         for i in range(len(lines)):
155             line = lines[i]
156             print("FILE INPUT LINE %s: %s" % (i, line), end='')
157             command_handler.handle_input(line, store=False)
158 else:
159     command_handler.handle_input('MAP_SIZE Y:5,X:5')
160     command_handler.handle_input('TERRAIN_LINE 0 "xxxxx"')
161     command_handler.handle_input('TERRAIN_LINE 1 "x...x"')
162     command_handler.handle_input('TERRAIN_LINE 2 "x.X.x"')
163     command_handler.handle_input('TERRAIN_LINE 3 "x...x"')
164     command_handler.handle_input('TERRAIN_LINE 4 "xxxxx"')
165     command_handler.handle_input('THING_TYPE 0 human')
166     command_handler.handle_input('THING_POS 0 Y:3,X:3')
167     command_handler.handle_input('THING_TYPE 1 monster')
168     command_handler.handle_input('THING_POS 1 Y:1,X:1')
169
170
171 server_.io.run_server_with_io_loop(command_handler)