6 from plomrogue.io_tcp import PlomSocket
7 from plomrogue.game import GameBase
8 from plomrogue.parser import Parser
9 from plomrogue.mapping import YX
10 from plomrogue.things import ThingBase
11 from plomrogue.misc import quote
13 def cmd_TURN(game, n):
17 game.turn_complete = False
18 cmd_TURN.argtypes = 'int:nonneg'
20 def cmd_LOGIN_OK(game):
21 game.tui.switch_mode('post_login_wait')
22 game.tui.send('GET_GAMESTATE')
23 game.tui.log_msg('@ welcome')
24 cmd_LOGIN_OK.argtypes = ''
26 def cmd_CHAT(game, msg):
27 game.tui.log_msg('# ' + msg)
28 game.tui.do_refresh = True
29 cmd_CHAT.argtypes = 'string'
31 def cmd_PLAYER_ID(game, player_id):
32 game.player_id = player_id
33 cmd_PLAYER_ID.argtypes = 'int:nonneg'
35 def cmd_THING_POS(game, thing_id, position):
36 t = game.get_thing(thing_id, True)
38 cmd_THING_POS.argtypes = 'int:nonneg yx_tuple:nonneg'
40 def cmd_THING_NAME(game, thing_id, name):
41 t = game.get_thing(thing_id, True)
43 cmd_THING_NAME.argtypes = 'int:nonneg string'
45 def cmd_MAP(game, size, content):
46 game.map_geometry.size = size
47 game.map_content = content
48 cmd_MAP.argtypes = 'yx_tuple:pos string'
50 def cmd_GAME_STATE_COMPLETE(game):
52 if game.tui.mode.name == 'post_login_wait':
53 game.tui.switch_mode('play')
55 if game.tui.mode.shows_info:
57 player = game.get_thing(game.player_id, False)
58 if player.position in game.portals:
59 host, port = game.portals[player.position].split(':')
60 game.tui.teleport_target_host = host
61 game.tui.teleport_target_port = port
62 game.tui.switch_mode('teleport')
63 game.turn_complete = True
64 game.tui.do_refresh = True
65 cmd_GAME_STATE_COMPLETE.argtypes = ''
67 def cmd_PORTAL(game, position, msg):
68 game.portals[position] = msg
69 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
71 def cmd_PLAY_ERROR(game, msg):
73 game.tui.do_refresh = True
74 cmd_PLAY_ERROR.argtypes = 'string'
76 def cmd_GAME_ERROR(game, msg):
77 game.tui.log_msg('? game error: ' + msg)
78 game.tui.do_refresh = True
79 cmd_GAME_ERROR.argtypes = 'string'
81 def cmd_ARGUMENT_ERROR(game, msg):
82 game.tui.log_msg('? syntax error: ' + msg)
83 game.tui.do_refresh = True
84 cmd_ARGUMENT_ERROR.argtypes = 'string'
86 def cmd_ANNOTATION(game, position, msg):
87 game.info_db[position] = msg
88 if game.tui.mode.shows_info:
89 game.tui.do_refresh = True
90 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
93 commands = {'LOGIN_OK': cmd_LOGIN_OK,
95 'PLAYER_ID': cmd_PLAYER_ID,
97 'THING_POS': cmd_THING_POS,
98 'THING_NAME': cmd_THING_NAME,
100 'PORTAL': cmd_PORTAL,
101 'ANNOTATION': cmd_ANNOTATION,
102 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
103 'ARGUMENT_ERROR': cmd_ARGUMENT_ERROR,
104 'GAME_ERROR': cmd_GAME_ERROR,
105 'PLAY_ERROR': cmd_PLAY_ERROR}
106 thing_type = ThingBase
107 turn_complete = False
109 def __init__(self, *args, **kwargs):
110 super().__init__(*args, **kwargs)
111 self.map_content = ''
116 def get_command(self, command_name):
117 from functools import partial
118 f = partial(self.commands[command_name], self)
119 f.argtypes = self.commands[command_name].argtypes
126 def __init__(self, name, has_input_prompt=False, shows_info=False,
129 self.has_input_prompt = has_input_prompt
130 self.shows_info = shows_info
131 self.is_intro = is_intro
133 def __init__(self, host, port):
136 self.mode_play = self.Mode('play')
137 self.mode_study = self.Mode('study', shows_info=True)
138 self.mode_edit = self.Mode('edit')
139 self.mode_annotate = self.Mode('annotate', has_input_prompt=True, shows_info=True)
140 self.mode_portal = self.Mode('portal', has_input_prompt=True, shows_info=True)
141 self.mode_chat = self.Mode('chat', has_input_prompt=True)
142 self.mode_waiting_for_server = self.Mode('waiting_for_server', is_intro=True)
143 self.mode_login = self.Mode('login', has_input_prompt=True, is_intro=True)
144 self.mode_post_login_wait = self.Mode('post_login_wait', is_intro=True)
145 self.mode_teleport = self.Mode('teleport', has_input_prompt=True)
148 self.parser = Parser(self.game)
150 self.do_refresh = True
151 self.queue = queue.Queue()
152 self.switch_mode('waiting_for_server')
153 curses.wrapper(self.loop)
160 self.socket.send(msg)
161 except BrokenPipeError:
162 self.log_msg('@ server disconnected :(')
163 self.do_refresh = True
165 def log_msg(self, msg):
167 if len(self.log) > 100:
168 self.log = self.log[-100:]
170 def query_info(self):
171 self.send('GET_ANNOTATION ' + str(self.explorer))
173 def switch_mode(self, mode_name, keep_position = False):
174 self.mode = getattr(self, 'mode_' + mode_name)
175 if self.mode.shows_info and not keep_position:
176 player = self.game.get_thing(self.game.player_id, False)
177 self.explorer = YX(player.position.y, player.position.x)
178 if self.mode.name == 'waiting_for_server':
179 self.log_msg('@ waiting for server …')
180 elif self.mode.name == 'login':
181 self.log_msg('@ enter username')
182 elif self.mode.name == 'teleport':
183 self.log_msg("@ May teleport to %s:%s" % (self.teleport_target_host,
184 self.teleport_target_port));
185 self.log_msg("@ Enter 'YES!' to affirm.");
186 elif self.mode.name == 'annotate' and self.explorer in self.game.info_db:
187 info = self.game.info_db[self.explorer]
190 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
191 self.input_ = self.game.portals[self.explorer]
194 self.log_msg("HELP:");
195 self.log_msg("chat mode commands:");
196 self.log_msg(" :nick NAME - re-name yourself to NAME");
197 self.log_msg(" :msg USER TEXT - send TEXT to USER");
198 self.log_msg(" :help - show this help");
199 self.log_msg(" :p or :play - switch to play mode");
200 self.log_msg(" :? or :study - switch to study mode");
201 self.log_msg("commands common to study and play mode:");
202 self.log_msg(" w,a,s,d - move");
203 self.log_msg(" c - switch to chat mode");
204 self.log_msg("commands specific to play mode:");
205 self.log_msg(" e - write following ASCII character");
206 self.log_msg(" f - flatten surroundings");
207 self.log_msg(" ? - switch to study mode");
208 self.log_msg("commands specific to study mode:");
209 self.log_msg(" e - annotate terrain");
210 self.log_msg(" p - switch to play mode");
212 def loop(self, stdscr):
214 def safe_addstr(y, x, line):
215 if y < self.size.y - 1 or x + len(line) < self.size.x:
216 stdscr.addstr(y, x, line)
217 else: # workaround to <https://stackoverflow.com/q/7063128>
218 cut_i = self.size.x - x - 1
220 last_char = line[cut_i]
221 stdscr.addstr(y, self.size.x - 2, last_char)
222 stdscr.insstr(y, self.size.x - 2, ' ')
223 stdscr.addstr(y, x, cut)
229 for msg in self.socket.recv():
236 s = socket.create_connection((self.host, self.port))
237 self.socket = PlomSocket(s)
238 self.socket_thread = threading.Thread(target=recv_loop)
239 self.socket_thread.start()
240 self.switch_mode('login')
242 except ConnectionRefusedError:
243 self.log_msg('@ server connect failure, trying again …')
250 self.switch_mode('waiting_for_server')
253 def handle_input(msg):
254 command, args = self.parser.parse(msg)
257 def msg_into_lines_of_width(msg, width):
261 for i in range(len(msg)):
262 if x >= width or msg[i] == "\n":
272 def reset_screen_size():
273 self.size = YX(*stdscr.getmaxyx())
274 self.size = self.size - YX(self.size.y % 2, 0)
275 self.size = self.size - YX(0, self.size.x % 4)
276 self.window_width = int(self.size.x / 2)
278 def recalc_input_lines():
279 if not self.mode.has_input_prompt:
280 self.input_lines = []
282 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
285 def move_explorer(direction):
286 target = self.game.map_geometry.move(self.explorer, direction)
288 self.explorer = target
295 for line in self.log:
296 lines += msg_into_lines_of_width(line, self.window_width)
299 max_y = self.size.y - len(self.input_lines)
300 for i in range(len(lines)):
301 if (i >= max_y - height_header):
303 safe_addstr(max_y - i - 1, self.window_width, lines[i])
306 if not self.game.turn_complete:
308 if self.explorer in self.game.portals:
309 info = 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
311 info = 'PORTAL: (none)\n'
312 if self.explorer in self.game.info_db:
313 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
315 info += 'ANNOTATION: waiting …'
316 lines = msg_into_lines_of_width(info, self.window_width)
318 for i in range(len(lines)):
319 y = height_header + i
320 if y >= self.size.y - len(self.input_lines):
322 safe_addstr(y, self.window_width, lines[i])
325 y = self.size.y - len(self.input_lines)
326 for i in range(len(self.input_lines)):
327 safe_addstr(y, self.window_width, self.input_lines[i])
331 if not self.game.turn_complete:
333 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
336 safe_addstr(1, self.window_width, 'MODE: ' + self.mode.name)
339 if not self.game.turn_complete:
342 for y in range(self.game.map_geometry.size.y):
343 start = self.game.map_geometry.size.x * y
344 end = start + self.game.map_geometry.size.x
345 map_lines_split += [list(self.game.map_content[start:end])]
346 for t in self.game.things:
347 map_lines_split[t.position.y][t.position.x] = '@'
348 if self.mode.shows_info:
349 map_lines_split[self.explorer.y][self.explorer.x] = '?'
351 for line in map_lines_split:
352 map_lines += [''.join(line)]
353 map_center = YX(int(self.game.map_geometry.size.y / 2),
354 int(self.game.map_geometry.size.x / 2))
355 window_center = YX(int(self.size.y / 2),
356 int(self.window_width / 2))
357 player = self.game.get_thing(self.game.player_id, False)
358 center = player.position
359 if self.mode.shows_info:
360 center = self.explorer
361 offset = center - window_center
362 term_y = max(0, -offset.y)
363 term_x = max(0, -offset.x)
364 map_y = max(0, offset.y)
365 map_x = max(0, offset.x)
366 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
367 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
368 safe_addstr(term_y, term_x, to_draw)
375 if self.mode.has_input_prompt:
377 if self.mode.shows_info:
382 if not self.mode.is_intro:
386 curses.curs_set(False) # hide cursor
389 self.explorer = YX(0, 0)
396 self.do_refresh = False
399 msg = self.queue.get(block=False)
404 key = stdscr.getkey()
405 self.do_refresh = True
408 if key == 'KEY_RESIZE':
410 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
411 self.input_ = self.input_[:-1]
412 elif self.mode.has_input_prompt and key != '\n': # Return key
414 max_length = self.window_width * self.size.y - len(input_prompt) - 1
415 if len(self.input_) > max_length:
416 self.input_ = self.input_[:max_length]
417 elif self.mode == self.mode_login and key == '\n':
418 self.send('LOGIN ' + quote(self.input_))
420 elif self.mode == self.mode_chat and key == '\n':
421 if self.input_[0] == ':':
422 if self.input_ in {':p', ':play'}:
423 self.switch_mode('play')
424 elif self.input_ in {':?', ':study'}:
425 self.switch_mode('study')
426 if self.input_ == ':help':
428 if self.input_ == ':reconnect':
430 elif self.input_.startswith(':nick'):
431 tokens = self.input_.split(maxsplit=1)
433 self.send('LOGIN ' + quote(tokens[1]))
435 self.log_msg('? need login name')
436 elif self.input_.startswith(':msg'):
437 tokens = self.input_.split(maxsplit=2)
439 self.send('QUERY %s %s' % (quote(tokens[1]),
442 self.log_msg('? need message target and message')
444 self.log_msg('? unknown command')
446 self.send('ALL ' + quote(self.input_))
448 elif self.mode == self.mode_annotate and key == '\n':
449 if self.input_ == '':
451 self.send('ANNOTATE %s %s' % (self.explorer, quote(self.input_)))
453 self.switch_mode('study', keep_position=True)
454 elif self.mode == self.mode_portal and key == '\n':
455 if self.input_ == '':
457 self.send('PORTAL %s %s' % (self.explorer, quote(self.input_)))
459 self.switch_mode('study', keep_position=True)
460 elif self.mode == self.mode_teleport and key == '\n':
461 if self.input_ == 'YES!':
462 self.host = self.teleport_target_host
463 self.port = self.teleport_target_port
466 self.log_msg('@ teleport aborted')
467 self.switch_mode('play')
469 elif self.mode == self.mode_study:
471 self.switch_mode('chat')
473 self.switch_mode('play')
475 self.switch_mode('annotate', keep_position=True)
477 self.switch_mode('portal', keep_position=True)
481 move_explorer('LEFT')
483 move_explorer('DOWN')
485 move_explorer('RIGHT')
486 elif self.mode == self.mode_play:
488 self.switch_mode('chat')
490 self.switch_mode('study')
492 self.switch_mode('edit')
494 self.send('TASK:FLATTEN_SURROUNDINGS')
496 self.send('TASK:MOVE UP')
498 self.send('TASK:MOVE LEFT')
500 self.send('TASK:MOVE DOWN')
502 self.send('TASK:MOVE RIGHT')
503 elif self.mode == self.mode_edit:
504 self.send('TASK:WRITE ' + key)
505 self.switch_mode('play')
507 TUI('127.0.0.1', 5000)