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 # Dummy for saving all commands to reconstruct current world state.
121 # Misses same optimizations as record() from the original record().
123 for id in world_db["ThingActions"]:
124 ta = world_db["ThingActions"][id]
125 ta_string = ta_string + "TA_ID " + str(id) + "\n" + \
126 "TA_EFFORT " + str(ta["TA_EFFORT"]) + "\n" + \
127 "TA_NAME '" + ta["TA_NAME"] + "'\n"
129 for id in world_db["ThingTypes"]:
130 tt = world_db["ThingTypes"][id]
131 tt_string = tt_string + "TT_ID " + str(id) + "\n" + \
133 str(tt["TT_CONSUMABLE"]) + "\n" + \
135 str(tt["TT_LIFEPOINTS"]) + "\n" + \
136 "TT_PROLIFERATE " + \
137 str(tt["TT_PROLIFERATE"]) + "\n" + \
138 "TT_START_NUMBER " + \
139 str(tt["TT_START_NUMBER"]) + "\n" + \
140 "TT_NAME '" + tt["TT_NAME"] + "'\n" + \
141 "TT_SYMBOL '" + tt["TT_SYMBOL"] + "'\n"
142 for id in world_db["ThingTypes"]:
143 tt = world_db["ThingTypes"][id]
144 tt_string = tt_string + "TT_ID " + str(id) + "\n" + \
146 str(tt["TT_CORPSE_ID"]) + "\n"
147 atomic_write(io_db["path_save"],
148 "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"]) + "\n" +
149 "MAP_LENGTH " + str(world_db["MAP_LENGTH"]) + "\n" +
150 "PLAYER_TYPE " + str(world_db["PLAYER_TYPE"]) + "\n" +
151 "TURN " + str(world_db["TURN"]) + "\n" +
152 "SEED_RANDOMNESS " + str(world_db["SEED_RANDOMNESS"]) + "\n" +
153 "SEED_MAP " + str(world_db["SEED_MAP"]) + "\n" +
154 ta_string + tt_string)
155 # TODO: If all this ever does is just writing down what's in world_db, some
156 # loop over its entries should be all that's needed.
159 def obey_lines_in_file(path, name, do_record=False):
160 """Call obey() on each line of path's file, use name in input prefix."""
161 file = open(path, "r")
163 for line in file.readlines():
164 obey(line.rstrip(), name + "file line " + str(line_n),
170 def parse_command_line_arguments():
171 """Return settings values read from command line arguments."""
172 parser = argparse.ArgumentParser()
173 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
175 opts, unknown = parser.parse_known_args()
180 """Ensure valid server out file belonging to current process.
182 This is done by comparing io_db["teststring"] to what's found at the start
183 of the current file at io_db["path_out"]. On failure, set
184 io_db["kicked_by_rival"] and raise SystemExit.
186 if not os.access(io_db["path_out"], os.F_OK):
187 raise SystemExit("Server output file has disappeared.")
188 file = open(io_db["path_out"], "r")
189 test = file.readline().rstrip("\n")
191 if test != io_db["teststring"]:
192 io_db["kicked_by_rival"] = True
193 msg = "Server test string in server output file does not match. This" \
194 " indicates that the current server process has been " \
195 "superseded by another one."
196 raise SystemExit(msg)
200 """Return next newline-delimited command from server in file.
202 Keep building return string until a newline is encountered. Pause between
203 unsuccessful reads, and after too much waiting, run server_test().
210 add = io_db["file_in"].readline()
212 command = command + add
213 if len(command) > 0 and "\n" == command[-1]:
214 command = command[:-1]
217 time.sleep(wait_on_fail)
218 if now + max_wait < time.time():
225 """Replay game from record file.
227 Use opts.replay as breakpoint turn to which to replay automatically before
228 switching to manual input by non-meta commands in server input file
229 triggering further reads of record file. Ensure opts.replay is at least 1.
233 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
234 " (if so late a turn is to be found).")
235 if not os.access(io_db["path_record"], os.F_OK):
236 raise SystemExit("No record file found to replay.")
237 io_db["file_record"] = open(io_db["path_record"], "r")
238 io_db["file_record"].prefix = "record file line "
239 io_db["file_record"].line_n = 1
240 while world_db["TURN"] < opts.replay:
241 line = io_db["file_record"].readline()
244 obey(line.rstrip(), io_db["file_record"].prefix
245 + str(io_db["file_record"].line_n))
246 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
248 obey(read_command(), "in file", replay=True)
252 """Play game by server input file commands. Before, load save file found.
254 If no save file is found, a new world is generated from the commands in the
255 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
256 command and all that follow via the server input file.
258 if os.access(io_db["path_save"], os.F_OK):
259 obey_lines_in_file(io_db["path_save"], "save")
261 if not os.access(io_db["path_worldconf"], os.F_OK):
262 msg = "No world config file from which to start a new world."
263 raise SystemExit(msg)
264 obey_lines_in_file(io_db["path_worldconf"], "world config ",
266 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
268 obey(read_command(), "in file", do_record=True)
273 print("I'd (re-)make the map now, if only I knew how.")
276 def set_world_inactive():
277 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
279 if os.access(io_db["path_worldstate"], os.F_OK):
280 os.remove(io_db["path_worldstate"])
281 world_db["WORLD_ACTIVE"] = 0
284 def integer_test(val_string, min, max):
285 """Return val_string if possible integer >= min and <= max, else None."""
287 val = int(val_string)
288 if val < min or val > max:
292 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
297 def worlddb_value_setter(key, min, max):
298 """Generate: Set world_db[key] to int(val_string) if >= min and <= max."""
299 def func(val_string):
300 val = integer_test(val_string, min, max)
307 """Send PONG line to server output file."""
308 io_db["file_out"].write("PONG\n")
309 io_db["file_out"].flush()
313 """Abort server process."""
314 raise SystemExit("received QUIT command")
317 def command_seedmap(seed_string):
318 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
319 worlddb_value_setter("SEED_MAP", 0, 4294967295)(seed_string)
323 def command_makeworld(seed_string):
325 worlddb_value_setter("SEED_MAP", 0, 4294967295)(seed_string)
326 worlddb_value_setter("SEED_RANDOMNESS", 0, 4294967295)(seed_string)
327 # TODO: Test for existence of player thing and 'wait' thing action?
330 def command_maplength(maplength_string):
333 # TODO: remove things, map
334 worlddb_value_setter("MAP_LENGTH", 1, 256)(maplength_string)
337 def command_worldactive(worldactive_string):
339 val = integer_test(worldactive_string, 0, 1)
341 if 0 != world_db["WORLD_ACTIVE"] and 0 == val:
343 elif 0 == world_db["WORLD_ACTIVE"]:
345 player_exists = False
347 # TODO: perform tests:
348 # Is there thing action of name 'wait'?
349 # Is there a player thing?
351 if wait_exists and player_exists and map_exists:
352 # TODO: rebuild al things' FOVs, map memories
353 world_db["WORLD_ACTIVE"] = 1
356 def command_ttid(id_string):
357 """Set ID of ThingType to manipulate. ID unused? Create new ThingType.
359 The ID of the ThingType to manipulate is stored as command_ttid.id. If
360 the integer of the input value is valid (>= -32768 and <= 32767), but <0 or
361 >255, a new ID is calculated: the lowest unused ID >=0 and <= 255.
363 id = integer_test(id_string, -32768, 32767)
365 if id in world_db["ThingTypes"]:
368 if id < 0 or id > 255:
372 if id not in world_db["ThingTypes"]:
376 "No unused ID available to add to ID list.")
379 world_db["ThingTypes"][id] = {
384 "TT_START_NUMBER": 0,
390 def test_for_id_maker(object, category):
391 """Return decorator testing for object having "id" attribute."""
394 if hasattr(object, "id"):
397 print("Ignoring: No " + category +
398 " defined to manipulate yet.")
403 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
406 def ThingType_value_setter(key, min, max):
407 """Build: Set selected ThingType's [key] to int(val_string) >=min/<=max."""
410 val = integer_test(val_string, min, max)
412 world_db["ThingTypes"][command_ttid.id][key] = val
417 def command_ttname(name):
418 """Set to name TT_NAME of selected ThingType."""
419 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
423 def command_ttsymbol(char):
424 """Set to char TT_SYMBOL of selected ThingType. """
426 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
428 print("Ignoring: Argument must be single character.")
432 def command_ttcorpseid(str_int):
433 """Set to int(str_int) TT_CORPSE_ID of selected ThingType."""
434 val = integer_test(str_int, 0, 255)
436 if val in world_db["ThingTypes"]:
437 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
439 print("Corpse ID belongs to no known object type.")
442 def command_taid(id_string):
443 """Set ID of ThingAction to manipulate. ID unused? Create new ThingAction.
445 The ID of the ThingAction to manipulate is stored as command_taid.id. If
446 the integer of the input value is valid (>= 0 and <= 255), but 0, a new ID
447 is calculated: The lowest unused ID >0 and <= 255.
449 id = integer_test(id_string, 0, 255)
451 if id in world_db["ThingActions"]:
457 if id not in world_db["ThingActions"]:
461 "No unused ID available to add to ID list.")
464 world_db["ThingActions"][id] = {
470 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
474 def command_taeffort(str_int):
475 """Set to int(str_int) TA_EFFORT of selected ThingAction."""
476 val = integer_test(str_int, 0, 255)
478 world_db["ThingActions"][command_taid.id]["TA_EFFORT"] = val
482 def command_taname(name):
483 """Set to name TA_NAME of selected ThingAction.
485 The name must match a valid thing action function. If after the name
486 setting no ThingAction with name "wait" remains, call set_world_inactive().
488 if name == "wait" or name == "move" or name == "use" or name == "drop" \
489 or name == "pick_up":
490 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
491 if 1 == world_db["WORLD_ACTIVE"]:
493 for id in world_db["ThingActions"]:
494 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
500 print("Ignoring: Invalid action name.")
501 # In contrast to the original,naming won't map a function to a ThingAction.
504 """Commands database.
506 Map command start tokens to ([0]) number of expected command arguments, ([1])
507 the command's meta-ness (i.e. is it to be written to the record file, is it to
508 be ignored in replay mode if read from server input file), and ([2]) a function
512 "QUIT": (0, True, command_quit),
513 "PING": (0, True, command_ping),
514 "MAKE_WORLD": (1, False, command_makeworld),
515 "SEED_MAP": (1, False, command_seedmap),
516 "SEED_RANDOMNESS": (1, False, worlddb_value_setter("SEED_RANDOMNESS",
518 "TURN": (1, False, worlddb_value_setter("TURN", 0, 65535)),
519 "PLAYER_TYPE": (1, False, worlddb_value_setter("PLAYER_TYPE", 0, 255)),
520 "MAP_LENGTH": (1, False, command_maplength),
521 "WORLD_ACTIVE": (1, False, command_worldactive),
522 "TA_ID": (1, False, command_taid),
523 "TA_EFFORT": (1, False, command_taeffort),
524 "TA_NAME": (1, False, command_taname),
525 "TT_ID": (1, False, command_ttid),
526 "TT_NAME": (1, False, command_ttname),
527 "TT_SYMBOL": (1, False, command_ttsymbol),
528 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
529 "TT_CONSUMABLE": (1, False, ThingType_value_setter("TT_CONSUMABLE",
531 "TT_START_NUMBER": (1, False, ThingType_value_setter("TT_START_NUMBER",
533 "TT_PROLIFERATE": (1, False, ThingType_value_setter("TT_PROLIFERATE",
535 "TT_LIFEPOINTS": (1, False, ThingType_value_setter("TT_LIFEPOINTS",
540 """World state database. With sane default values."""
544 "SEED_RANDOMNESS": 0,
554 """File IO database."""
557 "path_record": "record",
558 "path_worldconf": "confserver/world",
559 "path_server": "server/",
560 "path_in": "server/in",
561 "path_out": "server/out",
562 "path_worldstate": "server/worldstate",
563 "tmp_suffix": "_tmp",
564 "kicked_by_rival": False
569 opts = parse_command_line_arguments()
571 # print("DUMMY: Run game.")
572 if None != opts.replay:
576 except SystemExit as exit:
577 print("ABORTING: " + exit.args[0])
579 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
583 # print("DUMMY: (Clean up C heap.)")