/
server.py
250 lines (208 loc) · 7.92 KB
/
server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
"""
Tron Battle server.
"""
import os
import random
import time
import datetime
import threading
from subprocess import Popen, PIPE
from grid import TronGrid
from player import PlayerInfo
class StreamReader(threading.Thread):
"""Thread that reads a stream."""
def __init__(self, stream):
threading.Thread.__init__(self)
self.stream = stream
self.returned_lines = 0
self.lines = []
def yield_lines(self):
"""Yield the lines that have been read but not returned yet."""
while self.returned_lines < len(self.lines):
yield self.lines[self.returned_lines]
self.returned_lines += 1
def run(self):
for line in self.stream.readlines():
self.lines.append(line)
class PlayerProgram(PlayerInfo):
"""Controller of a player program."""
def __init__(self, number, title, command, server_log):
PlayerInfo.__init__(self, number)
self.steps = 0
self.total_step_time = 0
self.max_step_time = 0
self.title = title
self.msg = ''
self.server_log = server_log
self.points = []
self.process = Popen(command, shell=True, stdin=PIPE, stdout=PIPE,
stderr=PIPE)
self.stderr_reader = StreamReader(self.process.stderr)
self.stderr_reader.start()
def log(self, msg):
"""Log the message (to the server log)."""
self.server_log('[Player {}:{}] {}'.format(self.number,
self.title, msg))
def get_move(self, index):
"""Return the move with a given number as a string.
:return: "UP", "DOWN", "LEFT", "RIGHT" or None.
"""
try:
if index >= 0:
last = self.points[index + 1]
prev = self.points[index]
else:
last = self.points[index]
prev = self.points[index - 1]
except IndexError:
return None
last, prev = map(lambda pt: TronGrid.coords2index(*pt), [last, prev])
offset = last - prev
r_dir = {off: dir for dir, off in TronGrid.DIRECTIONS.items()}
return r_dir.get(offset, None)
@property
def avg_step_time(self):
return self.total_step_time / self.steps
def move(self, x0, y0, x1, y1):
"""Add logging to moving."""
PlayerInfo.move(self, x0, y0, x1, y1)
if self.points:
if self.points[-1] != (x1, y1):
self.points.append((x1, y1))
else:
if (x0, y0) == (x1, y1):
self.points = [(x1, y1)]
else:
self.points = [(x0, y0), (x1, y1)]
self.log('New coordinates: {} {} {} {}'.format(x0, y0, x1, y1))
def send_numbers(self, *numbers):
"""Send space-separated line of numbers to the program."""
to_send = ' '.join(map(str, numbers))
try:
self.process.stdin.write(to_send + '\n')
self.process.stdin.flush()
self.log('Sent {}'.format(to_send))
except IOError, err:
self.log('Error while sending: {}'.format(err))
def send_game_info(self, player_count):
"""Send the opening line: player count and player number."""
self.send_numbers(player_count, self.number)
def send_player_coords(self, player):
"""Send the coordinates of the player."""
self.send_numbers(*player.coords)
def receive_command(self):
"""Receive one line from the program."""
start = time.time()
got = self.process.stdout.readline().strip()
self.msg = got
end = time.time()
step_time = (end - start) * 1000.0
self.steps += 1
self.total_step_time += step_time
if step_time > self.max_step_time:
self.max_step_time = step_time
self.log('Received {} (in {} ms)'.format(got, step_time))
self.check_stderr()
return got
def check_stderr(self):
"""Check program stderr and return anything that got there."""
for line in list(self.stderr_reader.yield_lines()):
if line.endswith('\n'):
line = line[:-1]
self.log('[ERR] ' + line)
def die(self, msg):
"""Terminate this player."""
self.log('Died: {}'.format(msg))
self.is_alive = False
self.process.terminate()
self.process = None
class TronServer(object):
def __init__(self):
self.players = {}
self.open_log_file()
self.reset()
random.seed(time.time())
def open_log_file(self):
"""Open the log file."""
self.log_filename = os.path.join(os.getcwd(),
time.strftime('tron-log-%Y%m%d%H%M%S'))
self.log_fp = open(self.log_filename, 'at')
self.log('Opened log')
def reset(self):
"""Reset the game."""
if self.players:
for player in self.alive_players:
player.die('Game finished.')
self.grid = TronGrid()
self.player_count = 0
self.turn_count = 0
self.players = {}
self.log('Initialized battle field.')
def log(self, msg):
"""Write the message to the log."""
timestamp = datetime.datetime.now().time().isoformat()
self.log_fp.write('[{}] {}\n'.format(timestamp, msg))
# processing of special commands
if '@DUMP' in msg:
self.dump_grid()
def find_empty_spot(self):
"""Find an empty point in the field."""
while 1:
x = random.randrange(30)
y = random.randrange(20)
if self.grid.get(x, y) == 0:
return x, y
def add_player(self, title, command):
"""Launch a player and add to the game."""
index = self.player_count
self.player_count += 1
player = self.players[index] = \
PlayerProgram(index, title, command, self.log)
self.log('Added player {} as {} ({})'.format(title, index, command))
x, y = self.find_empty_spot()
player.move(x, y, x, y)
def dump_grid(self):
"""Dump the playing field to the log file."""
self.log('Dumping the grid')
for line in str(self.grid).split('\n'):
self.log(line)
def kill_player(self, player, msg):
"""Declare the player dead and remove from the field."""
player.die(msg)
self.dump_grid()
self.grid.replace(self.grid.body_of(player.number), 0)
self.grid.replace(self.grid.head_of(player.number), 0)
def play_player_turn(self, player):
"""Play the turn of one player."""
player.send_game_info(self.player_count)
map(player.send_player_coords, self.players.values())
cmd = player.receive_command()
pos = self.grid.coords2index(*player.head)
move = self.grid.DIRECTIONS.get(cmd, None)
if move:
new_pos = pos + move
if self.grid[new_pos] != 0:
self.kill_player(player, '{} is an illegal move.'.format(cmd))
else:
self.grid[pos] = self.grid.body_of(player.number)
self.grid[new_pos] = self.grid.head_of(player.number)
player.move(*(player.tail + self.grid.index2coords(new_pos)))
else:
self.kill_player(player, 'Invalid command: {}.'.format(cmd))
@property
def alive_players(self):
"""Return the list of alive players."""
return filter(lambda p: p.is_alive, self.players.values())
@property
def players_list(self):
"""Return the players as a list."""
return [self.players[i] for i in xrange(len(self.players))]
def play_turn(self):
"""Play one turn for all alive players."""
self.turn_count += 1
self.log('Starting turn {}'.format(self.turn_count))
for player in self.alive_players:
self.play_player_turn(player)
for player in self.players_list:
player.check_stderr()
self.log('Completed turn {}'.format(self.turn_count))