home · contact · privacy
73c920e265344789e896cbd1b8e35715a1dc8b1e
[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(str_y, str_x):
522     """Write to out file list of Things known to player at coordinate y, x."""
523     def write_thing_if_here():
524         if y == world_db["Things"][id]["T_POSY"] \
525            and x == world_db["Things"][id]["T_POSX"]:
526             type = world_db["Things"][id]["T_TYPE"]
527             name = world_db["ThingTypes"][type]["TT_NAME"]
528             strong_write(io_db["file_out"], name + "\n")
529     if world_db["WORLD_ACTIVE"]:
530         y = integer_test(str_y, 0, 255)
531         x = integer_test(str_x, 0, 255)
532         length = world_db["MAP_LENGTH"]
533         if None != y and None != x and y < length and x < length:
534             pos = (y * world_db["MAP_LENGTH"]) + x
535             strong_write(io_db["file_out"], "THINGS_HERE START\n")
536             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
537                 for id in world_db["Things"]:
538                     write_thing_if_here()
539             else:
540                 for id in world_db["Things"]["T_MEMTHING"]:
541                     write_thing_if_here()
542             strong_write(io_db["file_out"], "THINGS_HERE END\n")
543         else:
544             print("Ignoring: Invalid map coordinates.")
545     else:
546         print("Ignoring: Command only works on existing worlds.")
547
548
549 def command_seedmap(seed_string):
550     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
551     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
552     remake_map()
553
554
555 def command_makeworld(seed_string):
556     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
557
558     Make seed world_db["SEED_RANDOMNESS"] and world_db["SEED_MAP"]. Do more
559     only with a "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType
560     of TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
561     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
562     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
563     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
564     other. Init player's memory map. Write "NEW_WORLD" line to out file.
565     """
566     setter(None, "SEED_RANDOMNESS", 0, 4294967295)(seed_string)
567     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
568     player_will_be_generated = False
569     playertype = world_db["PLAYER_TYPE"]
570     for ThingType in world_db["ThingTypes"]:
571         if playertype == ThingType:
572             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
573                 player_will_be_generated = True
574             break
575     if not player_will_be_generated:
576         print("Ignoring beyond SEED_MAP: " +
577               "No player type with start number >0 defined.")
578         return
579     wait_action = False
580     for ThingAction in world_db["ThingActions"]:
581         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
582             wait_action = True
583     if not wait_action:
584         print("Ignoring beyond SEED_MAP: " +
585               "No thing action with name 'wait' defined.")
586         return
587     world_db["Things"] = {}
588     remake_map()
589     world_db["WORLD_ACTIVE"] = 1
590     world_db["TURN"] = 1
591     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
592         id = id_setter(-1, "Things")
593         world_db["Things"][id] = new_Thing(playertype)
594     # TODO: Positioning.
595     update_map_memory(world_db["Things"][0])
596     for type in world_db["ThingTypes"]:
597         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
598             if type != playertype:
599                 id = id_setter(-1, "Things")
600                 world_db["Things"][id] = new_Thing(type)
601     # TODO: Positioning.
602     strong_write(io_db["file_out"], "NEW_WORLD\n")
603
604
605 def command_maplength(maplength_string):
606     """Redefine map length. Invalidate map, therefore lose all things on it."""
607     set_world_inactive()
608     world_db["Things"] = {}
609     setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
610
611
612 def command_worldactive(worldactive_string):
613     """Toggle world_db["WORLD_ACTIVE"] if possible.
614
615     An active world can always be set inactive. An inactive world can only be
616     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
617     activation, rebuild all Things' FOVs, and the player's map memory.
618     """
619     # In original version, map existence was also tested (unnecessarily?).
620     val = integer_test(worldactive_string, 0, 1)
621     if val:
622         if 0 != world_db["WORLD_ACTIVE"]:
623             if 0 == val:
624                 set_world_inactive()
625             else:
626                 print("World already active.")
627         elif 0 == world_db["WORLD_ACTIVE"]:
628             wait_exists = False
629             for ThingAction in world_db["ThingActions"]:
630                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
631                     wait_exists = True
632                     break
633             player_exists = False
634             for Thing in world_db["Things"]:
635                 if 0 == Thing:
636                     player_exists = True
637                     break
638             if wait_exists and player_exists:
639                 for id in world_db["Things"]:
640                     if world_db["Things"][id]["T_LIFEPOINTS"]:
641                         build_fov_map(world_db["Things"][id])
642                         if 0 == id:
643                             update_map_memory(world_db["Things"][id])
644                 world_db["WORLD_ACTIVE"] = 1
645
646
647 def test_for_id_maker(object, category):
648     """Return decorator testing for object having "id" attribute."""
649     def decorator(f):
650         def helper(*args):
651             if hasattr(object, "id"):
652                 f(*args)
653             else:
654                 print("Ignoring: No " + category +
655                       " defined to manipulate yet.")
656         return helper
657     return decorator
658
659
660 def command_tid(id_string):
661     """Set ID of Thing to manipulate. ID unused? Create new one.
662
663     Default new Thing's type to the first available ThingType, others: zero.
664     """
665     id = id_setter(id_string, "Things", command_tid)
666     if None != id:
667         if world_db["ThingTypes"] == {}:
668             print("Ignoring: No ThingType to settle new Thing in.")
669             return
670         type = list(world_db["ThingTypes"].keys())[0]
671         world_db["Things"][id] = new_Thing(type)
672
673
674 test_Thing_id = test_for_id_maker(command_tid, "Thing")
675
676
677 @test_Thing_id
678 def command_tcommand(str_int):
679     """Set T_COMMAND of selected Thing."""
680     val = integer_test(str_int, 0, 255)
681     if None != val:
682         if 0 == val or val in world_db["ThingActions"]:
683             world_db["Things"][command_tid.id]["T_COMMAND"] = val
684         else:
685             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
686
687
688 @test_Thing_id
689 def command_ttype(str_int):
690     """Set T_TYPE of selected Thing."""
691     val = integer_test(str_int, 0, 255)
692     if None != val:
693         if val in world_db["ThingTypes"]:
694             world_db["Things"][command_tid.id]["T_TYPE"] = val
695         else:
696             print("Ignoring: ThingType ID belongs to no known ThingType.")
697
698
699 @test_Thing_id
700 def command_tcarries(str_int):
701     """Append int(str_int) to T_CARRIES of selected Thing.
702
703     The ID int(str_int) must not be of the selected Thing, and must belong to a
704     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
705     """
706     val = integer_test(str_int, 0, 255)
707     if None != val:
708         if val == command_tid.id:
709             print("Ignoring: Thing cannot carry itself.")
710         elif val in world_db["Things"] \
711              and not world_db["Things"][val]["carried"]:
712             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
713             world_db["Things"][val]["carried"] = True
714         else:
715             print("Ignoring: Thing not available for carrying.")
716     # Note that the whole carrying structure is different from the C version:
717     # Carried-ness is marked by a "carried" flag, not by Things containing
718     # Things internally.
719
720
721 @test_Thing_id
722 def command_tmemthing(str_t, str_y, str_x):
723     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
724
725     The type must fit to an existing ThingType, and the position into the map.
726     """
727     type = integer_test(str_t, 0, 255)
728     posy = integer_test(str_y, 0, 255)
729     posx = integer_test(str_x, 0, 255)
730     if None != type and None != posy and None != posx:
731         if type not in world_db["ThingTypes"] \
732            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
733             print("Ignoring: Illegal value for thing type or position.")
734         else:
735             memthing = (type, posy, posx)
736             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
737
738
739 def setter_map(maptype):
740     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
741
742     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
743     """
744     @test_Thing_id
745     def helper(str_int, mapline):
746         val = integer_test(str_int, 0, 255)
747         if None != val:
748             if val >= world_db["MAP_LENGTH"]:
749                 print("Illegal value for map line number.")
750             elif len(mapline) != world_db["MAP_LENGTH"]:
751                 print("Map line length is unequal map width.")
752             else:
753                 length = world_db["MAP_LENGTH"]
754                 map = None
755                 if not world_db["Things"][command_tid.id][maptype]:
756                     map = bytearray(b' ' * (length ** 2))
757                 else:
758                     map = world_db["Things"][command_tid.id][maptype]
759                 map[val * length:(val * length) + length] = mapline.encode()
760                 world_db["Things"][command_tid.id][maptype] = map
761     return helper
762
763
764 def setter_tpos(axis):
765     """Generate setter for T_POSX or  T_POSY of selected Thing.
766
767     If world is active, rebuilds animate things' fovmap, player's memory map.
768     """
769     @test_Thing_id
770     def helper(str_int):
771         val = integer_test(str_int, 0, 255)
772         if None != val:
773             if val < world_db["MAP_LENGTH"]:
774                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
775                 if world_db["WORLD_ACTIVE"] \
776                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
777                     build_fov_map(world_db["Things"][command_tid.id])
778                     if 0 == command_tid.id:
779                         update_map_memory(world_db["Things"][command_tid.id])
780             else:
781                 print("Ignoring: Position is outside of map.")
782     return helper
783
784
785 def command_ttid(id_string):
786     """Set ID of ThingType to manipulate. ID unused? Create new one.
787
788     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
789     """
790     id = id_setter(id_string, "ThingTypes", command_ttid)
791     if None != id:
792         world_db["ThingTypes"][id] = {
793             "TT_NAME": "(none)",
794             "TT_CONSUMABLE": 0,
795             "TT_LIFEPOINTS": 0,
796             "TT_PROLIFERATE": 0,
797             "TT_START_NUMBER": 0,
798             "TT_SYMBOL": "?",
799             "TT_CORPSE_ID": id
800         }
801
802
803 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
804
805
806 @test_ThingType_id
807 def command_ttname(name):
808     """Set TT_NAME of selected ThingType."""
809     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
810
811
812 @test_ThingType_id
813 def command_ttsymbol(char):
814     """Set TT_SYMBOL of selected ThingType. """
815     if 1 == len(char):
816         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
817     else:
818         print("Ignoring: Argument must be single character.")
819
820
821 @test_ThingType_id
822 def command_ttcorpseid(str_int):
823     """Set TT_CORPSE_ID of selected ThingType."""
824     val = integer_test(str_int, 0, 255)
825     if None != val:
826         if val in world_db["ThingTypes"]:
827             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
828         else:
829             print("Ignoring: Corpse ID belongs to no known ThignType.")
830
831
832 def command_taid(id_string):
833     """Set ID of ThingAction to manipulate. ID unused? Create new one.
834
835     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
836     """
837     id = id_setter(id_string, "ThingActions", command_taid, True)
838     if None != id:
839         world_db["ThingActions"][id] = {
840             "TA_EFFORT": 1,
841             "TA_NAME": "wait"
842         }
843
844
845 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
846
847
848 @test_ThingAction_id
849 def command_taname(name):
850     """Set TA_NAME of selected ThingAction.
851
852     The name must match a valid thing action function. If after the name
853     setting no ThingAction with name "wait" remains, call set_world_inactive().
854     """
855     if name == "wait" or name == "move" or name == "use" or name == "drop" \
856        or name == "pick_up":
857         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
858         if 1 == world_db["WORLD_ACTIVE"]:
859             wait_defined = False
860             for id in world_db["ThingActions"]:
861                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
862                     wait_defined = True
863                     break
864             if not wait_defined:
865                 set_world_inactive()
866     else:
867         print("Ignoring: Invalid action name.")
868     # In contrast to the original,naming won't map a function to a ThingAction.
869
870
871 """Commands database.
872
873 Map command start tokens to ([0]) number of expected command arguments, ([1])
874 the command's meta-ness (i.e. is it to be written to the record file, is it to
875 be ignored in replay mode if read from server input file), and ([2]) a function
876 to be called on it.
877 """
878 commands_db = {
879     "QUIT": (0, True, command_quit),
880     "PING": (0, True, command_ping),
881     "THINGS_HERE": (2, True, command_thingshere),
882     "MAKE_WORLD": (1, False, command_makeworld),
883     "SEED_MAP": (1, False, command_seedmap),
884     "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
885                                          0, 4294967295)),
886     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
887     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
888     "MAP_LENGTH": (1, False, command_maplength),
889     "WORLD_ACTIVE": (1, False, command_worldactive),
890     "TA_ID": (1, False, command_taid),
891     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
892     "TA_NAME": (1, False, command_taname),
893     "TT_ID": (1, False, command_ttid),
894     "TT_NAME": (1, False, command_ttname),
895     "TT_SYMBOL": (1, False, command_ttsymbol),
896     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
897     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
898                                        0, 65535)),
899     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
900                                          0, 255)),
901     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
902                                         0, 255)),
903     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
904     "T_ID": (1, False, command_tid),
905     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
906     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
907     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
908     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
909     "T_COMMAND": (1, False, command_tcommand),
910     "T_TYPE": (1, False, command_ttype),
911     "T_CARRIES": (1, False, command_tcarries),
912     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
913     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
914     "T_MEMTHING": (3, False, command_tmemthing),
915     "T_POSY": (1, False, setter_tpos("Y")),
916     "T_POSX": (1, False, setter_tpos("X")),
917 }
918
919
920 """World state database. With sane default values."""
921 world_db = {
922     "TURN": 0,
923     "SEED_MAP": 0,
924     "SEED_RANDOMNESS": 0,
925     "PLAYER_TYPE": 0,
926     "MAP_LENGTH": 64,
927     "WORLD_ACTIVE": 0,
928     "ThingActions": {},
929     "ThingTypes": {},
930     "Things": {}
931 }
932
933
934 """File IO database."""
935 io_db = {
936     "path_save": "save",
937     "path_record": "record",
938     "path_worldconf": "confserver/world",
939     "path_server": "server/",
940     "path_in": "server/in",
941     "path_out": "server/out",
942     "path_worldstate": "server/worldstate",
943     "tmp_suffix": "_tmp",
944     "kicked_by_rival": False,
945     "worldstate_updateable": False
946 }
947
948
949 try:
950     opts = parse_command_line_arguments()
951     setup_server_io()
952     if None != opts.replay:
953         replay_game()
954     else:
955         play_game()
956 except SystemExit as exit:
957     print("ABORTING: " + exit.args[0])
958 except:
959     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
960     raise
961 finally:
962     cleanup_server_io()