home · contact · privacy
b5e4db6bc8db6173391b927dad9960def3bec8d9
[plomrogue] / plomrogue-server.py
1 import argparse
2 import errno
3 import os
4 import shlex
5 import shutil
6 import time
7
8
9 def strong_write(file, string):
10     """Apply write(string), flush(), and os.fsync() to file."""
11     file.write(string)
12     file.flush()
13     os.fsync(file)
14
15
16 def setup_server_io():
17     """Fill IO files DB with proper file( path)s. Write process IO test string.
18
19     Ensure IO files directory at server/. Remove any old input file if found.
20     Set up new input file for reading, and new output file for writing. Start
21     output file with process hash line of format PID + " " + floated UNIX time
22     (io_db["teststring"]). Raise SystemExit if file is found at path of either
23     record or save file plus io_db["tmp_suffix"].
24     """
25     def detect_atomic_leftover(path, tmp_suffix):
26         path_tmp = path + tmp_suffix
27         msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
28               "aborted previous attempt to write '" + path + "'. Aborting " \
29              "until matter is resolved by removing it from its current path."
30         if os.access(path_tmp, os.F_OK):
31             raise SystemExit(msg)
32     io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
33     os.makedirs(io_db["path_server"], exist_ok=True)
34     io_db["file_out"] = open(io_db["path_out"], "w")
35     strong_write(io_db["file_out"], io_db["teststring"] + "\n")
36     if os.access(io_db["path_in"], os.F_OK):
37         os.remove(io_db["path_in"])
38     io_db["file_in"] = open(io_db["path_in"], "w")
39     io_db["file_in"].close()
40     io_db["file_in"] = open(io_db["path_in"], "r")
41     detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
42     detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
43
44
45 def cleanup_server_io():
46     """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
47     def helper(file_key, path_key):
48         if file_key in io_db:
49             io_db[file_key].close()
50             if not io_db["kicked_by_rival"] \
51                and os.access(io_db[path_key], os.F_OK):
52                 os.remove(io_db[path_key])
53     helper("file_out", "path_out")
54     helper("file_in", "path_in")
55     helper("file_worldstate", "path_worldstate")
56     if "file_record" in io_db:
57         io_db["file_record"].close()
58
59
60 def obey(command, prefix, replay=False, do_record=False):
61     """Call function from commands_db mapped to command's first token.
62
63     Tokenize command string with shlex.split(comments=True). If replay is set,
64     a non-meta command from the commands_db merely triggers obey() on the next
65     command from the records file. If not, non-meta commands set
66     io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
67     do_record is set, are recorded via record(), and save_world() is called.
68     The prefix string is inserted into the server's input message between its
69     beginning 'input ' & ':'. All activity is preceded by a server_test() call.
70     """
71     server_test()
72     print("input " + prefix + ": " + command)
73     try:
74         tokens = shlex.split(command, comments=True)
75     except ValueError as err:
76         print("Can't tokenize command string: " + str(err) + ".")
77         return
78     if len(tokens) > 0 and tokens[0] in commands_db \
79        and len(tokens) == commands_db[tokens[0]][0] + 1:
80         if commands_db[tokens[0]][1]:
81             commands_db[tokens[0]][2](*tokens[1:])
82         elif replay:
83             print("Due to replay mode, reading command as 'go on in record'.")
84             line = io_db["file_record"].readline()
85             if len(line) > 0:
86                 obey(line.rstrip(), io_db["file_record"].prefix
87                      + str(io_db["file_record"].line_n))
88                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
89             else:
90                 print("Reached end of record file.")
91         else:
92             commands_db[tokens[0]][2](*tokens[1:])
93             if do_record:
94                 record(command)
95                 save_world()
96             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
97     elif 0 != len(tokens):
98         print("Invalid command/argument, or bad number of tokens.")
99
100
101 def atomic_write(path, text, do_append=False):
102     """Atomic write of text to file at path, appended if do_append is set."""
103     path_tmp = path + io_db["tmp_suffix"]
104     mode = "w"
105     if do_append:
106         mode = "a"
107         if os.access(path, os.F_OK):
108             shutil.copyfile(path, path_tmp)
109     file = open(path_tmp, mode)
110     strong_write(file, text)
111     file.close()
112     if os.access(path, os.F_OK):
113         os.remove(path)
114     os.rename(path_tmp, path)
115
116
117 def record(command):
118     """Append command string plus newline to record file. (Atomic.)"""
119     # This misses some optimizations from the original record(), namely only
120     # finishing the atomic write with expensive flush() and fsync() every 15
121     # seconds unless explicitely forced. Implement as needed.
122     atomic_write(io_db["path_record"], command + "\n", do_append=True)
123
124
125 def save_world():
126     """Save all commands needed to reconstruct current world state."""
127     # TODO: Misses same optimizations as record() from the original record().
128
129     def quote(string):
130         string = string.replace("\u005C", '\u005C\u005C')
131         return '"' + string.replace('"', '\u005C"') + '"'
132
133     def mapsetter(key):
134         def helper(id):
135             string = ""
136             if world_db["Things"][id][key]:
137                 map = world_db["Things"][id][key]
138                 length = world_db["MAP_LENGTH"]
139                 for i in range(length):
140                     line = map[i * length:(i * length) + length].decode()
141                     string = string + key + " " + str(i) + " " + quote(line) \
142                              + "\n"
143             return string
144         return helper
145
146     def memthing(id):
147         string = ""
148         for memthing in world_db["Things"][id]["T_MEMTHING"]:
149             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
150                      str(memthing[1]) + " " + str(memthing[2]) + "\n"
151         return string
152
153     def helper(category, id_string, special_keys={}):
154         string = ""
155         for id in world_db[category]:
156             string = string + id_string + " " + str(id) + "\n"
157             for key in world_db[category][id]:
158                 if not key in special_keys:
159                     x = world_db[category][id][key]
160                     argument = quote(x) if str == type(x) else str(x)
161                     string = string + key + " " + argument + "\n"
162                 elif special_keys[key]:
163                     string = string + special_keys[key](id)
164         return string
165
166     string = ""
167     for key in world_db:
168         if dict != type(world_db[key]) and key != "MAP":
169             string = string + key + " " + str(world_db[key]) + "\n"
170     string = string + helper("ThingActions", "TA_ID")
171     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
172     for id in world_db["ThingTypes"]:
173         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
174                  str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
175     string = string + helper("Things", "T_ID",
176                              {"T_CARRIES": False, "carried": False,
177                               "T_MEMMAP": mapsetter("T_MEMMAP"),
178                               "T_MEMTHING": memthing, "fovmap": False,
179                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
180     for id in world_db["Things"]:
181         if [] != world_db["Things"][id]["T_CARRIES"]:
182             string = string + "T_ID " + str(id) + "\n"
183             for carried_id in world_db["Things"][id]["T_CARRIES"]:
184                 string = string + "T_CARRIES " + str(carried_id) + "\n"
185     string = string + "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
186     atomic_write(io_db["path_save"], string)
187
188
189 def obey_lines_in_file(path, name, do_record=False):
190     """Call obey() on each line of path's file, use name in input prefix."""
191     file = open(path, "r")
192     line_n = 1
193     for line in file.readlines():
194         obey(line.rstrip(), name + "file line " + str(line_n),
195              do_record=do_record)
196         line_n = line_n + 1
197     file.close()
198
199
200 def parse_command_line_arguments():
201     """Return settings values read from command line arguments."""
202     parser = argparse.ArgumentParser()
203     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
204                         action='store')
205     opts, unknown = parser.parse_known_args()
206     return opts
207
208
209 def server_test():
210     """Ensure valid server out file belonging to current process.
211
212     This is done by comparing io_db["teststring"] to what's found at the start
213     of the current file at io_db["path_out"]. On failure, set
214     io_db["kicked_by_rival"] and raise SystemExit.
215     """
216     if not os.access(io_db["path_out"], os.F_OK):
217         raise SystemExit("Server output file has disappeared.")
218     file = open(io_db["path_out"], "r")
219     test = file.readline().rstrip("\n")
220     file.close()
221     if test != io_db["teststring"]:
222         io_db["kicked_by_rival"] = True
223         msg = "Server test string in server output file does not match. This" \
224               " indicates that the current server process has been " \
225               "superseded by another one."
226         raise SystemExit(msg)
227
228
229 def read_command():
230     """Return next newline-delimited command from server in file.
231
232     Keep building return string until a newline is encountered. Pause between
233     unsuccessful reads, and after too much waiting, run server_test().
234     """
235     wait_on_fail = 1
236     max_wait = 5
237     now = time.time()
238     command = ""
239     while True:
240         add = io_db["file_in"].readline()
241         if len(add) > 0:
242             command = command + add
243             if len(command) > 0 and "\n" == command[-1]:
244                 command = command[:-1]
245                 break
246         else:
247             time.sleep(wait_on_fail)
248             if now + max_wait < time.time():
249                 server_test()
250                 now = time.time()
251     return command
252
253
254 def try_worldstate_update():
255     """Write worldstate file if io_db["worldstate_updateable"] is set."""
256     if io_db["worldstate_updateable"]:
257
258         def draw_visible_Things(map, run):
259             for id in world_db["Things"]:
260                 type = world_db["Things"][id]["T_TYPE"]
261                 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
262                 alive = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]
263                 if (0 == run and not consumable and not alive) \
264                    or (1 == run and consumable and not alive) \
265                    or (2 == run and alive):
266                     y = world_db["Things"][id]["T_POSY"]
267                     x = world_db["Things"][id]["T_POSX"]
268                     fovflag = world_db["Things"][0]["fovmap"][(y * length) + x]
269                     if 'v' == chr(fovflag):
270                         c = world_db["ThingTypes"][type]["TT_SYMBOL"]
271                         map[(y * length) + x] = ord(c)
272
273         def write_map(string, map):
274             for i in range(length):
275                 line = map[i * length:(i * length) + length].decode()
276                 string = string + line + "\n"
277             return string
278
279         inventory = ""
280         if [] == world_db["Things"][0]["T_CARRIES"]:
281             inventory = "(none)\n"
282         else:
283             for id in world_db["Things"][0]["T_CARRIES"]:
284                 type_id = world_db["Things"][id]["T_TYPE"]
285                 name = world_db["ThingTypes"][type_id]["TT_NAME"]
286                 inventory = inventory + name + "\n"
287         string = str(world_db["TURN"]) + "\n" + \
288                  str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
289                  str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
290                  inventory + "%\n" + \
291                  str(world_db["Things"][0]["T_POSY"]) + "\n" + \
292                  str(world_db["Things"][0]["T_POSX"]) + "\n" + \
293                  str(world_db["MAP_LENGTH"]) + "\n"
294         length = world_db["MAP_LENGTH"]
295         fov = bytearray(b' ' * (length ** 2))
296         for pos in range(length ** 2):
297             fovflag = world_db["Things"][0]["fovmap"][pos]
298             if 'v' == chr(fovflag):
299                 fov[pos] = world_db["MAP"][pos]
300         for i in range(3):
301             draw_visible_Things(fov, i)
302         string = write_map(string, fov)
303         mem = world_db["Things"][0]["T_MEMMAP"][:]
304         for i in range(2):
305             for id in world_db["Things"][0]["T_MEMTHING"]:
306                 type = world_db["Things"][id]["T_TYPE"]
307                 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
308                 if (i == 0 and not consumable) or (i == 1 and consumable):
309                         c = world_db["ThingTypes"][type]["TT_SYMBOL"]
310                         mem[(y * length) + x] = ord(c)
311         string = write_map(string, mem)
312         atomic_write(io_db["path_worldstate"], string)
313         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
314         io_db["worldstate_updateable"] = False
315
316
317 def replay_game():
318     """Replay game from record file.
319
320     Use opts.replay as breakpoint turn to which to replay automatically before
321     switching to manual input by non-meta commands in server input file
322     triggering further reads of record file. Ensure opts.replay is at least 1.
323     Run try_worldstate_update() before each interactive obey()/read_command().
324     """
325     if opts.replay < 1:
326         opts.replay = 1
327     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
328           " (if so late a turn is to be found).")
329     if not os.access(io_db["path_record"], os.F_OK):
330         raise SystemExit("No record file found to replay.")
331     io_db["file_record"] = open(io_db["path_record"], "r")
332     io_db["file_record"].prefix = "record file line "
333     io_db["file_record"].line_n = 1
334     while world_db["TURN"] < opts.replay:
335         line = io_db["file_record"].readline()
336         if "" == line:
337             break
338         obey(line.rstrip(), io_db["file_record"].prefix
339              + str(io_db["file_record"].line_n))
340         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
341     while True:
342         try_worldstate_update()
343         obey(read_command(), "in file", replay=True)
344
345
346 def play_game():
347     """Play game by server input file commands. Before, load save file found.
348
349     If no save file is found, a new world is generated from the commands in the
350     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
351     command and all that follow via the server input file. Run
352     try_worldstate_update() before each interactive obey()/read_command().
353     """
354     if os.access(io_db["path_save"], os.F_OK):
355         obey_lines_in_file(io_db["path_save"], "save")
356     else:
357         if not os.access(io_db["path_worldconf"], os.F_OK):
358             msg = "No world config file from which to start a new world."
359             raise SystemExit(msg)
360         obey_lines_in_file(io_db["path_worldconf"], "world config ",
361                            do_record=True)
362         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
363     while True:
364         try_worldstate_update()
365         obey(read_command(), "in file", do_record=True)
366
367
368 def remake_map():
369     # DUMMY map creator.
370     world_db["MAP"] = bytearray(b'.' * (world_db["MAP_LENGTH"] ** 2))
371
372
373 def update_map_memory(t):
374     """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
375     if not t["T_MEMMAP"]:
376         t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
377     if not t["T_MEMDEPTHMAP"]:
378         t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
379     for pos in range(world_db["MAP_LENGTH"] ** 2):
380         if "v" == chr(t["fovmap"][pos]):
381             t["T_MEMDEPTHMAP"][pos] = ord("0")
382             if " " == chr(t["T_MEMMAP"][pos]):
383                 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
384             continue
385         # TODO: Aging of MEMDEPTHMAP.
386     for id in t["T_MEMTHING"]:
387         y = world_db["Things"][id]["T_POSY"]
388         x = world_db["Things"][id]["T_POSY"]
389         if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
390             t["T_MEMTHING"].remove(id)
391     for id in world_db["Things"]:
392         type = world_db["Things"][id]["T_TYPE"]
393         if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
394             y = world_db["Things"][id]["T_POSY"]
395             x = world_db["Things"][id]["T_POSY"]
396             if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
397                 t["T_MEMTHING"] = (type, y, x)
398
399
400 def set_world_inactive():
401     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
402     server_test()
403     if os.access(io_db["path_worldstate"], os.F_OK):
404         os.remove(io_db["path_worldstate"])
405     world_db["WORLD_ACTIVE"] = 0
406
407
408 def integer_test(val_string, min, max):
409     """Return val_string if possible integer >= min and <= max, else None."""
410     try:
411         val = int(val_string)
412         if val < min or val > max:
413             raise ValueError
414         return val
415     except ValueError:
416         print("Ignoring: Please use integer >= " + str(min) + " and <= " +
417               str(max) + ".")
418         return None
419
420
421 def setter(category, key, min, max):
422     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
423     if category is None:
424         def f(val_string):
425             val = integer_test(val_string, min, max)
426             if None != val:
427                 world_db[key] = val
428     else:
429         if category == "Thing":
430             id_store = command_tid
431             decorator = test_Thing_id
432         elif category == "ThingType":
433             id_store = command_ttid
434             decorator = test_ThingType_id
435         elif category == "ThingAction":
436             id_store = command_taid
437             decorator = test_ThingAction_id
438
439         @decorator
440         def f(val_string):
441             val = integer_test(val_string, min, max)
442             if None != val:
443                 world_db[category + "s"][id_store.id][key] = val
444     return f
445
446
447 def build_fov_map(t):
448     """Build Thing's FOV map."""
449     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
450     # DUMMY so far. Just builds an all-visible map.
451
452
453 def new_Thing(type):
454     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
455     thing = {
456         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
457         "T_ARGUMENT": 0,
458         "T_PROGRESS": 0,
459         "T_SATIATION": 0,
460         "T_COMMAND": 0,
461         "T_TYPE": type,
462         "T_POSY": 0,
463         "T_POSX": 0,
464         "T_CARRIES": [],
465         "carried": False,
466         "T_MEMTHING": [],
467         "T_MEMMAP": False,
468         "T_MEMDEPTHMAP": False,
469         "fovmap": False
470     }
471     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
472         build_fov_map(thing)
473     return thing
474
475
476 def id_setter(id, category, id_store=False, start_at_1=False):
477     """Set ID of object of category to manipulate ID unused? Create new one.
478
479     The ID is stored as id_store.id (if id_store is set). If the integer of the
480     input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
481     32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
482     >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
483     new object is created, otherwise the new object's ID.
484     """
485     min = 0 if start_at_1 else -32768
486     max = 255 if start_at_1 else 32767
487     if str == type(id):
488         id = integer_test(id, min, max)
489     if None != id:
490         if id in world_db[category]:
491             if id_store:
492                 id_store.id = id
493             return None
494         else:
495             if (start_at_1 and 0 == id) \
496                or ((not start_at_1) and (id < 0 or id > 255)):
497                 id = -1
498                 while 1:
499                     id = id + 1
500                     if id not in world_db[category]:
501                         break
502                 if id > 255:
503                     print("Ignoring: "
504                           "No unused ID available to add to ID list.")
505                     return None
506             if id_store:
507                 id_store.id = id
508     return id
509
510
511 def command_ping():
512     """Send PONG line to server output file."""
513     strong_write(io_db["file_out"], "PONG\n")
514
515
516 def command_quit():
517     """Abort server process."""
518     raise SystemExit("received QUIT command")
519
520
521 def command_thingshere(y, x):
522     # DUMMY
523     print("Ignoring not-yet implemented THINGS_HERE command.")
524
525
526 def command_seedmap(seed_string):
527     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
528     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
529     remake_map()
530
531
532 def command_makeworld(seed_string):
533     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
534
535     Make seed world_db["SEED_RANDOMNESS"] and world_db["SEED_MAP"]. Do more
536     only with a "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType
537     of TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
538     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
539     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
540     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
541     other. Init player's memory map. Write "NEW_WORLD" line to out file.
542     """
543     setter(None, "SEED_RANDOMNESS", 0, 4294967295)(seed_string)
544     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
545     player_will_be_generated = False
546     playertype = world_db["PLAYER_TYPE"]
547     for ThingType in world_db["ThingTypes"]:
548         if playertype == ThingType:
549             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
550                 player_will_be_generated = True
551             break
552     if not player_will_be_generated:
553         print("Ignoring beyond SEED_MAP: " +
554               "No player type with start number >0 defined.")
555         return
556     wait_action = False
557     for ThingAction in world_db["ThingActions"]:
558         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
559             wait_action = True
560     if not wait_action:
561         print("Ignoring beyond SEED_MAP: " +
562               "No thing action with name 'wait' defined.")
563         return
564     world_db["Things"] = {}
565     remake_map()
566     world_db["WORLD_ACTIVE"] = 1
567     world_db["TURN"] = 1
568     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
569         id = id_setter(-1, "Things")
570         world_db["Things"][id] = new_Thing(playertype)
571     # TODO: Positioning.
572     update_map_memory(world_db["Things"][0])
573     for type in world_db["ThingTypes"]:
574         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
575             id = id_setter(-1, "Things")
576             world_db["Things"][id] = new_Thing(playertype)
577     # TODO: Positioning.
578     strong_write(io_db["file_out"], "NEW_WORLD\n")
579
580
581 def command_maplength(maplength_string):
582     """Redefine map length. Invalidate map, therefore lose all things on it."""
583     set_world_inactive()
584     world_db["Things"] = {}
585     setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
586
587
588 def command_worldactive(worldactive_string):
589     """Toggle world_db["WORLD_ACTIVE"] if possible.
590
591     An active world can always be set inactive. An inactive world can only be
592     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
593     activation, rebuild all Things' FOVs, and the player's map memory.
594     """
595     # In original version, map existence was also tested (unnecessarily?).
596     val = integer_test(worldactive_string, 0, 1)
597     if val:
598         if 0 != world_db["WORLD_ACTIVE"]:
599             if 0 == val:
600                 set_world_inactive()
601             else:
602                 print("World already active.")
603         elif 0 == world_db["WORLD_ACTIVE"]:
604             wait_exists = False
605             for ThingAction in world_db["ThingActions"]:
606                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
607                     wait_exists = True
608                     break
609             player_exists = False
610             for Thing in world_db["Things"]:
611                 if 0 == Thing:
612                     player_exists = True
613                     break
614             if wait_exists and player_exists:
615                 for id in world_db["Things"]:
616                     if world_db["Things"][id]["T_LIFEPOINTS"]:
617                         build_fov_map(world_db["Things"][id])
618                         if 0 == id:
619                             update_map_memory(world_db["Things"][id])
620                 world_db["WORLD_ACTIVE"] = 1
621
622
623 def test_for_id_maker(object, category):
624     """Return decorator testing for object having "id" attribute."""
625     def decorator(f):
626         def helper(*args):
627             if hasattr(object, "id"):
628                 f(*args)
629             else:
630                 print("Ignoring: No " + category +
631                       " defined to manipulate yet.")
632         return helper
633     return decorator
634
635
636 def command_tid(id_string):
637     """Set ID of Thing to manipulate. ID unused? Create new one.
638
639     Default new Thing's type to the first available ThingType, others: zero.
640     """
641     id = id_setter(id_string, "Things", command_tid)
642     if None != id:
643         if world_db["ThingTypes"] == {}:
644             print("Ignoring: No ThingType to settle new Thing in.")
645             return
646         type = list(world_db["ThingTypes"].keys())[0]
647         world_db["Things"][id] = new_Thing(type)
648
649
650 test_Thing_id = test_for_id_maker(command_tid, "Thing")
651
652
653 @test_Thing_id
654 def command_tcommand(str_int):
655     """Set T_COMMAND of selected Thing."""
656     val = integer_test(str_int, 0, 255)
657     if None != val:
658         if 0 == val or val in world_db["ThingActions"]:
659             world_db["Things"][command_tid.id]["T_COMMAND"] = val
660         else:
661             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
662
663
664 @test_Thing_id
665 def command_ttype(str_int):
666     """Set T_TYPE of selected Thing."""
667     val = integer_test(str_int, 0, 255)
668     if None != val:
669         if val in world_db["ThingTypes"]:
670             world_db["Things"][command_tid.id]["T_TYPE"] = val
671         else:
672             print("Ignoring: ThingType ID belongs to no known ThingType.")
673
674
675 @test_Thing_id
676 def command_tcarries(str_int):
677     """Append int(str_int) to T_CARRIES of selected Thing.
678
679     The ID int(str_int) must not be of the selected Thing, and must belong to a
680     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
681     """
682     val = integer_test(str_int, 0, 255)
683     if None != val:
684         if val == command_tid.id:
685             print("Ignoring: Thing cannot carry itself.")
686         elif val in world_db["Things"] \
687              and not world_db["Things"][val]["carried"]:
688             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
689             world_db["Things"][val]["carried"] = True
690         else:
691             print("Ignoring: Thing not available for carrying.")
692     # Note that the whole carrying structure is different from the C version:
693     # Carried-ness is marked by a "carried" flag, not by Things containing
694     # Things internally.
695
696
697 @test_Thing_id
698 def command_tmemthing(str_t, str_y, str_x):
699     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
700
701     The type must fit to an existing ThingType, and the position into the map.
702     """
703     type = integer_test(str_t, 0, 255)
704     posy = integer_test(str_y, 0, 255)
705     posx = integer_test(str_x, 0, 255)
706     if None != type and None != posy and None != posx:
707         if type not in world_db["ThingTypes"] \
708            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
709             print("Ignoring: Illegal value for thing type or position.")
710         else:
711             memthing = (type, posy, posx)
712             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
713
714
715 def setter_map(maptype):
716     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
717
718     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
719     """
720     @test_Thing_id
721     def helper(str_int, mapline):
722         val = integer_test(str_int, 0, 255)
723         if None != val:
724             if val >= world_db["MAP_LENGTH"]:
725                 print("Illegal value for map line number.")
726             elif len(mapline) != world_db["MAP_LENGTH"]:
727                 print("Map line length is unequal map width.")
728             else:
729                 length = world_db["MAP_LENGTH"]
730                 map = None
731                 if not world_db["Things"][command_tid.id][maptype]:
732                     map = bytearray(b' ' * (length ** 2))
733                 else:
734                     map = world_db["Things"][command_tid.id][maptype]
735                 map[val * length:(val * length) + length] = mapline.encode()
736                 world_db["Things"][command_tid.id][maptype] = map
737     return helper
738
739
740 def setter_tpos(axis):
741     """Generate setter for T_POSX or  T_POSY of selected Thing.
742
743     If world is active, rebuilds animate things' fovmap, player's memory map.
744     """
745     @test_Thing_id
746     def helper(str_int):
747         val = integer_test(str_int, 0, 255)
748         if None != val:
749             if val < world_db["MAP_LENGTH"]:
750                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
751                 if world_db["WORLD_ACTIVE"] \
752                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
753                     build_fov_map(world_db["Things"][command_tid.id])
754                     if 0 == command_tid.id:
755                         update_map_memory(world_db["Things"][command_tid.id])
756             else:
757                 print("Ignoring: Position is outside of map.")
758     return helper
759
760
761 def command_ttid(id_string):
762     """Set ID of ThingType to manipulate. ID unused? Create new one.
763
764     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
765     """
766     id = id_setter(id_string, "ThingTypes", command_ttid)
767     if None != id:
768         world_db["ThingTypes"][id] = {
769             "TT_NAME": "(none)",
770             "TT_CONSUMABLE": 0,
771             "TT_LIFEPOINTS": 0,
772             "TT_PROLIFERATE": 0,
773             "TT_START_NUMBER": 0,
774             "TT_SYMBOL": "?",
775             "TT_CORPSE_ID": id
776         }
777
778
779 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
780
781
782 @test_ThingType_id
783 def command_ttname(name):
784     """Set TT_NAME of selected ThingType."""
785     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
786
787
788 @test_ThingType_id
789 def command_ttsymbol(char):
790     """Set TT_SYMBOL of selected ThingType. """
791     if 1 == len(char):
792         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
793     else:
794         print("Ignoring: Argument must be single character.")
795
796
797 @test_ThingType_id
798 def command_ttcorpseid(str_int):
799     """Set TT_CORPSE_ID of selected ThingType."""
800     val = integer_test(str_int, 0, 255)
801     if None != val:
802         if val in world_db["ThingTypes"]:
803             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
804         else:
805             print("Ignoring: Corpse ID belongs to no known ThignType.")
806
807
808 def command_taid(id_string):
809     """Set ID of ThingAction to manipulate. ID unused? Create new one.
810
811     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
812     """
813     id = id_setter(id_string, "ThingActions", command_taid, True)
814     if None != id:
815         world_db["ThingActions"][id] = {
816             "TA_EFFORT": 1,
817             "TA_NAME": "wait"
818         }
819
820
821 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
822
823
824 @test_ThingAction_id
825 def command_taname(name):
826     """Set TA_NAME of selected ThingAction.
827
828     The name must match a valid thing action function. If after the name
829     setting no ThingAction with name "wait" remains, call set_world_inactive().
830     """
831     if name == "wait" or name == "move" or name == "use" or name == "drop" \
832        or name == "pick_up":
833         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
834         if 1 == world_db["WORLD_ACTIVE"]:
835             wait_defined = False
836             for id in world_db["ThingActions"]:
837                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
838                     wait_defined = True
839                     break
840             if not wait_defined:
841                 set_world_inactive()
842     else:
843         print("Ignoring: Invalid action name.")
844     # In contrast to the original,naming won't map a function to a ThingAction.
845
846
847 """Commands database.
848
849 Map command start tokens to ([0]) number of expected command arguments, ([1])
850 the command's meta-ness (i.e. is it to be written to the record file, is it to
851 be ignored in replay mode if read from server input file), and ([2]) a function
852 to be called on it.
853 """
854 commands_db = {
855     "QUIT": (0, True, command_quit),
856     "PING": (0, True, command_ping),
857     "THINGS_HERE": (2, True, command_thingshere),
858     "MAKE_WORLD": (1, False, command_makeworld),
859     "SEED_MAP": (1, False, command_seedmap),
860     "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
861                                          0, 4294967295)),
862     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
863     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
864     "MAP_LENGTH": (1, False, command_maplength),
865     "WORLD_ACTIVE": (1, False, command_worldactive),
866     "TA_ID": (1, False, command_taid),
867     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
868     "TA_NAME": (1, False, command_taname),
869     "TT_ID": (1, False, command_ttid),
870     "TT_NAME": (1, False, command_ttname),
871     "TT_SYMBOL": (1, False, command_ttsymbol),
872     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
873     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
874                                        0, 65535)),
875     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
876                                          0, 255)),
877     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
878                                         0, 255)),
879     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
880     "T_ID": (1, False, command_tid),
881     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
882     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
883     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
884     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
885     "T_COMMAND": (1, False, command_tcommand),
886     "T_TYPE": (1, False, command_ttype),
887     "T_CARRIES": (1, False, command_tcarries),
888     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
889     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
890     "T_MEMTHING": (3, False, command_tmemthing),
891     "T_POSY": (1, False, setter_tpos("Y")),
892     "T_POSX": (1, False, setter_tpos("X")),
893 }
894
895
896 """World state database. With sane default values."""
897 world_db = {
898     "TURN": 0,
899     "SEED_MAP": 0,
900     "SEED_RANDOMNESS": 0,
901     "PLAYER_TYPE": 0,
902     "MAP_LENGTH": 64,
903     "WORLD_ACTIVE": 0,
904     "ThingActions": {},
905     "ThingTypes": {},
906     "Things": {}
907 }
908
909
910 """File IO database."""
911 io_db = {
912     "path_save": "save",
913     "path_record": "record",
914     "path_worldconf": "confserver/world",
915     "path_server": "server/",
916     "path_in": "server/in",
917     "path_out": "server/out",
918     "path_worldstate": "server/worldstate",
919     "tmp_suffix": "_tmp",
920     "kicked_by_rival": False,
921     "worldstate_updateable": False
922 }
923
924
925 try:
926     opts = parse_command_line_arguments()
927     setup_server_io()
928     # print("DUMMY: Run game.")
929     if None != opts.replay:
930         replay_game()
931     else:
932         play_game()
933 except SystemExit as exit:
934     print("ABORTING: " + exit.args[0])
935 except:
936     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
937     raise
938 finally:
939     cleanup_server_io()
940     # print("DUMMY: (Clean up C heap.)")