home · contact · privacy
Server/py: Add map navigation infrastructure / API to libplomrogue.c.
[plomrogue] / plomrogue-server.py
1 import argparse
2 import errno
3 import os
4 import shlex
5 import shutil
6 import time
7 import ctypes
8
9
10 class RandomnessIO:
11     """"Interface to libplomrogue's pseudo-randomness generator."""
12
13     def set_seed(self, seed):
14         libpr.seed_rrand(1, seed)
15
16     def get_seed(self):
17         return libpr.seed_rrand(0, 0)
18
19     def next(self):
20         return libpr.rrand()
21
22     seed = property(get_seed, set_seed)
23
24
25 def prep_library():
26     """Prepare ctypes library at ./libplomrogue.so"""
27     libpath = ("./libplomrogue.so")
28     if not os.access(libpath, os.F_OK):
29         raise SystemExit("No library " + libpath + ", run ./compile.sh first?")
30     libpr = ctypes.cdll.LoadLibrary(libpath)
31     libpr.seed_rrand.argtypes = [ctypes.c_uint8, ctypes.c_uint32]
32     libpr.seed_rrand.restype = ctypes.c_uint32
33     libpr.rrand.argtypes = []
34     libpr.rrand.restype = ctypes.c_uint16
35     libpr.set_maplength.argtypes = [ctypes.c_uint16]
36     libpr.mv_yx_in_dir_legal_wrap.argtypes = [ctypes.c_char, ctypes.c_uint8,
37                                               ctypes.c_uint8]
38     libpr.mv_yx_in_dir_legal_wrap.restype = ctypes.c_uint8
39     return libpr
40
41
42 def strong_write(file, string):
43     """Apply write(string), flush(), and os.fsync() to file."""
44     file.write(string)
45     file.flush()
46     os.fsync(file)
47
48
49 def setup_server_io():
50     """Fill IO files DB with proper file( path)s. Write process IO test string.
51
52     Ensure IO files directory at server/. Remove any old input file if found.
53     Set up new input file for reading, and new output file for writing. Start
54     output file with process hash line of format PID + " " + floated UNIX time
55     (io_db["teststring"]). Raise SystemExit if file is found at path of either
56     record or save file plus io_db["tmp_suffix"].
57     """
58     def detect_atomic_leftover(path, tmp_suffix):
59         path_tmp = path + tmp_suffix
60         msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
61               "aborted previous attempt to write '" + path + "'. Aborting " \
62              "until matter is resolved by removing it from its current path."
63         if os.access(path_tmp, os.F_OK):
64             raise SystemExit(msg)
65     io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
66     os.makedirs(io_db["path_server"], exist_ok=True)
67     io_db["file_out"] = open(io_db["path_out"], "w")
68     strong_write(io_db["file_out"], io_db["teststring"] + "\n")
69     if os.access(io_db["path_in"], os.F_OK):
70         os.remove(io_db["path_in"])
71     io_db["file_in"] = open(io_db["path_in"], "w")
72     io_db["file_in"].close()
73     io_db["file_in"] = open(io_db["path_in"], "r")
74     detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
75     detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
76
77
78 def cleanup_server_io():
79     """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
80     def helper(file_key, path_key):
81         if file_key in io_db:
82             io_db[file_key].close()
83             if not io_db["kicked_by_rival"] \
84                and os.access(io_db[path_key], os.F_OK):
85                 os.remove(io_db[path_key])
86     helper("file_out", "path_out")
87     helper("file_in", "path_in")
88     helper("file_worldstate", "path_worldstate")
89     if "file_record" in io_db:
90         io_db["file_record"].close()
91
92
93 def obey(command, prefix, replay=False, do_record=False):
94     """Call function from commands_db mapped to command's first token.
95
96     Tokenize command string with shlex.split(comments=True). If replay is set,
97     a non-meta command from the commands_db merely triggers obey() on the next
98     command from the records file. If not, non-meta commands set
99     io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
100     do_record is set, are recorded via record(), and save_world() is called.
101     The prefix string is inserted into the server's input message between its
102     beginning 'input ' & ':'. All activity is preceded by a server_test() call.
103     """
104     server_test()
105     print("input " + prefix + ": " + command)
106     try:
107         tokens = shlex.split(command, comments=True)
108     except ValueError as err:
109         print("Can't tokenize command string: " + str(err) + ".")
110         return
111     if len(tokens) > 0 and tokens[0] in commands_db \
112        and len(tokens) == commands_db[tokens[0]][0] + 1:
113         if commands_db[tokens[0]][1]:
114             commands_db[tokens[0]][2](*tokens[1:])
115         elif replay:
116             print("Due to replay mode, reading command as 'go on in record'.")
117             line = io_db["file_record"].readline()
118             if len(line) > 0:
119                 obey(line.rstrip(), io_db["file_record"].prefix
120                      + str(io_db["file_record"].line_n))
121                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
122             else:
123                 print("Reached end of record file.")
124         else:
125             commands_db[tokens[0]][2](*tokens[1:])
126             if do_record:
127                 record(command)
128                 save_world()
129             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
130     elif 0 != len(tokens):
131         print("Invalid command/argument, or bad number of tokens.")
132
133
134 def atomic_write(path, text, do_append=False):
135     """Atomic write of text to file at path, appended if do_append is set."""
136     path_tmp = path + io_db["tmp_suffix"]
137     mode = "w"
138     if do_append:
139         mode = "a"
140         if os.access(path, os.F_OK):
141             shutil.copyfile(path, path_tmp)
142     file = open(path_tmp, mode)
143     strong_write(file, text)
144     file.close()
145     if os.access(path, os.F_OK):
146         os.remove(path)
147     os.rename(path_tmp, path)
148
149
150 def record(command):
151     """Append command string plus newline to record file. (Atomic.)"""
152     # This misses some optimizations from the original record(), namely only
153     # finishing the atomic write with expensive flush() and fsync() every 15
154     # seconds unless explicitely forced. Implement as needed.
155     atomic_write(io_db["path_record"], command + "\n", do_append=True)
156
157
158 def save_world():
159     """Save all commands needed to reconstruct current world state."""
160     # TODO: Misses same optimizations as record() from the original record().
161
162     def quote(string):
163         string = string.replace("\u005C", '\u005C\u005C')
164         return '"' + string.replace('"', '\u005C"') + '"'
165
166     def mapsetter(key):
167         def helper(id):
168             string = ""
169             if world_db["Things"][id][key]:
170                 map = world_db["Things"][id][key]
171                 length = world_db["MAP_LENGTH"]
172                 for i in range(length):
173                     line = map[i * length:(i * length) + length].decode()
174                     string = string + key + " " + str(i) + " " + quote(line) \
175                              + "\n"
176             return string
177         return helper
178
179     def memthing(id):
180         string = ""
181         for memthing in world_db["Things"][id]["T_MEMTHING"]:
182             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
183                      str(memthing[1]) + " " + str(memthing[2]) + "\n"
184         return string
185
186     def helper(category, id_string, special_keys={}):
187         string = ""
188         for id in world_db[category]:
189             string = string + id_string + " " + str(id) + "\n"
190             for key in world_db[category][id]:
191                 if not key in special_keys:
192                     x = world_db[category][id][key]
193                     argument = quote(x) if str == type(x) else str(x)
194                     string = string + key + " " + argument + "\n"
195                 elif special_keys[key]:
196                     string = string + special_keys[key](id)
197         return string
198
199     string = ""
200     for key in world_db:
201         if dict != type(world_db[key]) and key != "MAP":
202             string = string + key + " " + str(world_db[key]) + "\n"
203     string = string + helper("ThingActions", "TA_ID")
204     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
205     for id in world_db["ThingTypes"]:
206         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
207                  str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
208     string = string + helper("Things", "T_ID",
209                              {"T_CARRIES": False, "carried": False,
210                               "T_MEMMAP": mapsetter("T_MEMMAP"),
211                               "T_MEMTHING": memthing, "fovmap": False,
212                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
213     for id in world_db["Things"]:
214         if [] != world_db["Things"][id]["T_CARRIES"]:
215             string = string + "T_ID " + str(id) + "\n"
216             for carried_id in world_db["Things"][id]["T_CARRIES"]:
217                 string = string + "T_CARRIES " + str(carried_id) + "\n"
218     string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
219              "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
220     atomic_write(io_db["path_save"], string)
221
222
223 def obey_lines_in_file(path, name, do_record=False):
224     """Call obey() on each line of path's file, use name in input prefix."""
225     file = open(path, "r")
226     line_n = 1
227     for line in file.readlines():
228         obey(line.rstrip(), name + "file line " + str(line_n),
229              do_record=do_record)
230         line_n = line_n + 1
231     file.close()
232
233
234 def parse_command_line_arguments():
235     """Return settings values read from command line arguments."""
236     parser = argparse.ArgumentParser()
237     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
238                         action='store')
239     opts, unknown = parser.parse_known_args()
240     return opts
241
242
243 def server_test():
244     """Ensure valid server out file belonging to current process.
245
246     This is done by comparing io_db["teststring"] to what's found at the start
247     of the current file at io_db["path_out"]. On failure, set
248     io_db["kicked_by_rival"] and raise SystemExit.
249     """
250     if not os.access(io_db["path_out"], os.F_OK):
251         raise SystemExit("Server output file has disappeared.")
252     file = open(io_db["path_out"], "r")
253     test = file.readline().rstrip("\n")
254     file.close()
255     if test != io_db["teststring"]:
256         io_db["kicked_by_rival"] = True
257         msg = "Server test string in server output file does not match. This" \
258               " indicates that the current server process has been " \
259               "superseded by another one."
260         raise SystemExit(msg)
261
262
263 def read_command():
264     """Return next newline-delimited command from server in file.
265
266     Keep building return string until a newline is encountered. Pause between
267     unsuccessful reads, and after too much waiting, run server_test().
268     """
269     wait_on_fail = 1
270     max_wait = 5
271     now = time.time()
272     command = ""
273     while True:
274         add = io_db["file_in"].readline()
275         if len(add) > 0:
276             command = command + add
277             if len(command) > 0 and "\n" == command[-1]:
278                 command = command[:-1]
279                 break
280         else:
281             time.sleep(wait_on_fail)
282             if now + max_wait < time.time():
283                 server_test()
284                 now = time.time()
285     return command
286
287
288 def try_worldstate_update():
289     """Write worldstate file if io_db["worldstate_updateable"] is set."""
290     if io_db["worldstate_updateable"]:
291
292         def draw_visible_Things(map, run):
293             for id in world_db["Things"]:
294                 type = world_db["Things"][id]["T_TYPE"]
295                 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
296                 alive = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]
297                 if (0 == run and not consumable and not alive) \
298                    or (1 == run and consumable and not alive) \
299                    or (2 == run and alive):
300                     y = world_db["Things"][id]["T_POSY"]
301                     x = world_db["Things"][id]["T_POSX"]
302                     fovflag = world_db["Things"][0]["fovmap"][(y * length) + x]
303                     if 'v' == chr(fovflag):
304                         c = world_db["ThingTypes"][type]["TT_SYMBOL"]
305                         map[(y * length) + x] = ord(c)
306
307         def write_map(string, map):
308             for i in range(length):
309                 line = map[i * length:(i * length) + length].decode()
310                 string = string + line + "\n"
311             return string
312
313         inventory = ""
314         if [] == world_db["Things"][0]["T_CARRIES"]:
315             inventory = "(none)\n"
316         else:
317             for id in world_db["Things"][0]["T_CARRIES"]:
318                 type_id = world_db["Things"][id]["T_TYPE"]
319                 name = world_db["ThingTypes"][type_id]["TT_NAME"]
320                 inventory = inventory + name + "\n"
321         string = str(world_db["TURN"]) + "\n" + \
322                  str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
323                  str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
324                  inventory + "%\n" + \
325                  str(world_db["Things"][0]["T_POSY"]) + "\n" + \
326                  str(world_db["Things"][0]["T_POSX"]) + "\n" + \
327                  str(world_db["MAP_LENGTH"]) + "\n"
328         length = world_db["MAP_LENGTH"]
329         fov = bytearray(b' ' * (length ** 2))
330         for pos in range(length ** 2):
331             if 'v' == chr(world_db["Things"][0]["fovmap"][pos]):
332                 fov[pos] = world_db["MAP"][pos]
333         for i in range(3):
334             draw_visible_Things(fov, i)
335         string = write_map(string, fov)
336         mem = world_db["Things"][0]["T_MEMMAP"][:]
337         for i in range(2):
338             for memthing in world_db["Things"][0]["T_MEMTHING"]:
339                 type = world_db["Things"][memthing[0]]["T_TYPE"]
340                 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
341                 if (i == 0 and not consumable) or (i == 1 and consumable):
342                     c = world_db["ThingTypes"][type]["TT_SYMBOL"]
343                     mem[(memthing[1] * length) + memthing[2]] = ord(c)
344         string = write_map(string, mem)
345         atomic_write(io_db["path_worldstate"], string)
346         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
347         io_db["worldstate_updateable"] = False
348
349
350 def replay_game():
351     """Replay game from record file.
352
353     Use opts.replay as breakpoint turn to which to replay automatically before
354     switching to manual input by non-meta commands in server input file
355     triggering further reads of record file. Ensure opts.replay is at least 1.
356     Run try_worldstate_update() before each interactive obey()/read_command().
357     """
358     if opts.replay < 1:
359         opts.replay = 1
360     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
361           " (if so late a turn is to be found).")
362     if not os.access(io_db["path_record"], os.F_OK):
363         raise SystemExit("No record file found to replay.")
364     io_db["file_record"] = open(io_db["path_record"], "r")
365     io_db["file_record"].prefix = "record file line "
366     io_db["file_record"].line_n = 1
367     while world_db["TURN"] < opts.replay:
368         line = io_db["file_record"].readline()
369         if "" == line:
370             break
371         obey(line.rstrip(), io_db["file_record"].prefix
372              + str(io_db["file_record"].line_n))
373         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
374     while True:
375         try_worldstate_update()
376         obey(read_command(), "in file", replay=True)
377
378
379 def play_game():
380     """Play game by server input file commands. Before, load save file found.
381
382     If no save file is found, a new world is generated from the commands in the
383     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
384     command and all that follow via the server input file. Run
385     try_worldstate_update() before each interactive obey()/read_command().
386     """
387     if os.access(io_db["path_save"], os.F_OK):
388         obey_lines_in_file(io_db["path_save"], "save")
389     else:
390         if not os.access(io_db["path_worldconf"], os.F_OK):
391             msg = "No world config file from which to start a new world."
392             raise SystemExit(msg)
393         obey_lines_in_file(io_db["path_worldconf"], "world config ",
394                            do_record=True)
395         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
396     while True:
397         try_worldstate_update()
398         obey(read_command(), "in file", do_record=True)
399
400
401 def remake_map():
402     """(Re-)make island map.
403
404     Let "~" represent water, "." land, "X" trees: Build island shape randomly,
405     start with one land cell in the middle, then go into cycle of repeatedly
406     selecting a random sea cell and transforming it into land if it is neighbor
407     to land. The cycle ends when a land cell is due to be created at the map's
408     border. Then put some trees on the map (TODO: more precise algorithm desc).
409     """
410     def is_neighbor(coordinates, type):
411         y = coordinates[0]
412         x = coordinates[1]
413         length = world_db["MAP_LENGTH"]
414         ind = y % 2
415         diag_west = x + (ind > 0)
416         diag_east = x + (ind < (length - 1))
417         pos = (y * length) + x
418         if (y > 0 and diag_east
419             and type == chr(world_db["MAP"][pos - length + ind])) \
420            or (x < (length - 1)
421                and type == chr(world_db["MAP"][pos + 1])) \
422            or (y < (length - 1) and diag_east
423                and type == chr(world_db["MAP"][pos + length + ind])) \
424            or (y > 0 and diag_west
425                and type == chr(world_db["MAP"][pos - length - (not ind)])) \
426            or (x > 0
427                and type == chr(world_db["MAP"][pos - 1])) \
428            or (y < (length - 1) and diag_west
429                and type == chr(world_db["MAP"][pos + length - (not ind)])):
430             return True
431         return False
432     store_seed = rand.seed
433     world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
434     length = world_db["MAP_LENGTH"]
435     add_half_width = (not (length % 2)) * int(length / 2)
436     world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
437     while (1):
438         y = rand.next() % length
439         x = rand.next() % length
440         pos = (y * length) + x
441         if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
442             if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
443                 break
444             world_db["MAP"][pos] = ord(".")
445     n_trees = int((length ** 2) / 16)
446     i_trees = 0
447     while (i_trees <= n_trees):
448         single_allowed = rand.next() % 32
449         y = rand.next() % length
450         x = rand.next() % length
451         pos = (y * length) + x
452         if "." == chr(world_db["MAP"][pos]) \
453           and ((not single_allowed) or is_neighbor((y, x), "X")):
454             world_db["MAP"][pos] = ord("X")
455             i_trees += 1
456     rand.seed = store_seed
457     # This all-too-precise replica of the original C code misses iter_limit().
458
459
460 def update_map_memory(t):
461     """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
462     if not t["T_MEMMAP"]:
463         t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
464     if not t["T_MEMDEPTHMAP"]:
465         t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
466     for pos in range(world_db["MAP_LENGTH"] ** 2):
467         if "v" == chr(t["fovmap"][pos]):
468             t["T_MEMDEPTHMAP"][pos] = ord("0")
469             if " " == chr(t["T_MEMMAP"][pos]):
470                 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
471             continue
472         # TODO: Aging of MEMDEPTHMAP.
473     for memthing in t["T_MEMTHING"]:
474         y = world_db["Things"][memthing[0]]["T_POSY"]
475         x = world_db["Things"][memthing[1]]["T_POSY"]
476         if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
477             t["T_MEMTHING"].remove(memthing)
478     for id in world_db["Things"]:
479         type = world_db["Things"][id]["T_TYPE"]
480         if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
481             y = world_db["Things"][id]["T_POSY"]
482             x = world_db["Things"][id]["T_POSY"]
483             if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
484                 t["T_MEMTHING"].append((type, y, x))
485
486
487 def set_world_inactive():
488     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
489     server_test()
490     if os.access(io_db["path_worldstate"], os.F_OK):
491         os.remove(io_db["path_worldstate"])
492     world_db["WORLD_ACTIVE"] = 0
493
494
495 def integer_test(val_string, min, max):
496     """Return val_string if possible integer >= min and <= max, else None."""
497     try:
498         val = int(val_string)
499         if val < min or val > max:
500             raise ValueError
501         return val
502     except ValueError:
503         print("Ignoring: Please use integer >= " + str(min) + " and <= " +
504               str(max) + ".")
505         return None
506
507
508 def setter(category, key, min, max):
509     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
510     if category is None:
511         def f(val_string):
512             val = integer_test(val_string, min, max)
513             if None != val:
514                 world_db[key] = val
515     else:
516         if category == "Thing":
517             id_store = command_tid
518             decorator = test_Thing_id
519         elif category == "ThingType":
520             id_store = command_ttid
521             decorator = test_ThingType_id
522         elif category == "ThingAction":
523             id_store = command_taid
524             decorator = test_ThingAction_id
525
526         @decorator
527         def f(val_string):
528             val = integer_test(val_string, min, max)
529             if None != val:
530                 world_db[category + "s"][id_store.id][key] = val
531     return f
532
533
534 def build_fov_map(t):
535     """Build Thing's FOV map."""
536     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
537     # DUMMY so far. Just builds an all-visible map.
538
539
540 def actor_wait(t):
541     """Make t do nothing (but loudly, if player avatar)."""
542     if t == world_db["Things"][0]:
543         strong_write(io_db["file_out"], "LOG You wait.\n")
544
545
546 def actor_move(t):
547     pass
548
549
550 def actor_pick_up(t):
551     """Make t pick up (topmost?) Thing from ground into inventory."""
552     # Topmostness is actually not defined so far.
553     ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
554            if not world_db["Things"][id]["carried"]
555            if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
556            if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
557     if len(ids):
558         world_db["Things"][ids[0]]["carried"] = True
559         t["T_CARRIES"].append(ids[0])
560         if t == world_db["Things"][0]:
561             strong_write(io_db["file_out"], "LOG You pick up an object.\n")
562     elif t == world_db["Things"][0]:
563         err = "You try to pick up an object, but there is none."
564         strong_write(io_db["file_out"], "LOG " + err + "\n")
565
566
567 def actor_drop(t):
568     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
569     # TODO: Handle case where T_ARGUMENT matches nothing.
570     if len(t["T_CARRIES"]):
571         id = t["T_CARRIES"][t["T_ARGUMENT"]]
572         t["T_CARRIES"].remove(id)
573         world_db["Things"][id]["carried"] = False
574         if t == world_db["Things"][0]:
575             strong_write(io_db["file_out"], "LOG You drop an object.\n")
576     elif t == world_db["Things"][0]:
577         err = "You try to drop an object, but you own none."
578         strong_write(io_db["file_out"], "LOG " + err + "\n")
579
580
581 def actor_use(t):
582     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
583     # Original wrongly featured lifepoints increase through consumable!
584     # TODO: Handle case where T_ARGUMENT matches nothing.
585     if len(t["T_CARRIES"]):
586         id = t["T_CARRIES"][t["T_ARGUMENT"]]
587         type = world_db["Things"][id]["T_TYPE"]
588         if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
589             t["T_CARRIES"].remove(id)
590             del world_db["Things"][id]
591             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
592             strong_write(io_db["file_out"], "LOG You consume this object.\n")
593         else:
594             strong_write(io_db["file_out"], "LOG You try to use this object," +
595                                             "but fail.\n")
596     else:
597         strong_write(io_db["file_out"], "LOG You try to use an object, but " +
598                                         "you own none.\n")
599
600
601 def turn_over():
602     """Run game world and its inhabitants until new player input expected."""
603     id = 0
604     whilebreaker = False
605     while world_db["Things"][0]["T_LIFEPOINTS"]:
606         for id in [id for id in world_db["Things"]
607                    if world_db["Things"][id]["T_LIFEPOINTS"]]:
608             Thing = world_db["Things"][id]
609             if Thing["T_LIFEPOINTS"]:
610                 if not Thing["T_COMMAND"]:
611                     update_map_memory(Thing)
612                     if 0 == id:
613                         whilebreaker = True
614                         break
615                     # DUMMY: ai(thing)
616                     Thing["T_COMMAND"] = 1
617                 # DUMMY: try_healing
618                 Thing["T_PROGRESS"] += 1
619                 taid = [a for a in world_db["ThingActions"]
620                           if a == Thing["T_COMMAND"]][0]
621                 ThingAction = world_db["ThingActions"][taid]
622                 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
623                     eval("actor_" + ThingAction["TA_NAME"])(Thing)
624                     Thing["T_COMMAND"] = 0
625                     Thing["T_PROGRESS"] = 0
626                 # DUMMY: hunger
627             # DUMMY: thingproliferation
628         if whilebreaker:
629             break
630         world_db["TURN"] += 1
631
632
633 def new_Thing(type, pos=(0,0)):
634     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
635     thing = {
636         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
637         "T_ARGUMENT": 0,
638         "T_PROGRESS": 0,
639         "T_SATIATION": 0,
640         "T_COMMAND": 0,
641         "T_TYPE": type,
642         "T_POSY": pos[0],
643         "T_POSX": pos[1],
644         "T_CARRIES": [],
645         "carried": False,
646         "T_MEMTHING": [],
647         "T_MEMMAP": False,
648         "T_MEMDEPTHMAP": False,
649         "fovmap": False
650     }
651     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
652         build_fov_map(thing)
653     return thing
654
655
656 def id_setter(id, category, id_store=False, start_at_1=False):
657     """Set ID of object of category to manipulate ID unused? Create new one.
658
659     The ID is stored as id_store.id (if id_store is set). If the integer of the
660     input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
661     32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
662     >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
663     new object is created, otherwise the new object's ID.
664     """
665     min = 0 if start_at_1 else -32768
666     max = 255 if start_at_1 else 32767
667     if str == type(id):
668         id = integer_test(id, min, max)
669     if None != id:
670         if id in world_db[category]:
671             if id_store:
672                 id_store.id = id
673             return None
674         else:
675             if (start_at_1 and 0 == id) \
676                or ((not start_at_1) and (id < 0 or id > 255)):
677                 id = -1
678                 while 1:
679                     id = id + 1
680                     if id not in world_db[category]:
681                         break
682                 if id > 255:
683                     print("Ignoring: "
684                           "No unused ID available to add to ID list.")
685                     return None
686             if id_store:
687                 id_store.id = id
688     return id
689
690
691 def command_ping():
692     """Send PONG line to server output file."""
693     strong_write(io_db["file_out"], "PONG\n")
694
695
696 def command_quit():
697     """Abort server process."""
698     raise SystemExit("received QUIT command")
699
700
701 def command_thingshere(str_y, str_x):
702     """Write to out file list of Things known to player at coordinate y, x."""
703     def write_thing_if_here():
704         if y == world_db["Things"][id]["T_POSY"] \
705            and x == world_db["Things"][id]["T_POSX"] \
706            and not world_db["Things"][id]["carried"]:
707             type = world_db["Things"][id]["T_TYPE"]
708             name = world_db["ThingTypes"][type]["TT_NAME"]
709             strong_write(io_db["file_out"], name + "\n")
710     if world_db["WORLD_ACTIVE"]:
711         y = integer_test(str_y, 0, 255)
712         x = integer_test(str_x, 0, 255)
713         length = world_db["MAP_LENGTH"]
714         if None != y and None != x and y < length and x < length:
715             pos = (y * world_db["MAP_LENGTH"]) + x
716             strong_write(io_db["file_out"], "THINGS_HERE START\n")
717             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
718                 for id in world_db["Things"]:
719                     write_thing_if_here()
720             else:
721                 for id in world_db["Things"]["T_MEMTHING"]:
722                     write_thing_if_here()
723             strong_write(io_db["file_out"], "THINGS_HERE END\n")
724         else:
725             print("Ignoring: Invalid map coordinates.")
726     else:
727         print("Ignoring: Command only works on existing worlds.")
728
729
730 def play_commander(action, args=False):
731     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
732
733     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
734     """
735
736     def set_command():
737         id = [x for x in world_db["ThingActions"]
738                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
739         world_db["Things"][0]["T_COMMAND"] = id
740         turn_over()
741
742     def set_command_and_argument_int(str_arg):
743         val = integer_test(str_arg, 0, 255)
744         if None != val:
745             world_db["Things"][0]["T_ARGUMENT"] = val
746             set_command()
747
748     def set_command_and_argument_movestring(str_arg):
749         dirs = {"east": "d", "south-east": "c", "south-west": "x",
750                 "west": "s", "north-west": "w", "north-east": "e"}
751         if str_arg in dirs:
752             world_db["Things"][0]["T_ARGUMENT"] = dirs[str_arg]
753             set_command()
754         else:
755             print("Ignoring: Argument must be valid direction string.")
756
757     if action == "move":
758         return set_command_and_argument_movestring
759     elif args:
760         return set_command_and_argument_int
761     else:
762         return set_command
763
764
765 def command_seedrandomness(seed_string):
766     """Set rand seed to int(seed_string)."""
767     val = integer_test(seed_string, 0, 4294967295)
768     if None != val:
769         rand.seed = val
770
771
772 def command_seedmap(seed_string):
773     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
774     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
775     remake_map()
776
777
778 def command_makeworld(seed_string):
779     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
780
781     Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
782     "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
783     TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
784     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
785     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
786     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
787     other. Init player's memory map. Write "NEW_WORLD" line to out file.
788     """
789
790     def free_pos():
791         i = 0
792         while 1:
793             err = "Space to put thing on too hard to find. Map too small?"
794             while 1:
795                 y = rand.next() % world_db["MAP_LENGTH"]
796                 x = rand.next() % world_db["MAP_LENGTH"]
797                 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
798                     break
799                 i += 1
800                 if i == 65535:
801                     raise SystemExit(err)
802             # Replica of C code, wrongly ignores animatedness of new Thing.
803             pos_clear = (0 == len([id for id in world_db["Things"]
804                                    if world_db["Things"][id]["T_LIFEPOINTS"]
805                                    if world_db["Things"][id]["T_POSY"] == y
806                                    if world_db["Things"][id]["T_POSX"] == x]))
807             if pos_clear:
808                 break
809         return (y, x)
810
811     val = integer_test(seed_string, 0, 4294967295)
812     if None == val:
813         return
814     rand.seed = val
815     world_db["SEED_MAP"] = val
816     player_will_be_generated = False
817     playertype = world_db["PLAYER_TYPE"]
818     for ThingType in world_db["ThingTypes"]:
819         if playertype == ThingType:
820             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
821                 player_will_be_generated = True
822             break
823     if not player_will_be_generated:
824         print("Ignoring beyond SEED_MAP: " +
825               "No player type with start number >0 defined.")
826         return
827     wait_action = False
828     for ThingAction in world_db["ThingActions"]:
829         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
830             wait_action = True
831     if not wait_action:
832         print("Ignoring beyond SEED_MAP: " +
833               "No thing action with name 'wait' defined.")
834         return
835     world_db["Things"] = {}
836     remake_map()
837     world_db["WORLD_ACTIVE"] = 1
838     world_db["TURN"] = 1
839     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
840         id = id_setter(-1, "Things")
841         world_db["Things"][id] = new_Thing(playertype, free_pos())
842     update_map_memory(world_db["Things"][0])
843     for type in world_db["ThingTypes"]:
844         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
845             if type != playertype:
846                 id = id_setter(-1, "Things")
847                 world_db["Things"][id] = new_Thing(type, free_pos())
848     strong_write(io_db["file_out"], "NEW_WORLD\n")
849
850
851 def command_maplength(maplength_string):
852     """Redefine map length. Invalidate map, therefore lose all things on it."""
853     val = integer_test(val_string, 1, 256)
854     if None != val:
855         world_db["MAP_LENGTH"] = val
856         set_world_inactive()
857         world_db["Things"] = {}
858         libpr.set_maplength = val
859
860
861 def command_worldactive(worldactive_string):
862     """Toggle world_db["WORLD_ACTIVE"] if possible.
863
864     An active world can always be set inactive. An inactive world can only be
865     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
866     activation, rebuild all Things' FOVs, and the player's map memory.
867     """
868     # In original version, map existence was also tested (unnecessarily?).
869     val = integer_test(worldactive_string, 0, 1)
870     if val:
871         if 0 != world_db["WORLD_ACTIVE"]:
872             if 0 == val:
873                 set_world_inactive()
874             else:
875                 print("World already active.")
876         elif 0 == world_db["WORLD_ACTIVE"]:
877             wait_exists = False
878             for ThingAction in world_db["ThingActions"]:
879                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
880                     wait_exists = True
881                     break
882             player_exists = False
883             for Thing in world_db["Things"]:
884                 if 0 == Thing:
885                     player_exists = True
886                     break
887             if wait_exists and player_exists:
888                 for id in world_db["Things"]:
889                     if world_db["Things"][id]["T_LIFEPOINTS"]:
890                         build_fov_map(world_db["Things"][id])
891                         if 0 == id:
892                             update_map_memory(world_db["Things"][id])
893                 world_db["WORLD_ACTIVE"] = 1
894
895
896 def test_for_id_maker(object, category):
897     """Return decorator testing for object having "id" attribute."""
898     def decorator(f):
899         def helper(*args):
900             if hasattr(object, "id"):
901                 f(*args)
902             else:
903                 print("Ignoring: No " + category +
904                       " defined to manipulate yet.")
905         return helper
906     return decorator
907
908
909 def command_tid(id_string):
910     """Set ID of Thing to manipulate. ID unused? Create new one.
911
912     Default new Thing's type to the first available ThingType, others: zero.
913     """
914     id = id_setter(id_string, "Things", command_tid)
915     if None != id:
916         if world_db["ThingTypes"] == {}:
917             print("Ignoring: No ThingType to settle new Thing in.")
918             return
919         type = list(world_db["ThingTypes"].keys())[0]
920         world_db["Things"][id] = new_Thing(type)
921
922
923 test_Thing_id = test_for_id_maker(command_tid, "Thing")
924
925
926 @test_Thing_id
927 def command_tcommand(str_int):
928     """Set T_COMMAND of selected Thing."""
929     val = integer_test(str_int, 0, 255)
930     if None != val:
931         if 0 == val or val in world_db["ThingActions"]:
932             world_db["Things"][command_tid.id]["T_COMMAND"] = val
933         else:
934             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
935
936
937 @test_Thing_id
938 def command_ttype(str_int):
939     """Set T_TYPE of selected Thing."""
940     val = integer_test(str_int, 0, 255)
941     if None != val:
942         if val in world_db["ThingTypes"]:
943             world_db["Things"][command_tid.id]["T_TYPE"] = val
944         else:
945             print("Ignoring: ThingType ID belongs to no known ThingType.")
946
947
948 @test_Thing_id
949 def command_tcarries(str_int):
950     """Append int(str_int) to T_CARRIES of selected Thing.
951
952     The ID int(str_int) must not be of the selected Thing, and must belong to a
953     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
954     """
955     val = integer_test(str_int, 0, 255)
956     if None != val:
957         if val == command_tid.id:
958             print("Ignoring: Thing cannot carry itself.")
959         elif val in world_db["Things"] \
960              and not world_db["Things"][val]["carried"]:
961             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
962             world_db["Things"][val]["carried"] = True
963         else:
964             print("Ignoring: Thing not available for carrying.")
965     # Note that the whole carrying structure is different from the C version:
966     # Carried-ness is marked by a "carried" flag, not by Things containing
967     # Things internally.
968
969
970 @test_Thing_id
971 def command_tmemthing(str_t, str_y, str_x):
972     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
973
974     The type must fit to an existing ThingType, and the position into the map.
975     """
976     type = integer_test(str_t, 0, 255)
977     posy = integer_test(str_y, 0, 255)
978     posx = integer_test(str_x, 0, 255)
979     if None != type and None != posy and None != posx:
980         if type not in world_db["ThingTypes"] \
981            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
982             print("Ignoring: Illegal value for thing type or position.")
983         else:
984             memthing = (type, posy, posx)
985             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
986
987
988 def setter_map(maptype):
989     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
990
991     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
992     """
993     @test_Thing_id
994     def helper(str_int, mapline):
995         val = integer_test(str_int, 0, 255)
996         if None != val:
997             if val >= world_db["MAP_LENGTH"]:
998                 print("Illegal value for map line number.")
999             elif len(mapline) != world_db["MAP_LENGTH"]:
1000                 print("Map line length is unequal map width.")
1001             else:
1002                 length = world_db["MAP_LENGTH"]
1003                 map = None
1004                 if not world_db["Things"][command_tid.id][maptype]:
1005                     map = bytearray(b' ' * (length ** 2))
1006                 else:
1007                     map = world_db["Things"][command_tid.id][maptype]
1008                 map[val * length:(val * length) + length] = mapline.encode()
1009                 world_db["Things"][command_tid.id][maptype] = map
1010     return helper
1011
1012
1013 def setter_tpos(axis):
1014     """Generate setter for T_POSX or  T_POSY of selected Thing.
1015
1016     If world is active, rebuilds animate things' fovmap, player's memory map.
1017     """
1018     @test_Thing_id
1019     def helper(str_int):
1020         val = integer_test(str_int, 0, 255)
1021         if None != val:
1022             if val < world_db["MAP_LENGTH"]:
1023                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1024                 if world_db["WORLD_ACTIVE"] \
1025                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1026                     build_fov_map(world_db["Things"][command_tid.id])
1027                     if 0 == command_tid.id:
1028                         update_map_memory(world_db["Things"][command_tid.id])
1029             else:
1030                 print("Ignoring: Position is outside of map.")
1031     return helper
1032
1033
1034 def command_ttid(id_string):
1035     """Set ID of ThingType to manipulate. ID unused? Create new one.
1036
1037     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1038     """
1039     id = id_setter(id_string, "ThingTypes", command_ttid)
1040     if None != id:
1041         world_db["ThingTypes"][id] = {
1042             "TT_NAME": "(none)",
1043             "TT_CONSUMABLE": 0,
1044             "TT_LIFEPOINTS": 0,
1045             "TT_PROLIFERATE": 0,
1046             "TT_START_NUMBER": 0,
1047             "TT_SYMBOL": "?",
1048             "TT_CORPSE_ID": id
1049         }
1050
1051
1052 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1053
1054
1055 @test_ThingType_id
1056 def command_ttname(name):
1057     """Set TT_NAME of selected ThingType."""
1058     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1059
1060
1061 @test_ThingType_id
1062 def command_ttsymbol(char):
1063     """Set TT_SYMBOL of selected ThingType. """
1064     if 1 == len(char):
1065         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1066     else:
1067         print("Ignoring: Argument must be single character.")
1068
1069
1070 @test_ThingType_id
1071 def command_ttcorpseid(str_int):
1072     """Set TT_CORPSE_ID of selected ThingType."""
1073     val = integer_test(str_int, 0, 255)
1074     if None != val:
1075         if val in world_db["ThingTypes"]:
1076             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1077         else:
1078             print("Ignoring: Corpse ID belongs to no known ThignType.")
1079
1080
1081 def command_taid(id_string):
1082     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1083
1084     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1085     """
1086     id = id_setter(id_string, "ThingActions", command_taid, True)
1087     if None != id:
1088         world_db["ThingActions"][id] = {
1089             "TA_EFFORT": 1,
1090             "TA_NAME": "wait"
1091         }
1092
1093
1094 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1095
1096
1097 @test_ThingAction_id
1098 def command_taname(name):
1099     """Set TA_NAME of selected ThingAction.
1100
1101     The name must match a valid thing action function. If after the name
1102     setting no ThingAction with name "wait" remains, call set_world_inactive().
1103     """
1104     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1105        or name == "pick_up":
1106         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1107         if 1 == world_db["WORLD_ACTIVE"]:
1108             wait_defined = False
1109             for id in world_db["ThingActions"]:
1110                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1111                     wait_defined = True
1112                     break
1113             if not wait_defined:
1114                 set_world_inactive()
1115     else:
1116         print("Ignoring: Invalid action name.")
1117     # In contrast to the original,naming won't map a function to a ThingAction.
1118
1119
1120 """Commands database.
1121
1122 Map command start tokens to ([0]) number of expected command arguments, ([1])
1123 the command's meta-ness (i.e. is it to be written to the record file, is it to
1124 be ignored in replay mode if read from server input file), and ([2]) a function
1125 to be called on it.
1126 """
1127 commands_db = {
1128     "QUIT": (0, True, command_quit),
1129     "PING": (0, True, command_ping),
1130     "THINGS_HERE": (2, True, command_thingshere),
1131     "MAKE_WORLD": (1, False, command_makeworld),
1132     "SEED_MAP": (1, False, command_seedmap),
1133     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1134     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1135     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1136     "MAP_LENGTH": (1, False, command_maplength),
1137     "WORLD_ACTIVE": (1, False, command_worldactive),
1138     "TA_ID": (1, False, command_taid),
1139     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1140     "TA_NAME": (1, False, command_taname),
1141     "TT_ID": (1, False, command_ttid),
1142     "TT_NAME": (1, False, command_ttname),
1143     "TT_SYMBOL": (1, False, command_ttsymbol),
1144     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1145     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1146                                        0, 65535)),
1147     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1148                                          0, 255)),
1149     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1150                                         0, 255)),
1151     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1152     "T_ID": (1, False, command_tid),
1153     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1154     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1155     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1156     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1157     "T_COMMAND": (1, False, command_tcommand),
1158     "T_TYPE": (1, False, command_ttype),
1159     "T_CARRIES": (1, False, command_tcarries),
1160     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1161     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1162     "T_MEMTHING": (3, False, command_tmemthing),
1163     "T_POSY": (1, False, setter_tpos("Y")),
1164     "T_POSX": (1, False, setter_tpos("X")),
1165     "wait": (0, False, play_commander("wait")),
1166     "move": (1, False, play_commander("move")),
1167     "pick_up": (0, False, play_commander("pick_up")),
1168     "drop": (1, False, play_commander("drop", True)),
1169     "use": (1, False, play_commander("use", True)),
1170 }
1171
1172
1173 """World state database. With sane default values. (Randomness is in rand.)"""
1174 world_db = {
1175     "TURN": 0,
1176     "SEED_MAP": 0,
1177     "PLAYER_TYPE": 0,
1178     "MAP_LENGTH": 64,
1179     "WORLD_ACTIVE": 0,
1180     "ThingActions": {},
1181     "ThingTypes": {},
1182     "Things": {}
1183 }
1184
1185
1186 """File IO database."""
1187 io_db = {
1188     "path_save": "save",
1189     "path_record": "record",
1190     "path_worldconf": "confserver/world",
1191     "path_server": "server/",
1192     "path_in": "server/in",
1193     "path_out": "server/out",
1194     "path_worldstate": "server/worldstate",
1195     "tmp_suffix": "_tmp",
1196     "kicked_by_rival": False,
1197     "worldstate_updateable": False
1198 }
1199
1200
1201 try:
1202     libpr = prep_library()
1203     rand = RandomnessIO()
1204     opts = parse_command_line_arguments()
1205     setup_server_io()
1206     if None != opts.replay:
1207         replay_game()
1208     else:
1209         play_game()
1210 except SystemExit as exit:
1211     print("ABORTING: " + exit.args[0])
1212 except:
1213     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1214     raise
1215 finally:
1216     cleanup_server_io()