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, MapGeometrySquare, MapGeometryHex
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, geometry, size, content):
46 map_geometry_class = globals()['MapGeometry' + geometry]
47 game.map_geometry = map_geometry_class(size)
48 game.map_content = content
49 if type(game.map_geometry) == MapGeometrySquare:
50 game.tui.movement_keys = {
51 game.tui.keys['square_move_up']: 'UP',
52 game.tui.keys['square_move_left']: 'LEFT',
53 game.tui.keys['square_move_down']: 'DOWN',
54 game.tui.keys['square_move_right']: 'RIGHT',
56 elif type(game.map_geometry) == MapGeometryHex:
57 game.tui.movement_keys = {
58 game.tui.keys['hex_move_upleft']: 'UPLEFT',
59 game.tui.keys['hex_move_upright']: 'UPRIGHT',
60 game.tui.keys['hex_move_right']: 'RIGHT',
61 game.tui.keys['hex_move_downright']: 'DOWNRIGHT',
62 game.tui.keys['hex_move_downleft']: 'DOWNLEFT',
63 game.tui.keys['hex_move_left']: 'LEFT',
65 cmd_MAP.argtypes = 'string:map_geometry yx_tuple:pos string'
67 def cmd_GAME_STATE_COMPLETE(game):
69 if game.tui.mode.name == 'post_login_wait':
70 game.tui.switch_mode('play')
72 if game.tui.mode.shows_info:
74 player = game.get_thing(game.player_id, False)
75 if player.position in game.portals:
76 #host, port = game.portals[player.position].split(':')
77 game.tui.teleport_target_host = game.portals[player.position]
78 game.tui.teleport_target_port = 5000
79 game.tui.switch_mode('teleport')
80 game.turn_complete = True
81 game.tui.do_refresh = True
82 cmd_GAME_STATE_COMPLETE.argtypes = ''
84 def cmd_PORTAL(game, position, msg):
85 game.portals[position] = msg
86 cmd_PORTAL.argtypes = 'yx_tuple:nonneg string'
88 def cmd_PLAY_ERROR(game, msg):
90 game.tui.do_refresh = True
91 cmd_PLAY_ERROR.argtypes = 'string'
93 def cmd_GAME_ERROR(game, msg):
94 game.tui.log_msg('? game error: ' + msg)
95 game.tui.do_refresh = True
96 cmd_GAME_ERROR.argtypes = 'string'
98 def cmd_ARGUMENT_ERROR(game, msg):
99 game.tui.log_msg('? syntax error: ' + msg)
100 game.tui.do_refresh = True
101 cmd_ARGUMENT_ERROR.argtypes = 'string'
103 def cmd_ANNOTATION(game, position, msg):
104 game.info_db[position] = msg
105 if game.tui.mode.shows_info:
106 game.tui.do_refresh = True
107 cmd_ANNOTATION.argtypes = 'yx_tuple:nonneg string'
109 class Game(GameBase):
110 commands = {'LOGIN_OK': cmd_LOGIN_OK,
112 'PLAYER_ID': cmd_PLAYER_ID,
114 'THING_POS': cmd_THING_POS,
115 'THING_NAME': cmd_THING_NAME,
117 'PORTAL': cmd_PORTAL,
118 'ANNOTATION': cmd_ANNOTATION,
119 'GAME_STATE_COMPLETE': cmd_GAME_STATE_COMPLETE,
120 'ARGUMENT_ERROR': cmd_ARGUMENT_ERROR,
121 'GAME_ERROR': cmd_GAME_ERROR,
122 'PLAY_ERROR': cmd_PLAY_ERROR}
123 thing_type = ThingBase
124 turn_complete = False
126 def __init__(self, *args, **kwargs):
127 super().__init__(*args, **kwargs)
128 self.map_content = ''
133 def get_string_options(self, string_option_type):
134 if string_option_type == 'map_geometry':
135 return ['Hex', 'Square']
138 def get_command(self, command_name):
139 from functools import partial
140 f = partial(self.commands[command_name], self)
141 f.argtypes = self.commands[command_name].argtypes
148 def __init__(self, name, has_input_prompt=False, shows_info=False,
151 self.has_input_prompt = has_input_prompt
152 self.shows_info = shows_info
153 self.is_intro = is_intro
155 def __init__(self, host, port):
160 self.mode_play = self.Mode('play')
161 self.mode_study = self.Mode('study', shows_info=True)
162 self.mode_edit = self.Mode('edit')
163 self.mode_annotate = self.Mode('annotate', has_input_prompt=True, shows_info=True)
164 self.mode_portal = self.Mode('portal', has_input_prompt=True, shows_info=True)
165 self.mode_chat = self.Mode('chat', has_input_prompt=True)
166 self.mode_waiting_for_server = self.Mode('waiting_for_server', is_intro=True)
167 self.mode_login = self.Mode('login', has_input_prompt=True, is_intro=True)
168 self.mode_post_login_wait = self.Mode('post_login_wait', is_intro=True)
169 self.mode_teleport = self.Mode('teleport', has_input_prompt=True)
172 self.parser = Parser(self.game)
174 self.do_refresh = True
175 self.queue = queue.Queue()
176 self.login_name = None
177 self.switch_mode('waiting_for_server')
179 'switch_to_chat': 'C',
180 'switch_to_play': 'P',
181 'switch_to_annotate': 'E',
182 'switch_to_portal': 'p',
183 'switch_to_study': '?',
184 'switch_to_edit': 'E',
186 'hex_move_upleft': 'w',
187 'hex_move_upright': 'e',
188 'hex_move_right': 'd',
189 'hex_move_downright': 'c',
190 'hex_move_downleft': 'x',
191 'hex_move_left': 's',
192 'square_move_up': 'w',
193 'square_move_left': 'a',
194 'square_move_down': 's',
195 'square_move_right': 'd',
197 if os.path.isfile('config.json'):
198 with open('config.json', 'r') as f:
199 keys_conf = json.loads(f.read())
201 self.keys[k] = keys_conf[k]
202 curses.wrapper(self.loop)
209 self.socket.send(msg)
210 except BrokenPipeError:
211 self.log_msg('@ server disconnected :(')
212 self.do_refresh = True
214 def log_msg(self, msg):
216 if len(self.log) > 100:
217 self.log = self.log[-100:]
219 def query_info(self):
220 self.send('GET_ANNOTATION ' + str(self.explorer))
222 def switch_mode(self, mode_name, keep_position = False):
223 self.mode = getattr(self, 'mode_' + mode_name)
224 if self.mode.shows_info and not keep_position:
225 player = self.game.get_thing(self.game.player_id, False)
226 self.explorer = YX(player.position.y, player.position.x)
227 if self.mode.name == 'waiting_for_server':
228 self.log_msg('@ waiting for server …')
229 elif self.mode.name == 'login':
231 self.send('LOGIN ' + quote(self.login_name))
233 self.log_msg('@ enter username')
234 elif self.mode.name == 'teleport':
235 self.log_msg("@ May teleport to %s:%s" % (self.teleport_target_host,
236 self.teleport_target_port));
237 self.log_msg("@ Enter 'YES!' to enthusiastically affirm.");
238 elif self.mode.name == 'annotate' and self.explorer in self.game.info_db:
239 info = self.game.info_db[self.explorer]
242 elif self.mode.name == 'portal' and self.explorer in self.game.portals:
243 self.input_ = self.game.portals[self.explorer]
246 self.log_msg("HELP:");
247 self.log_msg("chat mode commands:");
248 self.log_msg(" /nick NAME - re-name yourself to NAME");
249 self.log_msg(" /msg USER TEXT - send TEXT to USER");
250 self.log_msg(" /help - show this help");
251 self.log_msg(" /P or /play - switch to play mode");
252 self.log_msg(" /? or /study - switch to study mode");
253 self.log_msg("commands common to study and play mode:");
254 self.log_msg(" %s - move" % ','.join(self.movement_keys));
255 self.log_msg(" %s - switch to chat mode" % self.keys['switch_to_chat']);
256 self.log_msg("commands specific to play mode:");
257 self.log_msg(" %s - write following ASCII character" % self.keys['switch_to_edit']);
258 self.log_msg(" %s - flatten surroundings" % self.keys['flatten']);
259 self.log_msg(" %s - switch to study mode" % self.keys['switch_to_study']);
260 self.log_msg("commands specific to study mode:");
261 self.log_msg(" %s - annotate terrain" % self.keys['switch_to_annotate']);
262 self.log_msg(" %s - switch to play mode" % self.keys['switch_to_play']);
264 def loop(self, stdscr):
266 def safe_addstr(y, x, line):
267 if y < self.size.y - 1 or x + len(line) < self.size.x:
268 stdscr.addstr(y, x, line)
269 else: # workaround to <https://stackoverflow.com/q/7063128>
270 cut_i = self.size.x - x - 1
272 last_char = line[cut_i]
273 stdscr.addstr(y, self.size.x - 2, last_char)
274 stdscr.insstr(y, self.size.x - 2, ' ')
275 stdscr.addstr(y, x, cut)
281 for msg in self.socket.recv():
288 s = socket.create_connection((self.host, self.port))
289 self.socket = PlomSocket(s)
290 self.socket_thread = threading.Thread(target=recv_loop)
291 self.socket_thread.start()
292 self.switch_mode('login')
294 except ConnectionRefusedError:
295 self.log_msg('@ server connect failure, trying again …')
302 self.switch_mode('waiting_for_server')
305 def handle_input(msg):
306 command, args = self.parser.parse(msg)
309 def msg_into_lines_of_width(msg, width):
313 for i in range(len(msg)):
314 if x >= width or msg[i] == "\n":
324 def reset_screen_size():
325 self.size = YX(*stdscr.getmaxyx())
326 self.size = self.size - YX(self.size.y % 4, 0)
327 self.size = self.size - YX(0, self.size.x % 4)
328 self.window_width = int(self.size.x / 2)
330 def recalc_input_lines():
331 if not self.mode.has_input_prompt:
332 self.input_lines = []
334 self.input_lines = msg_into_lines_of_width(input_prompt + self.input_,
337 def move_explorer(direction):
338 target = self.game.map_geometry.move(self.explorer, direction)
340 self.explorer = target
347 for line in self.log:
348 lines += msg_into_lines_of_width(line, self.window_width)
351 max_y = self.size.y - len(self.input_lines)
352 for i in range(len(lines)):
353 if (i >= max_y - height_header):
355 safe_addstr(max_y - i - 1, self.window_width, lines[i])
358 if not self.game.turn_complete:
360 if self.explorer in self.game.portals:
361 info = 'PORTAL: ' + self.game.portals[self.explorer] + '\n'
363 info = 'PORTAL: (none)\n'
364 if self.explorer in self.game.info_db:
365 info += 'ANNOTATION: ' + self.game.info_db[self.explorer]
367 info += 'ANNOTATION: waiting …'
368 lines = msg_into_lines_of_width(info, self.window_width)
370 for i in range(len(lines)):
371 y = height_header + i
372 if y >= self.size.y - len(self.input_lines):
374 safe_addstr(y, self.window_width, lines[i])
377 y = self.size.y - len(self.input_lines)
378 for i in range(len(self.input_lines)):
379 safe_addstr(y, self.window_width, self.input_lines[i])
383 if not self.game.turn_complete:
385 safe_addstr(0, self.window_width, 'TURN: ' + str(self.game.turn))
388 safe_addstr(1, self.window_width, 'MODE: ' + self.mode.name)
391 if not self.game.turn_complete:
394 for y in range(self.game.map_geometry.size.y):
395 start = self.game.map_geometry.size.x * y
396 end = start + self.game.map_geometry.size.x
397 map_lines_split += [list(self.game.map_content[start:end])]
398 for t in self.game.things:
399 map_lines_split[t.position.y][t.position.x] = '@'
400 if self.mode.shows_info:
401 map_lines_split[self.explorer.y][self.explorer.x] = '?'
403 if type(self.game.map_geometry) == MapGeometryHex:
405 for line in map_lines_split:
406 map_lines += [indent*' ' + ' '.join(line)]
407 indent = 0 if indent else 1
409 for line in map_lines_split:
410 map_lines += [' '.join(line)]
411 window_center = YX(int(self.size.y / 2),
412 int(self.window_width / 2))
413 player = self.game.get_thing(self.game.player_id, False)
414 center = player.position
415 if self.mode.shows_info:
416 center = self.explorer
417 center = YX(center.y, center.x * 2)
418 offset = center - window_center
419 if type(self.game.map_geometry) == MapGeometryHex and offset.y % 2:
421 term_y = max(0, -offset.y)
422 term_x = max(0, -offset.x)
423 map_y = max(0, offset.y)
424 map_x = max(0, offset.x)
425 while (term_y < self.size.y and map_y < self.game.map_geometry.size.y):
426 to_draw = map_lines[map_y][map_x:self.window_width + offset.x]
427 safe_addstr(term_y, term_x, to_draw)
434 if self.mode.has_input_prompt:
436 if self.mode.shows_info:
441 if not self.mode.is_intro:
445 curses.curs_set(False) # hide cursor
448 self.explorer = YX(0, 0)
455 self.do_refresh = False
458 msg = self.queue.get(block=False)
463 key = stdscr.getkey()
464 self.do_refresh = True
467 if key == 'KEY_RESIZE':
469 elif self.mode.has_input_prompt and key == 'KEY_BACKSPACE':
470 self.input_ = self.input_[:-1]
471 elif self.mode.has_input_prompt and key != '\n': # Return key
473 max_length = self.window_width * self.size.y - len(input_prompt) - 1
474 if len(self.input_) > max_length:
475 self.input_ = self.input_[:max_length]
476 elif self.mode == self.mode_login and key == '\n':
477 self.login_name = self.input_
478 self.send('LOGIN ' + quote(self.input_))
480 elif self.mode == self.mode_chat and key == '\n':
481 if self.input_[0] == '/':
482 if self.input_ in {'/P', '/play'}:
483 self.switch_mode('play')
484 elif self.input_ in {'/?', '/study'}:
485 self.switch_mode('study')
486 elif self.input_ == '/help':
488 elif self.input_ == '/reconnect':
490 elif self.input_.startswith('/nick'):
491 tokens = self.input_.split(maxsplit=1)
493 self.send('LOGIN ' + quote(tokens[1]))
495 self.log_msg('? need login name')
496 elif self.input_.startswith('/msg'):
497 tokens = self.input_.split(maxsplit=2)
499 self.send('QUERY %s %s' % (quote(tokens[1]),
502 self.log_msg('? need message target and message')
504 self.log_msg('? unknown command')
506 self.send('ALL ' + quote(self.input_))
508 elif self.mode == self.mode_annotate and key == '\n':
509 if self.input_ == '':
511 self.send('ANNOTATE %s %s' % (self.explorer, quote(self.input_)))
513 self.switch_mode('study', keep_position=True)
514 elif self.mode == self.mode_portal and key == '\n':
515 if self.input_ == '':
517 self.send('PORTAL %s %s' % (self.explorer, quote(self.input_)))
519 self.switch_mode('study', keep_position=True)
520 elif self.mode == self.mode_teleport and key == '\n':
521 if self.input_ == 'YES!':
522 self.host = self.teleport_target_host
523 self.port = self.teleport_target_port
526 self.log_msg('@ teleport aborted')
527 self.switch_mode('play')
529 elif self.mode == self.mode_study:
530 if key == self.keys['switch_to_chat']:
531 self.switch_mode('chat')
532 elif key == self.keys['switch_to_play']:
533 self.switch_mode('play')
534 elif key == self.keys['switch_to_annotate']:
535 self.switch_mode('annotate', keep_position=True)
536 elif key == self.keys['switch_to_portal']:
537 self.switch_mode('portal', keep_position=True)
538 elif key in self.movement_keys:
539 move_explorer(self.movement_keys[key])
540 elif self.mode == self.mode_play:
541 if key == self.keys['switch_to_chat']:
542 self.switch_mode('chat')
543 elif key == self.keys['switch_to_study']:
544 self.switch_mode('study')
545 if key == self.keys['switch_to_edit']:
546 self.switch_mode('edit')
547 elif key == self.keys['flatten']:
548 self.send('TASK:FLATTEN_SURROUNDINGS')
549 elif key in self.movement_keys:
550 self.send('TASK:MOVE ' + self.movement_keys[key])
551 elif self.mode == self.mode_edit:
552 self.send('TASK:WRITE ' + key)
553 self.switch_mode('play')
555 TUI('127.0.0.1', 5000)