home · contact · privacy
Server/py: Improve comments.
[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                 rmap = world_db["Things"][id][key]
138                 length = world_db["MAP_LENGTH"]
139                 for i in range(length):
140                     line = rmap[i * length:(i * length) + length].decode()
141                     string = string + key + " " + str(i) + quote(line) + "\n"
142             return string
143         return helper
144
145     def memthing(id):
146         string = ""
147         for memthing in world_db["Things"][id]["T_MEMTHING"]:
148             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
149                      str(memthing[1]) + " " + str(memthing[2]) + "\n"
150         return string
151
152     def helper(category, id_string, special_keys={}):
153         string = ""
154         for id in world_db[category]:
155             string = string + id_string + " " + str(id) + "\n"
156             for key in world_db[category][id]:
157                 if not key in special_keys:
158                     x = world_db[category][id][key]
159                     argument = quote(x) if str == type(x) else str(x)
160                     string = string + key + " " + argument + "\n"
161                 elif special_keys[key]:
162                     string = string + special_keys[key](id)
163         return string
164
165     string = ""
166     for key in world_db:
167         if dict != type(world_db[key]) and key != "MAP":
168             string = string + key + " " + str(world_db[key]) + "\n"
169     string = string + helper("ThingActions", "TA_ID")
170     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
171     for id in world_db["ThingTypes"]:
172         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
173                  str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
174     string = string + helper("Things", "T_ID",
175                              {"T_CARRIES": False, "carried": False,
176                               "T_MEMMAP": mapsetter("T_MEMMAP"),
177                               "T_MEMTHING": memthing,
178                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
179     for id in world_db["Things"]:
180         if [] != world_db["Things"][id]["T_CARRIES"]:
181             string = string + "T_ID " + str(id) + "\n"
182             for carried_id in world_db["Things"][id]["T_CARRIES"]:
183                 string = string + "T_CARRIES " + str(carried_id) + "\n"
184     string = string + "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
185     atomic_write(io_db["path_save"], string)
186
187
188 def obey_lines_in_file(path, name, do_record=False):
189     """Call obey() on each line of path's file, use name in input prefix."""
190     file = open(path, "r")
191     line_n = 1
192     for line in file.readlines():
193         obey(line.rstrip(), name + "file line " + str(line_n),
194              do_record=do_record)
195         line_n = line_n + 1
196     file.close()
197
198
199 def parse_command_line_arguments():
200     """Return settings values read from command line arguments."""
201     parser = argparse.ArgumentParser()
202     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
203                         action='store')
204     opts, unknown = parser.parse_known_args()
205     return opts
206
207
208 def server_test():
209     """Ensure valid server out file belonging to current process.
210
211     This is done by comparing io_db["teststring"] to what's found at the start
212     of the current file at io_db["path_out"]. On failure, set
213     io_db["kicked_by_rival"] and raise SystemExit.
214     """
215     if not os.access(io_db["path_out"], os.F_OK):
216         raise SystemExit("Server output file has disappeared.")
217     file = open(io_db["path_out"], "r")
218     test = file.readline().rstrip("\n")
219     file.close()
220     if test != io_db["teststring"]:
221         io_db["kicked_by_rival"] = True
222         msg = "Server test string in server output file does not match. This" \
223               " indicates that the current server process has been " \
224               "superseded by another one."
225         raise SystemExit(msg)
226
227
228 def read_command():
229     """Return next newline-delimited command from server in file.
230
231     Keep building return string until a newline is encountered. Pause between
232     unsuccessful reads, and after too much waiting, run server_test().
233     """
234     wait_on_fail = 1
235     max_wait = 5
236     now = time.time()
237     command = ""
238     while True:
239         add = io_db["file_in"].readline()
240         if len(add) > 0:
241             command = command + add
242             if len(command) > 0 and "\n" == command[-1]:
243                 command = command[:-1]
244                 break
245         else:
246             time.sleep(wait_on_fail)
247             if now + max_wait < time.time():
248                 server_test()
249                 now = time.time()
250     return command
251
252
253 def try_worldstate_update():
254     """Write worldstate file if io_db["worldstate_updateable"] is set."""
255     if io_db["worldstate_updateable"]:
256         inventory = ""
257         if [] == world_db["Things"][0]["T_CARRIES"]:
258             inventory = "(none)\n"
259         else:
260             for id in world_db["Things"][0]["T_CARRIES"]:
261                 type_id = world_db["Things"][id]["T_TYPE"]
262                 name = world_db["ThingTypes"][type_id]["TT_NAME"]
263                 inventory = inventory + name + "\n"
264         string = str(world_db["TURN"]) + "\n" + \
265                  str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
266                  str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
267                  inventory + "%\n" + \
268                  str(world_db["Things"][0]["T_POSY"]) + "\n" + \
269                  str(world_db["Things"][0]["T_POSX"]) + "\n" + \
270                  str(world_db["MAP_LENGTH"]) + "\n"
271         length = world_db["MAP_LENGTH"]
272         for i in range(length):
273             line = world_db["MAP"][i * length:(i * length) + length].decode()
274             string = string + line + "\n"
275         # TODO: no proper user-subjective map
276         atomic_write(io_db["path_worldstate"], string)
277         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
278         io_db["worldstate_updateable"] = False
279
280
281 def replay_game():
282     """Replay game from record file.
283
284     Use opts.replay as breakpoint turn to which to replay automatically before
285     switching to manual input by non-meta commands in server input file
286     triggering further reads of record file. Ensure opts.replay is at least 1.
287     Run try_worldstate_update() before each interactive obey()/read_command().
288     """
289     if opts.replay < 1:
290         opts.replay = 1
291     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
292           " (if so late a turn is to be found).")
293     if not os.access(io_db["path_record"], os.F_OK):
294         raise SystemExit("No record file found to replay.")
295     io_db["file_record"] = open(io_db["path_record"], "r")
296     io_db["file_record"].prefix = "record file line "
297     io_db["file_record"].line_n = 1
298     while world_db["TURN"] < opts.replay:
299         line = io_db["file_record"].readline()
300         if "" == line:
301             break
302         obey(line.rstrip(), io_db["file_record"].prefix
303              + str(io_db["file_record"].line_n))
304         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
305     while True:
306         try_worldstate_update()
307         obey(read_command(), "in file", replay=True)
308
309
310 def play_game():
311     """Play game by server input file commands. Before, load save file found.
312
313     If no save file is found, a new world is generated from the commands in the
314     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
315     command and all that follow via the server input file. Run
316     try_worldstate_update() before each interactive obey()/read_command().
317     """
318     if os.access(io_db["path_save"], os.F_OK):
319         obey_lines_in_file(io_db["path_save"], "save")
320     else:
321         if not os.access(io_db["path_worldconf"], os.F_OK):
322             msg = "No world config file from which to start a new world."
323             raise SystemExit(msg)
324         obey_lines_in_file(io_db["path_worldconf"], "world config ",
325                            do_record=True)
326         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
327     while True:
328         try_worldstate_update()
329         obey(read_command(), "in file", do_record=True)
330
331
332 def remake_map():
333     # DUMMY map creator.
334     world_db["MAP"] = bytearray(b'.' * (world_db["MAP_LENGTH"] ** 2))
335
336
337 def set_world_inactive():
338     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
339     server_test()
340     if os.access(io_db["path_worldstate"], os.F_OK):
341         os.remove(io_db["path_worldstate"])
342     world_db["WORLD_ACTIVE"] = 0
343
344
345 def integer_test(val_string, min, max):
346     """Return val_string if possible integer >= min and <= max, else None."""
347     try:
348         val = int(val_string)
349         if val < min or val > max:
350             raise ValueError
351         return val
352     except ValueError:
353         print("Ignoring: Please use integer >= " + str(min) + " and <= " +
354               str(max) + ".")
355         return None
356
357
358 def setter(category, key, min, max):
359     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
360     if category is None:
361         def f(val_string):
362             val = integer_test(val_string, min, max)
363             if None != val:
364                 world_db[key] = val
365     else:
366         if category == "Thing":
367             id_store = command_tid
368             decorator = test_Thing_id
369         elif category == "ThingType":
370             id_store = command_ttid
371             decorator = test_ThingType_id
372         elif category == "ThingAction":
373             id_store = command_taid
374             decorator = test_ThingAction_id
375
376         @decorator
377         def f(val_string):
378             val = integer_test(val_string, min, max)
379             if None != val:
380                 world_db[category + "s"][id_store.id][key] = val
381     return f
382
383
384 def new_Thing(type):
385     """Return prototype for Thing of T_TYPE of type."""
386     return {
387         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
388         "T_ARGUMENT": 0,
389         "T_PROGRESS": 0,
390         "T_SATIATION": 0,
391         "T_COMMAND": 0,
392         "T_TYPE": type,
393         "T_POSY": 0,
394         "T_POSX": 0,
395         "T_CARRIES": [],
396         "carried": False,
397         "T_MEMTHING": [],
398         "T_MEMMAP": False,
399         "T_MEMDEPTHMAP": False
400     }
401
402
403 def id_setter(id, category, id_store=False, start_at_1=False):
404     """Set ID of object of category to manipulate ID unused? Create new one.
405
406     The ID is stored as id_store.id (if id_store is set). If the integer of the
407     input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
408     32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
409     >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
410     new object is created, otherwise the new object's ID.
411     """
412     min = 0 if start_at_1 else -32768
413     max = 255 if start_at_1 else 32767
414     if str == type(id):
415         id = integer_test(id, min, max)
416     if None != id:
417         if id in world_db[category]:
418             if id_store:
419                 id_store.id = id
420             return None
421         else:
422             if (start_at_1 and 0 == id) \
423                or ((not start_at_1) and (id < 0 or id > 255)):
424                 id = -1
425                 while 1:
426                     id = id + 1
427                     if id not in world_db[category]:
428                         break
429                 if id > 255:
430                     print("Ignoring: "
431                           "No unused ID available to add to ID list.")
432                     return None
433             if id_store:
434                 id_store.id = id
435     return id
436
437
438 def command_ping():
439     """Send PONG line to server output file."""
440     strong_write(io_db["file_out"], "PONG\n")
441
442
443 def command_quit():
444     """Abort server process."""
445     raise SystemExit("received QUIT command")
446
447
448 def command_thingshere(y, x):
449     # DUMMY
450     print("Ignoring not-yet implemented THINGS_HERE command.")
451
452
453 def command_seedmap(seed_string):
454     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
455     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
456     remake_map()
457
458
459 def command_makeworld(seed_string):
460     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
461
462     Make seed world_db["SEED_RANDOMNESS"] and world_db["SEED_MAP"]. Do more
463     only with a "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType
464     of TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
465     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
466     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
467     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
468     other. Init player's FOV/memory map. Write "NEW_WORLD" line to out file.
469     """
470     setter(None, "SEED_RANDOMNESS", 0, 4294967295)(seed_string)
471     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
472     player_will_be_generated = False
473     playertype = world_db["PLAYER_TYPE"]
474     for ThingType in world_db["ThingTypes"]:
475         if playertype == ThingType:
476             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
477                 player_will_be_generated = True
478             break
479     if not player_will_be_generated:
480         print("Ignoring beyond SEED_MAP: " +
481               "No player type with start number >0 defined.")
482         return
483     wait_action = False
484     for ThingAction in world_db["ThingActions"]:
485         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
486             wait_action = True
487     if not wait_action:
488         print("Ignoring beyond SEED_MAP: " +
489               "No thing action with name 'wait' defined.")
490         return
491     world_db["Things"] = {}
492     remake_map()
493     world_db["WORLD_ACTIVE"] = 1
494     world_db["TURN"] = 1
495     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
496         id = id_setter(-1, "Things")
497         world_db["Things"][id] = new_Thing(playertype)
498     # TODO: Positioning. Init player's FOV / memory map.
499     for type in world_db["ThingTypes"]:
500         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
501             id = id_setter(-1, "Things")
502             world_db["Things"][id] = new_Thing(playertype)
503     # TODO: Positioning.
504     strong_write(io_db["file_out"], "NEW_WORLD\n")
505
506
507 def command_maplength(maplength_string):
508     """Redefine map length. Invalidate map, therefore lose all things on it."""
509     set_world_inactive()
510     world_db["Things"] = {}
511     setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
512
513
514 def command_worldactive(worldactive_string):
515     """Toggle world_db["WORLD_ACTIVE"] if possible.
516
517     An active world can always be set inactive. An inactive world can only be
518     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
519     activation, rebuild all Things' FOVs and map memories.
520     """
521     # In original version, map existence was also tested (unnecessarily?).
522     val = integer_test(worldactive_string, 0, 1)
523     if val:
524         if 0 != world_db["WORLD_ACTIVE"]:
525             if 0 == val:
526                 set_world_inactive()
527             else:
528                 print("World already active.")
529         elif 0 == world_db["WORLD_ACTIVE"]:
530             wait_exists = False
531             for ThingAction in world_db["ThingActions"]:
532                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
533                     wait_exists = True
534                     break
535             player_exists = False
536             for Thing in world_db["Things"]:
537                 if 0 == Thing:
538                     player_exists = True
539                     break
540             if wait_exists and player_exists:
541                 # TODO: rebuild all things' FOVs, map memories
542                 world_db["WORLD_ACTIVE"] = 1
543
544
545 def test_for_id_maker(object, category):
546     """Return decorator testing for object having "id" attribute."""
547     def decorator(f):
548         def helper(*args):
549             if hasattr(object, "id"):
550                 f(*args)
551             else:
552                 print("Ignoring: No " + category +
553                       " defined to manipulate yet.")
554         return helper
555     return decorator
556
557
558 def command_tid(id_string):
559     """Set ID of Thing to manipulate. ID unused? Create new one.
560
561     Default new Thing's type to the first available ThingType, others: zero.
562     """
563     id = id_setter(id_string, "Things", command_tid)
564     if None != id:
565         if world_db["ThingTypes"] == {}:
566             print("Ignoring: No ThingType to settle new Thing in.")
567             return
568         type = list(world_db["ThingTypes"].keys())[0]
569         world_db["Things"][id] = new_Thing(type)
570
571
572 test_Thing_id = test_for_id_maker(command_tid, "Thing")
573
574
575 @test_Thing_id
576 def command_tcommand(str_int):
577     """Set T_COMMAND of selected Thing."""
578     val = integer_test(str_int, 0, 255)
579     if None != val:
580         if 0 == val or val in world_db["ThingActions"]:
581             world_db["Things"][command_tid.id]["T_COMMAND"] = val
582         else:
583             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
584
585
586 @test_Thing_id
587 def command_ttype(str_int):
588     """Set T_TYPE of selected Thing."""
589     val = integer_test(str_int, 0, 255)
590     if None != val:
591         if val in world_db["ThingTypes"]:
592             world_db["Things"][command_tid.id]["T_TYPE"] = val
593         else:
594             print("Ignoring: ThingType ID belongs to no known ThingType.")
595
596
597 @test_Thing_id
598 def command_tcarries(str_int):
599     """Append int(str_int) to T_CARRIES of selected Thing.
600
601     The ID int(str_int) must not be of the selected Thing, and must belong to a
602     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
603     """
604     val = integer_test(str_int, 0, 255)
605     if None != val:
606         if val == command_tid.id:
607             print("Ignoring: Thing cannot carry itself.")
608         elif val in world_db["Things"] \
609              and not world_db["Things"][val]["carried"]:
610             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
611             world_db["Things"][val]["carried"] = True
612         else:
613             print("Ignoring: Thing not available for carrying.")
614     # Note that the whole carrying structure is different from the C version:
615     # Carried-ness is marked by a "carried" flag, not by Things containing
616     # Things internally.
617
618
619 @test_Thing_id
620 def command_tmemthing(str_t, str_y, str_x):
621     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
622
623     The type must fit to an existing ThingType, and the position into the map.
624     """
625     type = integer_test(str_t, 0, 255)
626     posy = integer_test(str_y, 0, 255)
627     posx = integer_test(str_x, 0, 255)
628     if None != type and None != posy and None != posx:
629         if type not in world_db["ThingTypes"] \
630            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
631             print("Ignoring: Illegal value for thing type or position.")
632         else:
633             memthing = (type, posy, posx)
634             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
635
636
637 def setter_map(maptype):
638     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
639
640     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
641     """
642     @test_Thing_id
643     def helper(str_int, mapline):
644         val = integer_test(str_int, 0, 255)
645         if None != val:
646             if val >= world_db["MAP_LENGTH"]:
647                 print("Illegal value for map line number.")
648             elif len(mapline) != world_db["MAP_LENGTH"]:
649                 print("Map line length is unequal map width.")
650             else:
651                 length = world_db["MAP_LENGTH"]
652                 rmap = None
653                 if not world_db["Things"][command_tid.id][maptype]:
654                     rmap = bytearray(b' ' * (length ** 2))
655                 else:
656                     rmap = world_db["Things"][command_tid.id][maptype]
657                 rmap[val * length:(val * length) + length] = mapline.encode()
658                 world_db["Things"][command_tid.id][maptype] = rmap
659     return helper
660
661
662 def setter_tpos(axis):
663     """Generate setter for T_POSX or  T_POSY of selected Thing."""
664     @test_Thing_id
665     def helper(str_int):
666         val = integer_test(str_int, 0, 255)
667         if None != val:
668             if val < world_db["MAP_LENGTH"]:
669                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
670                 # TODO: Delete Thing's FOV, and rebuild it if world is active.
671             else:
672                 print("Ignoring: Position is outside of map.")
673     return helper
674
675
676 def command_ttid(id_string):
677     """Set ID of ThingType to manipulate. ID unused? Create new one.
678
679     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
680     """
681     id = id_setter(id_string, "ThingTypes", command_ttid)
682     if None != id:
683         world_db["ThingTypes"][id] = {
684             "TT_NAME": "(none)",
685             "TT_CONSUMABLE": 0,
686             "TT_LIFEPOINTS": 0,
687             "TT_PROLIFERATE": 0,
688             "TT_START_NUMBER": 0,
689             "TT_SYMBOL": "?",
690             "TT_CORPSE_ID": id
691         }
692
693
694 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
695
696
697 @test_ThingType_id
698 def command_ttname(name):
699     """Set TT_NAME of selected ThingType."""
700     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
701
702
703 @test_ThingType_id
704 def command_ttsymbol(char):
705     """Set TT_SYMBOL of selected ThingType. """
706     if 1 == len(char):
707         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
708     else:
709         print("Ignoring: Argument must be single character.")
710
711
712 @test_ThingType_id
713 def command_ttcorpseid(str_int):
714     """Set TT_CORPSE_ID of selected ThingType."""
715     val = integer_test(str_int, 0, 255)
716     if None != val:
717         if val in world_db["ThingTypes"]:
718             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
719         else:
720             print("Ignoring: Corpse ID belongs to no known ThignType.")
721
722
723 def command_taid(id_string):
724     """Set ID of ThingAction to manipulate. ID unused? Create new one.
725
726     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
727     """
728     id = id_setter(id_string, "ThingActions", command_taid, True)
729     if None != id:
730         world_db["ThingActions"][id] = {
731             "TA_EFFORT": 1,
732             "TA_NAME": "wait"
733         }
734
735
736 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
737
738
739 @test_ThingAction_id
740 def command_taname(name):
741     """Set TA_NAME of selected ThingAction.
742
743     The name must match a valid thing action function. If after the name
744     setting no ThingAction with name "wait" remains, call set_world_inactive().
745     """
746     if name == "wait" or name == "move" or name == "use" or name == "drop" \
747        or name == "pick_up":
748         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
749         if 1 == world_db["WORLD_ACTIVE"]:
750             wait_defined = False
751             for id in world_db["ThingActions"]:
752                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
753                     wait_defined = True
754                     break
755             if not wait_defined:
756                 set_world_inactive()
757     else:
758         print("Ignoring: Invalid action name.")
759     # In contrast to the original,naming won't map a function to a ThingAction.
760
761
762 """Commands database.
763
764 Map command start tokens to ([0]) number of expected command arguments, ([1])
765 the command's meta-ness (i.e. is it to be written to the record file, is it to
766 be ignored in replay mode if read from server input file), and ([2]) a function
767 to be called on it.
768 """
769 commands_db = {
770     "QUIT": (0, True, command_quit),
771     "PING": (0, True, command_ping),
772     "THINGS_HERE": (2, True, command_thingshere),
773     "MAKE_WORLD": (1, False, command_makeworld),
774     "SEED_MAP": (1, False, command_seedmap),
775     "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
776                                          0, 4294967295)),
777     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
778     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
779     "MAP_LENGTH": (1, False, command_maplength),
780     "WORLD_ACTIVE": (1, False, command_worldactive),
781     "TA_ID": (1, False, command_taid),
782     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
783     "TA_NAME": (1, False, command_taname),
784     "TT_ID": (1, False, command_ttid),
785     "TT_NAME": (1, False, command_ttname),
786     "TT_SYMBOL": (1, False, command_ttsymbol),
787     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
788     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
789                                        0, 65535)),
790     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
791                                          0, 255)),
792     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
793                                         0, 255)),
794     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
795     "T_ID": (1, False, command_tid),
796     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
797     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
798     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
799     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
800     "T_COMMAND": (1, False, command_tcommand),
801     "T_TYPE": (1, False, command_ttype),
802     "T_CARRIES": (1, False, command_tcarries),
803     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
804     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
805     "T_MEMTHING": (3, False, command_tmemthing),
806     "T_POSY": (1, False, setter_tpos("Y")),
807     "T_POSX": (1, False, setter_tpos("X")),
808 }
809
810
811 """World state database. With sane default values."""
812 world_db = {
813     "TURN": 0,
814     "SEED_MAP": 0,
815     "SEED_RANDOMNESS": 0,
816     "PLAYER_TYPE": 0,
817     "MAP_LENGTH": 64,
818     "WORLD_ACTIVE": 0,
819     "ThingActions": {},
820     "ThingTypes": {},
821     "Things": {}
822 }
823
824
825 """File IO database."""
826 io_db = {
827     "path_save": "save",
828     "path_record": "record",
829     "path_worldconf": "confserver/world",
830     "path_server": "server/",
831     "path_in": "server/in",
832     "path_out": "server/out",
833     "path_worldstate": "server/worldstate",
834     "tmp_suffix": "_tmp",
835     "kicked_by_rival": False,
836     "worldstate_updateable": False
837 }
838
839
840 try:
841     opts = parse_command_line_arguments()
842     setup_server_io()
843     # print("DUMMY: Run game.")
844     if None != opts.replay:
845         replay_game()
846     else:
847         play_game()
848 except SystemExit as exit:
849     print("ABORTING: " + exit.args[0])
850 except:
851     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
852     raise
853 finally:
854     cleanup_server_io()
855     # print("DUMMY: (Clean up C heap.)")