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
387 # TODO: Generate things (player first, with updated memory), set TURN 1,
388 atomic_write(io_db["path_out"], "NEW_WORLD\n", do_append=True)
391 def command_maplength(maplength_string):
394 # TODO: remove map (is this necessary? no memory management trouble …)
395 world_db["Things"] = {}
396 setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
399 def command_worldactive(worldactive_string):
401 val = integer_test(worldactive_string, 0, 1)
403 if 0 != world_db["WORLD_ACTIVE"]:
407 print("World already active.")
408 elif 0 == world_db["WORLD_ACTIVE"]:
410 for ThingAction in world_db["ThingActions"]:
411 if "wait" == ThingAction["TA_NAME"]:
414 player_exists = False
415 for Thing in world_db["Things"]:
416 if 0 == ThingAction["T_ID"]:
419 map_exists = "MAP" in world_db
420 if wait_exists and player_exists and map_exists:
421 # TODO: rebuild all things' FOVs, map memories
422 world_db["WORLD_ACTIVE"] = 1
425 def id_setter(id_string, category, id_store, start_at_1=False):
426 """Set ID of object of category to manipulate ID unused? Create new one.
428 The ID is stored as id_store.id. If the integer of the input is valid (if
429 start_at_1, >= 0 and <= 255, else >= -32768 and <= 32767), but <0 or (if
430 start_at_1) <1, calculate new ID: lowest unused ID >=0 or (if start_at_1)
431 >= 1, and <= 255. None is always returned when no new object is created,
432 otherwise the new object's ID.
434 min = 0 if start_at_1 else -32768
435 max = 255 if start_at_1 else 32767
436 id = integer_test(id_string, min, max)
438 if id in world_db[category]:
442 if (start_at_1 and 0 == id) \
443 or ((not start_at_1) and (id < 0 or id > 255)):
447 if id not in world_db[category]:
451 "No unused ID available to add to ID list.")
457 def test_for_id_maker(object, category):
458 """Return decorator testing for object having "id" attribute."""
461 if hasattr(object, "id"):
464 print("Ignoring: No " + category +
465 " defined to manipulate yet.")
470 def command_tid(id_string):
471 """Set ID of Thing to manipulate. ID unused? Create new one.
473 Default new Thing's type to the first available ThingType, others: zero.
475 id = id_setter(id_string, "Things", command_tid)
477 if world_db["ThingTypes"] == {}:
478 print("Ignoring: No ThingType to settle new Thing in.")
480 world_db["Things"][id] = {
486 "T_TYPE": list(world_db["ThingTypes"].keys())[0],
493 "T_MEMDEPTHMAP": False
497 test_Thing_id = test_for_id_maker(command_tid, "Thing")
501 def command_tcommand(str_int):
502 """Set T_COMMAND of selected Thing."""
503 val = integer_test(str_int, 0, 255)
505 if 0 == val or val in world_db["ThingActions"]:
506 world_db["Things"][command_tid.id]["T_COMMAND"] = val
508 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
512 def command_ttype(str_int):
513 """Set T_TYPE of selected Thing."""
514 val = integer_test(str_int, 0, 255)
516 if val in world_db["ThingTypes"]:
517 world_db["Things"][command_tid.id]["T_TYPE"] = val
519 print("Ignoring: ThingType ID belongs to no known ThingType.")
523 def command_tcarries(str_int):
524 """Append int(str_int) to T_CARRIES of selected Thing.
526 The ID int(str_int) must not be of the selected Thing, and must belong to a
527 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
529 val = integer_test(str_int, 0, 255)
531 if val == command_tid.id:
532 print("Ignoring: Thing cannot carry itself.")
533 elif val in world_db["Things"] \
534 and not world_db["Things"][val]["carried"]:
535 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
536 world_db["Things"][val]["carried"] = True
538 print("Ignoring: Thing not available for carrying.")
539 # Note that the whole carrying structure is different from the C version:
540 # Carried-ness is marked by a "carried" flag, not by Things containing
545 def command_tmemthing(str_t, str_y, str_x):
546 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
548 The type must fit to an existing ThingType, and the position into the map.
550 type = integer_test(str_t, 0, 255)
551 posy = integer_test(str_y, 0, 255)
552 posx = integer_test(str_x, 0, 255)
553 if None != type and None != posy and None != posx:
554 if type not in world_db["ThingTypes"] \
555 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
556 print("Ignoring: Illegal value for thing type or position.")
558 memthing = (type, posy, posx)
559 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
562 def setter_map(maptype):
563 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
565 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
568 def helper(str_int, mapline):
569 val = integer_test(str_int, 0, 255)
571 if val >= world_db["MAP_LENGTH"]:
572 print("Illegal value for map line number.")
573 elif len(mapline) != world_db["MAP_LENGTH"]:
574 print("Map line length is unequal map width.")
576 length = world_db["MAP_LENGTH"]
578 if not world_db["Things"][command_tid.id][maptype]:
579 rmap = bytearray(b' ' * (length ** 2))
581 rmap = world_db["Things"][command_tid.id][maptype]
582 rmap[val * length:(val * length) + length] = mapline.encode()
583 world_db["Things"][command_tid.id][maptype] = rmap
587 def setter_tpos(axis):
588 """Generate setter for T_POSX or T_POSY of selected Thing."""
591 val = integer_test(str_int, 0, 255)
593 if val < world_db["MAP_LENGTH"]:
594 world_db["Things"][command_tid.id]["T_POS" + axis] = val
595 # TODO: Delete Thing's FOV, and rebuild it if world is active.
597 print("Ignoring: Position is outside of map.")
601 def command_ttid(id_string):
602 """Set ID of ThingType to manipulate. ID unused? Create new one.
604 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
606 id = id_setter(id_string, "ThingTypes", command_ttid)
608 world_db["ThingTypes"][id] = {
613 "TT_START_NUMBER": 0,
619 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
623 def command_ttname(name):
624 """Set TT_NAME of selected ThingType."""
625 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
629 def command_ttsymbol(char):
630 """Set TT_SYMBOL of selected ThingType. """
632 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
634 print("Ignoring: Argument must be single character.")
638 def command_ttcorpseid(str_int):
639 """Set TT_CORPSE_ID of selected ThingType."""
640 val = integer_test(str_int, 0, 255)
642 if val in world_db["ThingTypes"]:
643 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
645 print("Ignoring: Corpse ID belongs to no known ThignType.")
648 def command_taid(id_string):
649 """Set ID of ThingAction to manipulate. ID unused? Create new one.
651 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
653 id = id_setter(id_string, "ThingActions", command_taid, True)
655 world_db["ThingActions"][id] = {
661 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
665 def command_taname(name):
666 """Set TA_NAME of selected ThingAction.
668 The name must match a valid thing action function. If after the name
669 setting no ThingAction with name "wait" remains, call set_world_inactive().
671 if name == "wait" or name == "move" or name == "use" or name == "drop" \
672 or name == "pick_up":
673 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
674 if 1 == world_db["WORLD_ACTIVE"]:
676 for id in world_db["ThingActions"]:
677 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
683 print("Ignoring: Invalid action name.")
684 # In contrast to the original,naming won't map a function to a ThingAction.
687 """Commands database.
689 Map command start tokens to ([0]) number of expected command arguments, ([1])
690 the command's meta-ness (i.e. is it to be written to the record file, is it to
691 be ignored in replay mode if read from server input file), and ([2]) a function
695 "QUIT": (0, True, command_quit),
696 "PING": (0, True, command_ping),
697 "MAKE_WORLD": (1, False, command_makeworld),
698 "SEED_MAP": (1, False, command_seedmap),
699 "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
701 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
702 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
703 "MAP_LENGTH": (1, False, command_maplength),
704 "WORLD_ACTIVE": (1, False, command_worldactive),
705 "TA_ID": (1, False, command_taid),
706 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
707 "TA_NAME": (1, False, command_taname),
708 "TT_ID": (1, False, command_ttid),
709 "TT_NAME": (1, False, command_ttname),
710 "TT_SYMBOL": (1, False, command_ttsymbol),
711 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
712 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
714 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
716 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
718 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
719 "T_ID": (1, False, command_tid),
720 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
721 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
722 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
723 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
724 "T_COMMAND": (1, False, command_tcommand),
725 "T_TYPE": (1, False, command_ttype),
726 "T_CARRIES": (1, False, command_tcarries),
727 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
728 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
729 "T_MEMTHING": (3, False, command_tmemthing),
730 "T_POSY": (1, False, setter_tpos("Y")),
731 "T_POSX": (1, False, setter_tpos("X")),
735 """World state database. With sane default values."""
739 "SEED_RANDOMNESS": 0,
749 """File IO database."""
752 "path_record": "record",
753 "path_worldconf": "confserver/world",
754 "path_server": "server/",
755 "path_in": "server/in",
756 "path_out": "server/out",
757 "path_worldstate": "server/worldstate",
758 "tmp_suffix": "_tmp",
759 "kicked_by_rival": False
764 opts = parse_command_line_arguments()
766 # print("DUMMY: Run game.")
767 if None != opts.replay:
771 except SystemExit as exit:
772 print("ABORTING: " + exit.args[0])
774 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
778 # print("DUMMY: (Clean up C heap.)")