10 """Fill IO files DB with proper file( path)s. Write process IO test string.
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"].
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):
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"])
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):
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()
54 def obey(command, prefix, replay=False, do_record=False):
55 """Call function from commands_db mapped to command's first token.
57 The command string is tokenized by shlex.split(comments=True). If replay is
58 set, a non-meta command from the commands_db merely triggers obey() on the
59 next command from the records file. If not, and do do_record is set,
60 non-meta commands are recorded via record(), and save_world() is called.
61 The prefix string is inserted into the server's input message between its
62 beginning 'input ' & ':'. All activity is preceded by a server_test() call.
65 print("input " + prefix + ": " + command)
67 tokens = shlex.split(command, comments=True)
68 except ValueError as err:
69 print("Can't tokenize command string: " + str(err) + ".")
71 if len(tokens) > 0 and tokens[0] in commands_db \
72 and len(tokens) == commands_db[tokens[0]][0] + 1:
73 if commands_db[tokens[0]][1]:
74 commands_db[tokens[0]][2]()
76 print("Due to replay mode, reading command as 'go on in record'.")
77 line = io_db["file_record"].readline()
79 obey(line.rstrip(), io_db["file_record"].prefix
80 + str(io_db["file_record"].line_n))
81 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
83 print("Reached end of record file.")
85 commands_db[tokens[0]][2](*tokens[1:])
89 elif 0 != len(tokens):
90 print("Invalid command/argument, or bad number of tokens.")
93 def atomic_write(path, text, do_append=False):
94 """Atomic write of text to file at path, appended if do_append is set."""
95 path_tmp = path + io_db["tmp_suffix"]
99 if os.access(path, os.F_OK):
100 shutil.copyfile(path, path_tmp)
101 file = open(path_tmp, mode)
104 os.fsync(file.fileno())
106 if os.access(path, os.F_OK):
108 os.rename(path_tmp, path)
112 """Append command string plus newline to record file. (Atomic.)"""
113 # This misses some optimizations from the original record(), namely only
114 # finishing the atomic write with expensive flush() and fsync() every 15
115 # seconds unless explicitely forced. Implement as needed.
116 atomic_write(io_db["path_record"], command + "\n", do_append=True)
120 """Save all commands needed to reconstruct current world state."""
121 # TODO: Misses same optimizations as record() from the original record().
124 string = string.replace("\u005C", '\u005C\u005C')
125 return '"' + string.replace('"', '\u005C"') + '"'
130 if world_db["Things"][id][key]:
131 rmap = world_db["Things"][id][key]
132 length = world_db["MAP_LENGTH"]
133 for i in range(world_db["MAP_LENGTH"]):
134 line = rmap[i * length:(i * length) + length].decode()
135 string = string + key + " " + str(i) + quote(line) + "\n"
141 for memthing in world_db["Things"][id]["T_MEMTHING"]:
142 string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
143 str(memthing[1]) + " " + str(memthing[2]) + "\n"
146 def helper(category, id_string, special_keys={}):
148 for id in world_db[category]:
149 string = string + id_string + " " + str(id) + "\n"
150 for key in world_db[category][id]:
151 if not key in special_keys:
152 x = world_db[category][id][key]
153 argument = quote(x) if str == type(x) else str(x)
154 string = string + key + " " + argument + "\n"
155 elif special_keys[key]:
156 string = string + special_keys[key](id)
161 if dict != type(world_db[key]):
162 string = string + key + " " + str(world_db[key]) + "\n"
163 string = string + helper("ThingActions", "TA_ID")
164 string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
165 for id in world_db["ThingTypes"]:
166 string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
167 str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
168 string = string + helper("Things", "T_ID",
169 {"T_CARRIES": False, "carried": False,
170 "T_MEMMAP": mapsetter("T_MEMMAP"),
171 "T_MEMTHING": memthing,
172 "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
173 for id in world_db["Things"]:
174 if [] != world_db["Things"][id]["T_CARRIES"]:
175 string = string + "T_ID " + str(id) + "\n"
176 for carried_id in world_db["Things"][id]["T_CARRIES"]:
177 string = string + "T_CARRIES " + str(carried_id) + "\n"
178 atomic_write(io_db["path_save"], string)
181 def obey_lines_in_file(path, name, do_record=False):
182 """Call obey() on each line of path's file, use name in input prefix."""
183 file = open(path, "r")
185 for line in file.readlines():
186 obey(line.rstrip(), name + "file line " + str(line_n),
192 def parse_command_line_arguments():
193 """Return settings values read from command line arguments."""
194 parser = argparse.ArgumentParser()
195 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
197 opts, unknown = parser.parse_known_args()
202 """Ensure valid server out file belonging to current process.
204 This is done by comparing io_db["teststring"] to what's found at the start
205 of the current file at io_db["path_out"]. On failure, set
206 io_db["kicked_by_rival"] and raise SystemExit.
208 if not os.access(io_db["path_out"], os.F_OK):
209 raise SystemExit("Server output file has disappeared.")
210 file = open(io_db["path_out"], "r")
211 test = file.readline().rstrip("\n")
213 if test != io_db["teststring"]:
214 io_db["kicked_by_rival"] = True
215 msg = "Server test string in server output file does not match. This" \
216 " indicates that the current server process has been " \
217 "superseded by another one."
218 raise SystemExit(msg)
222 """Return next newline-delimited command from server in file.
224 Keep building return string until a newline is encountered. Pause between
225 unsuccessful reads, and after too much waiting, run server_test().
232 add = io_db["file_in"].readline()
234 command = command + add
235 if len(command) > 0 and "\n" == command[-1]:
236 command = command[:-1]
239 time.sleep(wait_on_fail)
240 if now + max_wait < time.time():
247 """Replay game from record file.
249 Use opts.replay as breakpoint turn to which to replay automatically before
250 switching to manual input by non-meta commands in server input file
251 triggering further reads of record file. Ensure opts.replay is at least 1.
255 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
256 " (if so late a turn is to be found).")
257 if not os.access(io_db["path_record"], os.F_OK):
258 raise SystemExit("No record file found to replay.")
259 io_db["file_record"] = open(io_db["path_record"], "r")
260 io_db["file_record"].prefix = "record file line "
261 io_db["file_record"].line_n = 1
262 while world_db["TURN"] < opts.replay:
263 line = io_db["file_record"].readline()
266 obey(line.rstrip(), io_db["file_record"].prefix
267 + str(io_db["file_record"].line_n))
268 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
270 obey(read_command(), "in file", replay=True)
274 """Play game by server input file commands. Before, load save file found.
276 If no save file is found, a new world is generated from the commands in the
277 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
278 command and all that follow via the server input file.
280 if os.access(io_db["path_save"], os.F_OK):
281 obey_lines_in_file(io_db["path_save"], "save")
283 if not os.access(io_db["path_worldconf"], os.F_OK):
284 msg = "No world config file from which to start a new world."
285 raise SystemExit(msg)
286 obey_lines_in_file(io_db["path_worldconf"], "world config ",
288 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
290 obey(read_command(), "in file", do_record=True)
295 world_db["MAP"] = bytearray(b'.' * (world_db["MAP_LENGTH"] ** 2))
298 def set_world_inactive():
299 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
301 if os.access(io_db["path_worldstate"], os.F_OK):
302 os.remove(io_db["path_worldstate"])
303 world_db["WORLD_ACTIVE"] = 0
306 def integer_test(val_string, min, max):
307 """Return val_string if possible integer >= min and <= max, else None."""
309 val = int(val_string)
310 if val < min or val > max:
314 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
319 def setter(category, key, min, max):
320 """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
323 val = integer_test(val_string, min, max)
327 if category == "Thing":
328 id_store = command_tid
329 decorator = test_Thing_id
330 elif category == "ThingType":
331 id_store = command_ttid
332 decorator = test_ThingType_id
333 elif category == "ThingAction":
334 id_store = command_taid
335 decorator = test_ThingAction_id
339 val = integer_test(val_string, min, max)
341 world_db[category + "s"][id_store.id][key] = val
346 """Send PONG line to server output file."""
347 io_db["file_out"].write("PONG\n")
348 io_db["file_out"].flush()
352 """Abort server process."""
353 raise SystemExit("received QUIT command")
356 def command_seedmap(seed_string):
357 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
358 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
362 def command_makeworld(seed_string):
364 setter(None, "SEED_RANDOMNESS", 0, 4294967295)(seed_string)
365 player_will_be_generated = False
366 for ThingType in world_db["ThingTypes"]:
368 if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
369 player_will_be_generated = True
371 if not player_will_be_generated:
372 print("Ignoring beyond SEED_MAP: " +
373 "No player type with start number >0 defined.")
376 for ThingAction in world_db["ThingActions"]:
377 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
380 print("Ignoring beyond SEED_MAP: " +
381 "No thing action with name 'wait' defined.")
383 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
384 world_db["Things"] = {}
386 world_db["WORLD_ACTIVE"] = 1
388 # TODO: Generate things (player first, with updated memory)
389 atomic_write(io_db["path_out"], "NEW_WORLD\n", do_append=True)
392 def command_maplength(maplength_string):
395 # TODO: remove map (is this necessary? no memory management trouble …)
396 world_db["Things"] = {}
397 setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
400 def command_worldactive(worldactive_string):
402 val = integer_test(worldactive_string, 0, 1)
404 if 0 != world_db["WORLD_ACTIVE"]:
408 print("World already active.")
409 elif 0 == world_db["WORLD_ACTIVE"]:
411 for ThingAction in world_db["ThingActions"]:
412 if "wait" == ThingAction["TA_NAME"]:
415 player_exists = False
416 for Thing in world_db["Things"]:
417 if 0 == ThingAction["T_ID"]:
420 map_exists = "MAP" in world_db
421 if wait_exists and player_exists and map_exists:
422 # TODO: rebuild all things' FOVs, map memories
423 world_db["WORLD_ACTIVE"] = 1
426 def id_setter(id_string, category, id_store, start_at_1=False):
427 """Set ID of object of category to manipulate ID unused? Create new one.
429 The ID is stored as id_store.id. If the integer of the input is valid (if
430 start_at_1, >= 0 and <= 255, else >= -32768 and <= 32767), but <0 or (if
431 start_at_1) <1, calculate new ID: lowest unused ID >=0 or (if start_at_1)
432 >= 1, and <= 255. None is always returned when no new object is created,
433 otherwise the new object's ID.
435 min = 0 if start_at_1 else -32768
436 max = 255 if start_at_1 else 32767
437 id = integer_test(id_string, min, max)
439 if id in world_db[category]:
443 if (start_at_1 and 0 == id) \
444 or ((not start_at_1) and (id < 0 or id > 255)):
448 if id not in world_db[category]:
452 "No unused ID available to add to ID list.")
458 def test_for_id_maker(object, category):
459 """Return decorator testing for object having "id" attribute."""
462 if hasattr(object, "id"):
465 print("Ignoring: No " + category +
466 " defined to manipulate yet.")
471 def command_tid(id_string):
472 """Set ID of Thing to manipulate. ID unused? Create new one.
474 Default new Thing's type to the first available ThingType, others: zero.
476 id = id_setter(id_string, "Things", command_tid)
478 if world_db["ThingTypes"] == {}:
479 print("Ignoring: No ThingType to settle new Thing in.")
481 world_db["Things"][id] = {
487 "T_TYPE": list(world_db["ThingTypes"].keys())[0],
494 "T_MEMDEPTHMAP": False
498 test_Thing_id = test_for_id_maker(command_tid, "Thing")
502 def command_tcommand(str_int):
503 """Set T_COMMAND of selected Thing."""
504 val = integer_test(str_int, 0, 255)
506 if 0 == val or val in world_db["ThingActions"]:
507 world_db["Things"][command_tid.id]["T_COMMAND"] = val
509 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
513 def command_ttype(str_int):
514 """Set T_TYPE of selected Thing."""
515 val = integer_test(str_int, 0, 255)
517 if val in world_db["ThingTypes"]:
518 world_db["Things"][command_tid.id]["T_TYPE"] = val
520 print("Ignoring: ThingType ID belongs to no known ThingType.")
524 def command_tcarries(str_int):
525 """Append int(str_int) to T_CARRIES of selected Thing.
527 The ID int(str_int) must not be of the selected Thing, and must belong to a
528 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
530 val = integer_test(str_int, 0, 255)
532 if val == command_tid.id:
533 print("Ignoring: Thing cannot carry itself.")
534 elif val in world_db["Things"] \
535 and not world_db["Things"][val]["carried"]:
536 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
537 world_db["Things"][val]["carried"] = True
539 print("Ignoring: Thing not available for carrying.")
540 # Note that the whole carrying structure is different from the C version:
541 # Carried-ness is marked by a "carried" flag, not by Things containing
546 def command_tmemthing(str_t, str_y, str_x):
547 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
549 The type must fit to an existing ThingType, and the position into the map.
551 type = integer_test(str_t, 0, 255)
552 posy = integer_test(str_y, 0, 255)
553 posx = integer_test(str_x, 0, 255)
554 if None != type and None != posy and None != posx:
555 if type not in world_db["ThingTypes"] \
556 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
557 print("Ignoring: Illegal value for thing type or position.")
559 memthing = (type, posy, posx)
560 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
563 def setter_map(maptype):
564 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
566 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
569 def helper(str_int, mapline):
570 val = integer_test(str_int, 0, 255)
572 if val >= world_db["MAP_LENGTH"]:
573 print("Illegal value for map line number.")
574 elif len(mapline) != world_db["MAP_LENGTH"]:
575 print("Map line length is unequal map width.")
577 length = world_db["MAP_LENGTH"]
579 if not world_db["Things"][command_tid.id][maptype]:
580 rmap = bytearray(b' ' * (length ** 2))
582 rmap = world_db["Things"][command_tid.id][maptype]
583 rmap[val * length:(val * length) + length] = mapline.encode()
584 world_db["Things"][command_tid.id][maptype] = rmap
588 def setter_tpos(axis):
589 """Generate setter for T_POSX or T_POSY of selected Thing."""
592 val = integer_test(str_int, 0, 255)
594 if val < world_db["MAP_LENGTH"]:
595 world_db["Things"][command_tid.id]["T_POS" + axis] = val
596 # TODO: Delete Thing's FOV, and rebuild it if world is active.
598 print("Ignoring: Position is outside of map.")
602 def command_ttid(id_string):
603 """Set ID of ThingType to manipulate. ID unused? Create new one.
605 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
607 id = id_setter(id_string, "ThingTypes", command_ttid)
609 world_db["ThingTypes"][id] = {
614 "TT_START_NUMBER": 0,
620 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
624 def command_ttname(name):
625 """Set TT_NAME of selected ThingType."""
626 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
630 def command_ttsymbol(char):
631 """Set TT_SYMBOL of selected ThingType. """
633 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
635 print("Ignoring: Argument must be single character.")
639 def command_ttcorpseid(str_int):
640 """Set TT_CORPSE_ID of selected ThingType."""
641 val = integer_test(str_int, 0, 255)
643 if val in world_db["ThingTypes"]:
644 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
646 print("Ignoring: Corpse ID belongs to no known ThignType.")
649 def command_taid(id_string):
650 """Set ID of ThingAction to manipulate. ID unused? Create new one.
652 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
654 id = id_setter(id_string, "ThingActions", command_taid, True)
656 world_db["ThingActions"][id] = {
662 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
666 def command_taname(name):
667 """Set TA_NAME of selected ThingAction.
669 The name must match a valid thing action function. If after the name
670 setting no ThingAction with name "wait" remains, call set_world_inactive().
672 if name == "wait" or name == "move" or name == "use" or name == "drop" \
673 or name == "pick_up":
674 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
675 if 1 == world_db["WORLD_ACTIVE"]:
677 for id in world_db["ThingActions"]:
678 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
684 print("Ignoring: Invalid action name.")
685 # In contrast to the original,naming won't map a function to a ThingAction.
688 """Commands database.
690 Map command start tokens to ([0]) number of expected command arguments, ([1])
691 the command's meta-ness (i.e. is it to be written to the record file, is it to
692 be ignored in replay mode if read from server input file), and ([2]) a function
696 "QUIT": (0, True, command_quit),
697 "PING": (0, True, command_ping),
698 "MAKE_WORLD": (1, False, command_makeworld),
699 "SEED_MAP": (1, False, command_seedmap),
700 "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
702 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
703 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
704 "MAP_LENGTH": (1, False, command_maplength),
705 "WORLD_ACTIVE": (1, False, command_worldactive),
706 "TA_ID": (1, False, command_taid),
707 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
708 "TA_NAME": (1, False, command_taname),
709 "TT_ID": (1, False, command_ttid),
710 "TT_NAME": (1, False, command_ttname),
711 "TT_SYMBOL": (1, False, command_ttsymbol),
712 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
713 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
715 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
717 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
719 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
720 "T_ID": (1, False, command_tid),
721 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
722 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
723 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
724 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
725 "T_COMMAND": (1, False, command_tcommand),
726 "T_TYPE": (1, False, command_ttype),
727 "T_CARRIES": (1, False, command_tcarries),
728 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
729 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
730 "T_MEMTHING": (3, False, command_tmemthing),
731 "T_POSY": (1, False, setter_tpos("Y")),
732 "T_POSX": (1, False, setter_tpos("X")),
736 """World state database. With sane default values."""
740 "SEED_RANDOMNESS": 0,
750 """File IO database."""
753 "path_record": "record",
754 "path_worldconf": "confserver/world",
755 "path_server": "server/",
756 "path_in": "server/in",
757 "path_out": "server/out",
758 "path_worldstate": "server/worldstate",
759 "tmp_suffix": "_tmp",
760 "kicked_by_rival": False
765 opts = parse_command_line_arguments()
767 # print("DUMMY: Run game.")
768 if None != opts.replay:
772 except SystemExit as exit:
773 print("ABORTING: " + exit.args[0])
775 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
779 # print("DUMMY: (Clean up C heap.)")