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