11 """"Interface to libplomrogue's pseudo-randomness generator."""
13 def set_seed(self, seed):
14 libpr.seed_rrand(1, seed)
17 return libpr.seed_rrand(0, 0)
22 seed = property(get_seed, set_seed)
26 """Prepare ctypes library at ./libplomrogue.so"""
27 libpath = ("./libplomrogue.so")
28 if not os.access(libpath, os.F_OK):
29 raise SystemExit("No library " + libpath + ", run ./compile.sh first?")
30 libpr = ctypes.cdll.LoadLibrary(libpath)
31 libpr.seed_rrand.argtypes = [ctypes.c_uint8, ctypes.c_uint32]
32 libpr.seed_rrand.restype = ctypes.c_uint32
33 libpr.rrand.argtypes = []
34 libpr.rrand.restype = ctypes.c_uint16
38 def strong_write(file, string):
39 """Apply write(string), flush(), and os.fsync() to file."""
45 def setup_server_io():
46 """Fill IO files DB with proper file( path)s. Write process IO test string.
48 Ensure IO files directory at server/. Remove any old input file if found.
49 Set up new input file for reading, and new output file for writing. Start
50 output file with process hash line of format PID + " " + floated UNIX time
51 (io_db["teststring"]). Raise SystemExit if file is found at path of either
52 record or save file plus io_db["tmp_suffix"].
54 def detect_atomic_leftover(path, tmp_suffix):
55 path_tmp = path + tmp_suffix
56 msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
57 "aborted previous attempt to write '" + path + "'. Aborting " \
58 "until matter is resolved by removing it from its current path."
59 if os.access(path_tmp, os.F_OK):
61 io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
62 os.makedirs(io_db["path_server"], exist_ok=True)
63 io_db["file_out"] = open(io_db["path_out"], "w")
64 strong_write(io_db["file_out"], io_db["teststring"] + "\n")
65 if os.access(io_db["path_in"], os.F_OK):
66 os.remove(io_db["path_in"])
67 io_db["file_in"] = open(io_db["path_in"], "w")
68 io_db["file_in"].close()
69 io_db["file_in"] = open(io_db["path_in"], "r")
70 detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
71 detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
74 def cleanup_server_io():
75 """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
76 def helper(file_key, path_key):
78 io_db[file_key].close()
79 if not io_db["kicked_by_rival"] \
80 and os.access(io_db[path_key], os.F_OK):
81 os.remove(io_db[path_key])
82 helper("file_out", "path_out")
83 helper("file_in", "path_in")
84 helper("file_worldstate", "path_worldstate")
85 if "file_record" in io_db:
86 io_db["file_record"].close()
89 def obey(command, prefix, replay=False, do_record=False):
90 """Call function from commands_db mapped to command's first token.
92 Tokenize command string with shlex.split(comments=True). If replay is set,
93 a non-meta command from the commands_db merely triggers obey() on the next
94 command from the records file. If not, non-meta commands set
95 io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
96 do_record is set, are recorded via record(), and save_world() is called.
97 The prefix string is inserted into the server's input message between its
98 beginning 'input ' & ':'. All activity is preceded by a server_test() call.
101 print("input " + prefix + ": " + command)
103 tokens = shlex.split(command, comments=True)
104 except ValueError as err:
105 print("Can't tokenize command string: " + str(err) + ".")
107 if len(tokens) > 0 and tokens[0] in commands_db \
108 and len(tokens) == commands_db[tokens[0]][0] + 1:
109 if commands_db[tokens[0]][1]:
110 commands_db[tokens[0]][2](*tokens[1:])
112 print("Due to replay mode, reading command as 'go on in record'.")
113 line = io_db["file_record"].readline()
115 obey(line.rstrip(), io_db["file_record"].prefix
116 + str(io_db["file_record"].line_n))
117 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
119 print("Reached end of record file.")
121 commands_db[tokens[0]][2](*tokens[1:])
125 io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
126 elif 0 != len(tokens):
127 print("Invalid command/argument, or bad number of tokens.")
130 def atomic_write(path, text, do_append=False):
131 """Atomic write of text to file at path, appended if do_append is set."""
132 path_tmp = path + io_db["tmp_suffix"]
136 if os.access(path, os.F_OK):
137 shutil.copyfile(path, path_tmp)
138 file = open(path_tmp, mode)
139 strong_write(file, text)
141 if os.access(path, os.F_OK):
143 os.rename(path_tmp, path)
147 """Append command string plus newline to record file. (Atomic.)"""
148 # This misses some optimizations from the original record(), namely only
149 # finishing the atomic write with expensive flush() and fsync() every 15
150 # seconds unless explicitely forced. Implement as needed.
151 atomic_write(io_db["path_record"], command + "\n", do_append=True)
155 """Save all commands needed to reconstruct current world state."""
156 # TODO: Misses same optimizations as record() from the original record().
159 string = string.replace("\u005C", '\u005C\u005C')
160 return '"' + string.replace('"', '\u005C"') + '"'
165 if world_db["Things"][id][key]:
166 map = world_db["Things"][id][key]
167 length = world_db["MAP_LENGTH"]
168 for i in range(length):
169 line = map[i * length:(i * length) + length].decode()
170 string = string + key + " " + str(i) + " " + quote(line) \
177 for memthing in world_db["Things"][id]["T_MEMTHING"]:
178 string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
179 str(memthing[1]) + " " + str(memthing[2]) + "\n"
182 def helper(category, id_string, special_keys={}):
184 for id in world_db[category]:
185 string = string + id_string + " " + str(id) + "\n"
186 for key in world_db[category][id]:
187 if not key in special_keys:
188 x = world_db[category][id][key]
189 argument = quote(x) if str == type(x) else str(x)
190 string = string + key + " " + argument + "\n"
191 elif special_keys[key]:
192 string = string + special_keys[key](id)
197 if dict != type(world_db[key]) and key != "MAP":
198 string = string + key + " " + str(world_db[key]) + "\n"
199 string = string + helper("ThingActions", "TA_ID")
200 string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
201 for id in world_db["ThingTypes"]:
202 string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
203 str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
204 string = string + helper("Things", "T_ID",
205 {"T_CARRIES": False, "carried": False,
206 "T_MEMMAP": mapsetter("T_MEMMAP"),
207 "T_MEMTHING": memthing, "fovmap": False,
208 "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
209 for id in world_db["Things"]:
210 if [] != world_db["Things"][id]["T_CARRIES"]:
211 string = string + "T_ID " + str(id) + "\n"
212 for carried_id in world_db["Things"][id]["T_CARRIES"]:
213 string = string + "T_CARRIES " + str(carried_id) + "\n"
214 string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
215 "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
216 atomic_write(io_db["path_save"], string)
219 def obey_lines_in_file(path, name, do_record=False):
220 """Call obey() on each line of path's file, use name in input prefix."""
221 file = open(path, "r")
223 for line in file.readlines():
224 obey(line.rstrip(), name + "file line " + str(line_n),
230 def parse_command_line_arguments():
231 """Return settings values read from command line arguments."""
232 parser = argparse.ArgumentParser()
233 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
235 opts, unknown = parser.parse_known_args()
240 """Ensure valid server out file belonging to current process.
242 This is done by comparing io_db["teststring"] to what's found at the start
243 of the current file at io_db["path_out"]. On failure, set
244 io_db["kicked_by_rival"] and raise SystemExit.
246 if not os.access(io_db["path_out"], os.F_OK):
247 raise SystemExit("Server output file has disappeared.")
248 file = open(io_db["path_out"], "r")
249 test = file.readline().rstrip("\n")
251 if test != io_db["teststring"]:
252 io_db["kicked_by_rival"] = True
253 msg = "Server test string in server output file does not match. This" \
254 " indicates that the current server process has been " \
255 "superseded by another one."
256 raise SystemExit(msg)
260 """Return next newline-delimited command from server in file.
262 Keep building return string until a newline is encountered. Pause between
263 unsuccessful reads, and after too much waiting, run server_test().
270 add = io_db["file_in"].readline()
272 command = command + add
273 if len(command) > 0 and "\n" == command[-1]:
274 command = command[:-1]
277 time.sleep(wait_on_fail)
278 if now + max_wait < time.time():
284 def try_worldstate_update():
285 """Write worldstate file if io_db["worldstate_updateable"] is set."""
286 if io_db["worldstate_updateable"]:
288 def draw_visible_Things(map, run):
289 for id in world_db["Things"]:
290 type = world_db["Things"][id]["T_TYPE"]
291 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
292 alive = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]
293 if (0 == run and not consumable and not alive) \
294 or (1 == run and consumable and not alive) \
295 or (2 == run and alive):
296 y = world_db["Things"][id]["T_POSY"]
297 x = world_db["Things"][id]["T_POSX"]
298 fovflag = world_db["Things"][0]["fovmap"][(y * length) + x]
299 if 'v' == chr(fovflag):
300 c = world_db["ThingTypes"][type]["TT_SYMBOL"]
301 map[(y * length) + x] = ord(c)
303 def write_map(string, map):
304 for i in range(length):
305 line = map[i * length:(i * length) + length].decode()
306 string = string + line + "\n"
310 if [] == world_db["Things"][0]["T_CARRIES"]:
311 inventory = "(none)\n"
313 for id in world_db["Things"][0]["T_CARRIES"]:
314 type_id = world_db["Things"][id]["T_TYPE"]
315 name = world_db["ThingTypes"][type_id]["TT_NAME"]
316 inventory = inventory + name + "\n"
317 string = str(world_db["TURN"]) + "\n" + \
318 str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
319 str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
320 inventory + "%\n" + \
321 str(world_db["Things"][0]["T_POSY"]) + "\n" + \
322 str(world_db["Things"][0]["T_POSX"]) + "\n" + \
323 str(world_db["MAP_LENGTH"]) + "\n"
324 length = world_db["MAP_LENGTH"]
325 fov = bytearray(b' ' * (length ** 2))
326 for pos in range(length ** 2):
327 if 'v' == chr(world_db["Things"][0]["fovmap"][pos]):
328 fov[pos] = world_db["MAP"][pos]
330 draw_visible_Things(fov, i)
331 string = write_map(string, fov)
332 mem = world_db["Things"][0]["T_MEMMAP"][:]
334 for memthing in world_db["Things"][0]["T_MEMTHING"]:
335 type = world_db["Things"][memthing[0]]["T_TYPE"]
336 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
337 if (i == 0 and not consumable) or (i == 1 and consumable):
338 c = world_db["ThingTypes"][type]["TT_SYMBOL"]
339 mem[(memthing[1] * length) + memthing[2]] = ord(c)
340 string = write_map(string, mem)
341 atomic_write(io_db["path_worldstate"], string)
342 strong_write(io_db["file_out"], "WORLD_UPDATED\n")
343 io_db["worldstate_updateable"] = False
347 """Replay game from record file.
349 Use opts.replay as breakpoint turn to which to replay automatically before
350 switching to manual input by non-meta commands in server input file
351 triggering further reads of record file. Ensure opts.replay is at least 1.
352 Run try_worldstate_update() before each interactive obey()/read_command().
356 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
357 " (if so late a turn is to be found).")
358 if not os.access(io_db["path_record"], os.F_OK):
359 raise SystemExit("No record file found to replay.")
360 io_db["file_record"] = open(io_db["path_record"], "r")
361 io_db["file_record"].prefix = "record file line "
362 io_db["file_record"].line_n = 1
363 while world_db["TURN"] < opts.replay:
364 line = io_db["file_record"].readline()
367 obey(line.rstrip(), io_db["file_record"].prefix
368 + str(io_db["file_record"].line_n))
369 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
371 try_worldstate_update()
372 obey(read_command(), "in file", replay=True)
376 """Play game by server input file commands. Before, load save file found.
378 If no save file is found, a new world is generated from the commands in the
379 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
380 command and all that follow via the server input file. Run
381 try_worldstate_update() before each interactive obey()/read_command().
383 if os.access(io_db["path_save"], os.F_OK):
384 obey_lines_in_file(io_db["path_save"], "save")
386 if not os.access(io_db["path_worldconf"], os.F_OK):
387 msg = "No world config file from which to start a new world."
388 raise SystemExit(msg)
389 obey_lines_in_file(io_db["path_worldconf"], "world config ",
391 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
393 try_worldstate_update()
394 obey(read_command(), "in file", do_record=True)
399 world_db["MAP"] = bytearray(b'.' * (world_db["MAP_LENGTH"] ** 2))
402 def update_map_memory(t):
403 """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
404 if not t["T_MEMMAP"]:
405 t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
406 if not t["T_MEMDEPTHMAP"]:
407 t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
408 for pos in range(world_db["MAP_LENGTH"] ** 2):
409 if "v" == chr(t["fovmap"][pos]):
410 t["T_MEMDEPTHMAP"][pos] = ord("0")
411 if " " == chr(t["T_MEMMAP"][pos]):
412 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
414 # TODO: Aging of MEMDEPTHMAP.
415 for memthing in t["T_MEMTHING"]:
416 y = world_db["Things"][memthing[0]]["T_POSY"]
417 x = world_db["Things"][memthing[1]]["T_POSY"]
418 if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
419 t["T_MEMTHING"].remove(memthing)
420 for id in world_db["Things"]:
421 type = world_db["Things"][id]["T_TYPE"]
422 if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
423 y = world_db["Things"][id]["T_POSY"]
424 x = world_db["Things"][id]["T_POSY"]
425 if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
426 t["T_MEMTHING"].append((type, y, x))
429 def set_world_inactive():
430 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
432 if os.access(io_db["path_worldstate"], os.F_OK):
433 os.remove(io_db["path_worldstate"])
434 world_db["WORLD_ACTIVE"] = 0
437 def integer_test(val_string, min, max):
438 """Return val_string if possible integer >= min and <= max, else None."""
440 val = int(val_string)
441 if val < min or val > max:
445 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
450 def setter(category, key, min, max):
451 """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
454 val = integer_test(val_string, min, max)
458 if category == "Thing":
459 id_store = command_tid
460 decorator = test_Thing_id
461 elif category == "ThingType":
462 id_store = command_ttid
463 decorator = test_ThingType_id
464 elif category == "ThingAction":
465 id_store = command_taid
466 decorator = test_ThingAction_id
470 val = integer_test(val_string, min, max)
472 world_db[category + "s"][id_store.id][key] = val
476 def build_fov_map(t):
477 """Build Thing's FOV map."""
478 t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
479 # DUMMY so far. Just builds an all-visible map.
483 """Make t do nothing (but loudly, if player avatar)."""
484 if t == world_db["Things"][0]:
485 strong_write(io_db["file_out"], "LOG You wait.\n")
492 def actor_pick_up(t):
493 """Make t pick up (topmost?) Thing from ground into inventory."""
494 # Topmostness is actually not defined so far.
495 ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
496 if not world_db["Things"][id]["carried"]
497 if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
498 if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
500 world_db["Things"][ids[0]]["carried"] = True
501 t["T_CARRIES"].append(ids[0])
502 if t == world_db["Things"][0]:
503 strong_write(io_db["file_out"], "LOG You pick up an object.\n")
504 elif t == world_db["Things"][0]:
505 err = "You try to pick up an object, but there is none."
506 strong_write(io_db["file_out"], "LOG " + err + "\n")
510 """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
511 # TODO: Handle case where T_ARGUMENT matches nothing.
512 if len(t["T_CARRIES"]):
513 id = t["T_CARRIES"][t["T_ARGUMENT"]]
514 t["T_CARRIES"].remove(id)
515 world_db["Things"][id]["carried"] = False
516 if t == world_db["Things"][0]:
517 strong_write(io_db["file_out"], "LOG You drop an object.\n")
518 elif t == world_db["Things"][0]:
519 err = "You try to drop an object, but you own none."
520 strong_write(io_db["file_out"], "LOG " + err + "\n")
524 """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
525 # Original wrongly featured lifepoints increase through consumable!
526 # TODO: Handle case where T_ARGUMENT matches nothing.
527 if len(t["T_CARRIES"]):
528 id = t["T_CARRIES"][t["T_ARGUMENT"]]
529 type = world_db["Things"][id]["T_TYPE"]
530 if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
531 t["T_CARRIES"].remove(id)
532 del world_db["Things"][id]
533 t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
534 strong_write(io_db["file_out"], "LOG You consume this object.\n")
536 strong_write(io_db["file_out"], "LOG You try to use this object," +
539 strong_write(io_db["file_out"], "LOG You try to use an object, but " +
544 """Run game world and its inhabitants until new player input expected."""
547 while world_db["Things"][0]["T_LIFEPOINTS"]:
548 for id in [id for id in world_db["Things"]
549 if world_db["Things"][id]["T_LIFEPOINTS"]]:
550 Thing = world_db["Things"][id]
551 if Thing["T_LIFEPOINTS"]:
552 if not Thing["T_COMMAND"]:
553 update_map_memory(Thing)
558 Thing["T_COMMAND"] = 1
560 Thing["T_PROGRESS"] += 1
561 taid = [a for a in world_db["ThingActions"]
562 if a == Thing["T_COMMAND"]][0]
563 ThingAction = world_db["ThingActions"][taid]
564 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
565 eval("actor_" + ThingAction["TA_NAME"])(Thing)
566 Thing["T_COMMAND"] = 0
567 Thing["T_PROGRESS"] = 0
569 # DUMMY: thingproliferation
572 world_db["TURN"] += 1
576 """Return Thing of type T_TYPE, with fovmap if alive and world active."""
578 "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
590 "T_MEMDEPTHMAP": False,
593 if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
598 def id_setter(id, category, id_store=False, start_at_1=False):
599 """Set ID of object of category to manipulate ID unused? Create new one.
601 The ID is stored as id_store.id (if id_store is set). If the integer of the
602 input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
603 32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
604 >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
605 new object is created, otherwise the new object's ID.
607 min = 0 if start_at_1 else -32768
608 max = 255 if start_at_1 else 32767
610 id = integer_test(id, min, max)
612 if id in world_db[category]:
617 if (start_at_1 and 0 == id) \
618 or ((not start_at_1) and (id < 0 or id > 255)):
622 if id not in world_db[category]:
626 "No unused ID available to add to ID list.")
634 """Send PONG line to server output file."""
635 strong_write(io_db["file_out"], "PONG\n")
639 """Abort server process."""
640 raise SystemExit("received QUIT command")
643 def command_thingshere(str_y, str_x):
644 """Write to out file list of Things known to player at coordinate y, x."""
645 def write_thing_if_here():
646 if y == world_db["Things"][id]["T_POSY"] \
647 and x == world_db["Things"][id]["T_POSX"] \
648 and not world_db["Things"][id]["carried"]:
649 type = world_db["Things"][id]["T_TYPE"]
650 name = world_db["ThingTypes"][type]["TT_NAME"]
651 strong_write(io_db["file_out"], name + "\n")
652 if world_db["WORLD_ACTIVE"]:
653 y = integer_test(str_y, 0, 255)
654 x = integer_test(str_x, 0, 255)
655 length = world_db["MAP_LENGTH"]
656 if None != y and None != x and y < length and x < length:
657 pos = (y * world_db["MAP_LENGTH"]) + x
658 strong_write(io_db["file_out"], "THINGS_HERE START\n")
659 if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
660 for id in world_db["Things"]:
661 write_thing_if_here()
663 for id in world_db["Things"]["T_MEMTHING"]:
664 write_thing_if_here()
665 strong_write(io_db["file_out"], "THINGS_HERE END\n")
667 print("Ignoring: Invalid map coordinates.")
669 print("Ignoring: Command only works on existing worlds.")
672 def play_commander(action, args=False):
673 """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
675 T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
679 id = [x for x in world_db["ThingActions"]
680 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
681 world_db["Things"][0]["T_COMMAND"] = id
683 # TODO: call turn_over()
685 def set_command_and_argument_int(str_arg):
686 val = integer_test(str_arg, 0, 255)
688 world_db["Things"][0]["T_ARGUMENT"] = val
691 print("Ignoring: Argument must be integer >= 0 <=255.")
693 def set_command_and_argument_movestring(str_arg):
694 dirs = {"east": "d", "south-east": "c", "south-west": "x",
695 "west": "s", "north-west": "w", "north-east": "e"}
697 world_db["Things"][0]["T_ARGUMENT"] = dirs[str_arg]
700 print("Ignoring: Argument must be valid direction string.")
703 return set_command_and_argument_movestring
705 return set_command_and_argument_int
710 def command_seedrandomness(seed_string):
711 """Set rand seed to int(seed_string)."""
712 val = integer_test(seed_string, 0, 4294967295)
716 print("Ignoring: Value must be integer >= 0, <= 4294967295.")
719 def command_seedmap(seed_string):
720 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
721 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
725 def command_makeworld(seed_string):
726 """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
728 Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
729 "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
730 TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
731 and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
732 according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
733 of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
734 other. Init player's memory map. Write "NEW_WORLD" line to out file.
736 val = integer_test(seed_string, 0, 4294967295)
738 print("Ignoring: Value must be integer >= 0, <= 4294967295.")
741 world_db["SEED_MAP"] = val
742 player_will_be_generated = False
743 playertype = world_db["PLAYER_TYPE"]
744 for ThingType in world_db["ThingTypes"]:
745 if playertype == ThingType:
746 if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
747 player_will_be_generated = True
749 if not player_will_be_generated:
750 print("Ignoring beyond SEED_MAP: " +
751 "No player type with start number >0 defined.")
754 for ThingAction in world_db["ThingActions"]:
755 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
758 print("Ignoring beyond SEED_MAP: " +
759 "No thing action with name 'wait' defined.")
761 world_db["Things"] = {}
763 world_db["WORLD_ACTIVE"] = 1
765 for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
766 id = id_setter(-1, "Things")
767 world_db["Things"][id] = new_Thing(playertype)
769 update_map_memory(world_db["Things"][0])
770 for type in world_db["ThingTypes"]:
771 for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
772 if type != playertype:
773 id = id_setter(-1, "Things")
774 world_db["Things"][id] = new_Thing(type)
776 strong_write(io_db["file_out"], "NEW_WORLD\n")
779 def command_maplength(maplength_string):
780 """Redefine map length. Invalidate map, therefore lose all things on it."""
782 world_db["Things"] = {}
783 setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
786 def command_worldactive(worldactive_string):
787 """Toggle world_db["WORLD_ACTIVE"] if possible.
789 An active world can always be set inactive. An inactive world can only be
790 set active with a "wait" ThingAction, and a player Thing (of ID 0). On
791 activation, rebuild all Things' FOVs, and the player's map memory.
793 # In original version, map existence was also tested (unnecessarily?).
794 val = integer_test(worldactive_string, 0, 1)
796 if 0 != world_db["WORLD_ACTIVE"]:
800 print("World already active.")
801 elif 0 == world_db["WORLD_ACTIVE"]:
803 for ThingAction in world_db["ThingActions"]:
804 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
807 player_exists = False
808 for Thing in world_db["Things"]:
812 if wait_exists and player_exists:
813 for id in world_db["Things"]:
814 if world_db["Things"][id]["T_LIFEPOINTS"]:
815 build_fov_map(world_db["Things"][id])
817 update_map_memory(world_db["Things"][id])
818 world_db["WORLD_ACTIVE"] = 1
821 def test_for_id_maker(object, category):
822 """Return decorator testing for object having "id" attribute."""
825 if hasattr(object, "id"):
828 print("Ignoring: No " + category +
829 " defined to manipulate yet.")
834 def command_tid(id_string):
835 """Set ID of Thing to manipulate. ID unused? Create new one.
837 Default new Thing's type to the first available ThingType, others: zero.
839 id = id_setter(id_string, "Things", command_tid)
841 if world_db["ThingTypes"] == {}:
842 print("Ignoring: No ThingType to settle new Thing in.")
844 type = list(world_db["ThingTypes"].keys())[0]
845 world_db["Things"][id] = new_Thing(type)
848 test_Thing_id = test_for_id_maker(command_tid, "Thing")
852 def command_tcommand(str_int):
853 """Set T_COMMAND of selected Thing."""
854 val = integer_test(str_int, 0, 255)
856 if 0 == val or val in world_db["ThingActions"]:
857 world_db["Things"][command_tid.id]["T_COMMAND"] = val
859 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
863 def command_ttype(str_int):
864 """Set T_TYPE of selected Thing."""
865 val = integer_test(str_int, 0, 255)
867 if val in world_db["ThingTypes"]:
868 world_db["Things"][command_tid.id]["T_TYPE"] = val
870 print("Ignoring: ThingType ID belongs to no known ThingType.")
874 def command_tcarries(str_int):
875 """Append int(str_int) to T_CARRIES of selected Thing.
877 The ID int(str_int) must not be of the selected Thing, and must belong to a
878 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
880 val = integer_test(str_int, 0, 255)
882 if val == command_tid.id:
883 print("Ignoring: Thing cannot carry itself.")
884 elif val in world_db["Things"] \
885 and not world_db["Things"][val]["carried"]:
886 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
887 world_db["Things"][val]["carried"] = True
889 print("Ignoring: Thing not available for carrying.")
890 # Note that the whole carrying structure is different from the C version:
891 # Carried-ness is marked by a "carried" flag, not by Things containing
896 def command_tmemthing(str_t, str_y, str_x):
897 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
899 The type must fit to an existing ThingType, and the position into the map.
901 type = integer_test(str_t, 0, 255)
902 posy = integer_test(str_y, 0, 255)
903 posx = integer_test(str_x, 0, 255)
904 if None != type and None != posy and None != posx:
905 if type not in world_db["ThingTypes"] \
906 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
907 print("Ignoring: Illegal value for thing type or position.")
909 memthing = (type, posy, posx)
910 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
913 def setter_map(maptype):
914 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
916 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
919 def helper(str_int, mapline):
920 val = integer_test(str_int, 0, 255)
922 if val >= world_db["MAP_LENGTH"]:
923 print("Illegal value for map line number.")
924 elif len(mapline) != world_db["MAP_LENGTH"]:
925 print("Map line length is unequal map width.")
927 length = world_db["MAP_LENGTH"]
929 if not world_db["Things"][command_tid.id][maptype]:
930 map = bytearray(b' ' * (length ** 2))
932 map = world_db["Things"][command_tid.id][maptype]
933 map[val * length:(val * length) + length] = mapline.encode()
934 world_db["Things"][command_tid.id][maptype] = map
938 def setter_tpos(axis):
939 """Generate setter for T_POSX or T_POSY of selected Thing.
941 If world is active, rebuilds animate things' fovmap, player's memory map.
945 val = integer_test(str_int, 0, 255)
947 if val < world_db["MAP_LENGTH"]:
948 world_db["Things"][command_tid.id]["T_POS" + axis] = val
949 if world_db["WORLD_ACTIVE"] \
950 and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
951 build_fov_map(world_db["Things"][command_tid.id])
952 if 0 == command_tid.id:
953 update_map_memory(world_db["Things"][command_tid.id])
955 print("Ignoring: Position is outside of map.")
959 def command_ttid(id_string):
960 """Set ID of ThingType to manipulate. ID unused? Create new one.
962 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
964 id = id_setter(id_string, "ThingTypes", command_ttid)
966 world_db["ThingTypes"][id] = {
971 "TT_START_NUMBER": 0,
977 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
981 def command_ttname(name):
982 """Set TT_NAME of selected ThingType."""
983 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
987 def command_ttsymbol(char):
988 """Set TT_SYMBOL of selected ThingType. """
990 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
992 print("Ignoring: Argument must be single character.")
996 def command_ttcorpseid(str_int):
997 """Set TT_CORPSE_ID of selected ThingType."""
998 val = integer_test(str_int, 0, 255)
1000 if val in world_db["ThingTypes"]:
1001 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1003 print("Ignoring: Corpse ID belongs to no known ThignType.")
1006 def command_taid(id_string):
1007 """Set ID of ThingAction to manipulate. ID unused? Create new one.
1009 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1011 id = id_setter(id_string, "ThingActions", command_taid, True)
1013 world_db["ThingActions"][id] = {
1019 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1022 @test_ThingAction_id
1023 def command_taname(name):
1024 """Set TA_NAME of selected ThingAction.
1026 The name must match a valid thing action function. If after the name
1027 setting no ThingAction with name "wait" remains, call set_world_inactive().
1029 if name == "wait" or name == "move" or name == "use" or name == "drop" \
1030 or name == "pick_up":
1031 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1032 if 1 == world_db["WORLD_ACTIVE"]:
1033 wait_defined = False
1034 for id in world_db["ThingActions"]:
1035 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1038 if not wait_defined:
1039 set_world_inactive()
1041 print("Ignoring: Invalid action name.")
1042 # In contrast to the original,naming won't map a function to a ThingAction.
1045 """Commands database.
1047 Map command start tokens to ([0]) number of expected command arguments, ([1])
1048 the command's meta-ness (i.e. is it to be written to the record file, is it to
1049 be ignored in replay mode if read from server input file), and ([2]) a function
1053 "QUIT": (0, True, command_quit),
1054 "PING": (0, True, command_ping),
1055 "THINGS_HERE": (2, True, command_thingshere),
1056 "MAKE_WORLD": (1, False, command_makeworld),
1057 "SEED_MAP": (1, False, command_seedmap),
1058 "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1059 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1060 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1061 "MAP_LENGTH": (1, False, command_maplength),
1062 "WORLD_ACTIVE": (1, False, command_worldactive),
1063 "TA_ID": (1, False, command_taid),
1064 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1065 "TA_NAME": (1, False, command_taname),
1066 "TT_ID": (1, False, command_ttid),
1067 "TT_NAME": (1, False, command_ttname),
1068 "TT_SYMBOL": (1, False, command_ttsymbol),
1069 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1070 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1072 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1074 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1076 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1077 "T_ID": (1, False, command_tid),
1078 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1079 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1080 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1081 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1082 "T_COMMAND": (1, False, command_tcommand),
1083 "T_TYPE": (1, False, command_ttype),
1084 "T_CARRIES": (1, False, command_tcarries),
1085 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1086 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1087 "T_MEMTHING": (3, False, command_tmemthing),
1088 "T_POSY": (1, False, setter_tpos("Y")),
1089 "T_POSX": (1, False, setter_tpos("X")),
1090 "wait": (0, False, play_commander("wait")),
1091 "move": (1, False, play_commander("move")),
1092 "pick_up": (0, False, play_commander("pick_up")),
1093 "drop": (1, False, play_commander("drop", True)),
1094 "use": (1, False, play_commander("use", True)),
1098 """World state database. With sane default values. (Randomness is in rand.)"""
1111 """File IO database."""
1113 "path_save": "save",
1114 "path_record": "record",
1115 "path_worldconf": "confserver/world",
1116 "path_server": "server/",
1117 "path_in": "server/in",
1118 "path_out": "server/out",
1119 "path_worldstate": "server/worldstate",
1120 "tmp_suffix": "_tmp",
1121 "kicked_by_rival": False,
1122 "worldstate_updateable": False
1127 libpr = prep_library()
1128 rand = RandomnessIO()
1129 opts = parse_command_line_arguments()
1131 if None != opts.replay:
1135 except SystemExit as exit:
1136 print("ABORTING: " + exit.args[0])
1138 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")