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
575 def new_Thing(type, pos=(0,0)):
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
684 def set_command_and_argument_int(str_arg):
685 val = integer_test(str_arg, 0, 255)
687 world_db["Things"][0]["T_ARGUMENT"] = val
690 print("Ignoring: Argument must be integer >= 0 <=255.")
692 def set_command_and_argument_movestring(str_arg):
693 dirs = {"east": "d", "south-east": "c", "south-west": "x",
694 "west": "s", "north-west": "w", "north-east": "e"}
696 world_db["Things"][0]["T_ARGUMENT"] = dirs[str_arg]
699 print("Ignoring: Argument must be valid direction string.")
702 return set_command_and_argument_movestring
704 return set_command_and_argument_int
709 def command_seedrandomness(seed_string):
710 """Set rand seed to int(seed_string)."""
711 val = integer_test(seed_string, 0, 4294967295)
715 print("Ignoring: Value must be integer >= 0, <= 4294967295.")
718 def command_seedmap(seed_string):
719 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
720 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
724 def command_makeworld(seed_string):
725 """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
727 Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
728 "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
729 TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
730 and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
731 according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
732 of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
733 other. Init player's memory map. Write "NEW_WORLD" line to out file.
739 err = "Space to put thing on too hard to find. Map too small?"
741 y = rand.next() % world_db["MAP_LENGTH"]
742 x = rand.next() % world_db["MAP_LENGTH"]
743 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
747 raise SystemExit(err)
748 # Replica of C code, wrongly ignores animatedness of new Thing.
749 pos_clear = (0 == len([id for id in world_db["Things"]
750 if world_db["Things"][id]["T_LIFEPOINTS"]
751 if world_db["Things"][id]["T_POSY"] == y
752 if world_db["Things"][id]["T_POSX"] == x]))
757 val = integer_test(seed_string, 0, 4294967295)
759 print("Ignoring: Value must be integer >= 0, <= 4294967295.")
762 world_db["SEED_MAP"] = val
763 player_will_be_generated = False
764 playertype = world_db["PLAYER_TYPE"]
765 for ThingType in world_db["ThingTypes"]:
766 if playertype == ThingType:
767 if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
768 player_will_be_generated = True
770 if not player_will_be_generated:
771 print("Ignoring beyond SEED_MAP: " +
772 "No player type with start number >0 defined.")
775 for ThingAction in world_db["ThingActions"]:
776 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
779 print("Ignoring beyond SEED_MAP: " +
780 "No thing action with name 'wait' defined.")
782 world_db["Things"] = {}
784 world_db["WORLD_ACTIVE"] = 1
786 for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
787 id = id_setter(-1, "Things")
788 world_db["Things"][id] = new_Thing(playertype, free_pos())
789 update_map_memory(world_db["Things"][0])
790 for type in world_db["ThingTypes"]:
791 for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
792 if type != playertype:
793 id = id_setter(-1, "Things")
794 world_db["Things"][id] = new_Thing(type, free_pos())
795 strong_write(io_db["file_out"], "NEW_WORLD\n")
798 def command_maplength(maplength_string):
799 """Redefine map length. Invalidate map, therefore lose all things on it."""
801 world_db["Things"] = {}
802 setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
805 def command_worldactive(worldactive_string):
806 """Toggle world_db["WORLD_ACTIVE"] if possible.
808 An active world can always be set inactive. An inactive world can only be
809 set active with a "wait" ThingAction, and a player Thing (of ID 0). On
810 activation, rebuild all Things' FOVs, and the player's map memory.
812 # In original version, map existence was also tested (unnecessarily?).
813 val = integer_test(worldactive_string, 0, 1)
815 if 0 != world_db["WORLD_ACTIVE"]:
819 print("World already active.")
820 elif 0 == world_db["WORLD_ACTIVE"]:
822 for ThingAction in world_db["ThingActions"]:
823 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
826 player_exists = False
827 for Thing in world_db["Things"]:
831 if wait_exists and player_exists:
832 for id in world_db["Things"]:
833 if world_db["Things"][id]["T_LIFEPOINTS"]:
834 build_fov_map(world_db["Things"][id])
836 update_map_memory(world_db["Things"][id])
837 world_db["WORLD_ACTIVE"] = 1
840 def test_for_id_maker(object, category):
841 """Return decorator testing for object having "id" attribute."""
844 if hasattr(object, "id"):
847 print("Ignoring: No " + category +
848 " defined to manipulate yet.")
853 def command_tid(id_string):
854 """Set ID of Thing to manipulate. ID unused? Create new one.
856 Default new Thing's type to the first available ThingType, others: zero.
858 id = id_setter(id_string, "Things", command_tid)
860 if world_db["ThingTypes"] == {}:
861 print("Ignoring: No ThingType to settle new Thing in.")
863 type = list(world_db["ThingTypes"].keys())[0]
864 world_db["Things"][id] = new_Thing(type)
867 test_Thing_id = test_for_id_maker(command_tid, "Thing")
871 def command_tcommand(str_int):
872 """Set T_COMMAND of selected Thing."""
873 val = integer_test(str_int, 0, 255)
875 if 0 == val or val in world_db["ThingActions"]:
876 world_db["Things"][command_tid.id]["T_COMMAND"] = val
878 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
882 def command_ttype(str_int):
883 """Set T_TYPE of selected Thing."""
884 val = integer_test(str_int, 0, 255)
886 if val in world_db["ThingTypes"]:
887 world_db["Things"][command_tid.id]["T_TYPE"] = val
889 print("Ignoring: ThingType ID belongs to no known ThingType.")
893 def command_tcarries(str_int):
894 """Append int(str_int) to T_CARRIES of selected Thing.
896 The ID int(str_int) must not be of the selected Thing, and must belong to a
897 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
899 val = integer_test(str_int, 0, 255)
901 if val == command_tid.id:
902 print("Ignoring: Thing cannot carry itself.")
903 elif val in world_db["Things"] \
904 and not world_db["Things"][val]["carried"]:
905 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
906 world_db["Things"][val]["carried"] = True
908 print("Ignoring: Thing not available for carrying.")
909 # Note that the whole carrying structure is different from the C version:
910 # Carried-ness is marked by a "carried" flag, not by Things containing
915 def command_tmemthing(str_t, str_y, str_x):
916 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
918 The type must fit to an existing ThingType, and the position into the map.
920 type = integer_test(str_t, 0, 255)
921 posy = integer_test(str_y, 0, 255)
922 posx = integer_test(str_x, 0, 255)
923 if None != type and None != posy and None != posx:
924 if type not in world_db["ThingTypes"] \
925 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
926 print("Ignoring: Illegal value for thing type or position.")
928 memthing = (type, posy, posx)
929 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
932 def setter_map(maptype):
933 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
935 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
938 def helper(str_int, mapline):
939 val = integer_test(str_int, 0, 255)
941 if val >= world_db["MAP_LENGTH"]:
942 print("Illegal value for map line number.")
943 elif len(mapline) != world_db["MAP_LENGTH"]:
944 print("Map line length is unequal map width.")
946 length = world_db["MAP_LENGTH"]
948 if not world_db["Things"][command_tid.id][maptype]:
949 map = bytearray(b' ' * (length ** 2))
951 map = world_db["Things"][command_tid.id][maptype]
952 map[val * length:(val * length) + length] = mapline.encode()
953 world_db["Things"][command_tid.id][maptype] = map
957 def setter_tpos(axis):
958 """Generate setter for T_POSX or T_POSY of selected Thing.
960 If world is active, rebuilds animate things' fovmap, player's memory map.
964 val = integer_test(str_int, 0, 255)
966 if val < world_db["MAP_LENGTH"]:
967 world_db["Things"][command_tid.id]["T_POS" + axis] = val
968 if world_db["WORLD_ACTIVE"] \
969 and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
970 build_fov_map(world_db["Things"][command_tid.id])
971 if 0 == command_tid.id:
972 update_map_memory(world_db["Things"][command_tid.id])
974 print("Ignoring: Position is outside of map.")
978 def command_ttid(id_string):
979 """Set ID of ThingType to manipulate. ID unused? Create new one.
981 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
983 id = id_setter(id_string, "ThingTypes", command_ttid)
985 world_db["ThingTypes"][id] = {
990 "TT_START_NUMBER": 0,
996 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1000 def command_ttname(name):
1001 """Set TT_NAME of selected ThingType."""
1002 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1006 def command_ttsymbol(char):
1007 """Set TT_SYMBOL of selected ThingType. """
1009 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1011 print("Ignoring: Argument must be single character.")
1015 def command_ttcorpseid(str_int):
1016 """Set TT_CORPSE_ID of selected ThingType."""
1017 val = integer_test(str_int, 0, 255)
1019 if val in world_db["ThingTypes"]:
1020 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1022 print("Ignoring: Corpse ID belongs to no known ThignType.")
1025 def command_taid(id_string):
1026 """Set ID of ThingAction to manipulate. ID unused? Create new one.
1028 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1030 id = id_setter(id_string, "ThingActions", command_taid, True)
1032 world_db["ThingActions"][id] = {
1038 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1041 @test_ThingAction_id
1042 def command_taname(name):
1043 """Set TA_NAME of selected ThingAction.
1045 The name must match a valid thing action function. If after the name
1046 setting no ThingAction with name "wait" remains, call set_world_inactive().
1048 if name == "wait" or name == "move" or name == "use" or name == "drop" \
1049 or name == "pick_up":
1050 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1051 if 1 == world_db["WORLD_ACTIVE"]:
1052 wait_defined = False
1053 for id in world_db["ThingActions"]:
1054 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1057 if not wait_defined:
1058 set_world_inactive()
1060 print("Ignoring: Invalid action name.")
1061 # In contrast to the original,naming won't map a function to a ThingAction.
1064 """Commands database.
1066 Map command start tokens to ([0]) number of expected command arguments, ([1])
1067 the command's meta-ness (i.e. is it to be written to the record file, is it to
1068 be ignored in replay mode if read from server input file), and ([2]) a function
1072 "QUIT": (0, True, command_quit),
1073 "PING": (0, True, command_ping),
1074 "THINGS_HERE": (2, True, command_thingshere),
1075 "MAKE_WORLD": (1, False, command_makeworld),
1076 "SEED_MAP": (1, False, command_seedmap),
1077 "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1078 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1079 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1080 "MAP_LENGTH": (1, False, command_maplength),
1081 "WORLD_ACTIVE": (1, False, command_worldactive),
1082 "TA_ID": (1, False, command_taid),
1083 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1084 "TA_NAME": (1, False, command_taname),
1085 "TT_ID": (1, False, command_ttid),
1086 "TT_NAME": (1, False, command_ttname),
1087 "TT_SYMBOL": (1, False, command_ttsymbol),
1088 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1089 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1091 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1093 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1095 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1096 "T_ID": (1, False, command_tid),
1097 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1098 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1099 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1100 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1101 "T_COMMAND": (1, False, command_tcommand),
1102 "T_TYPE": (1, False, command_ttype),
1103 "T_CARRIES": (1, False, command_tcarries),
1104 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1105 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1106 "T_MEMTHING": (3, False, command_tmemthing),
1107 "T_POSY": (1, False, setter_tpos("Y")),
1108 "T_POSX": (1, False, setter_tpos("X")),
1109 "wait": (0, False, play_commander("wait")),
1110 "move": (1, False, play_commander("move")),
1111 "pick_up": (0, False, play_commander("pick_up")),
1112 "drop": (1, False, play_commander("drop", True)),
1113 "use": (1, False, play_commander("use", True)),
1117 """World state database. With sane default values. (Randomness is in rand.)"""
1130 """File IO database."""
1132 "path_save": "save",
1133 "path_record": "record",
1134 "path_worldconf": "confserver/world",
1135 "path_server": "server/",
1136 "path_in": "server/in",
1137 "path_out": "server/out",
1138 "path_worldstate": "server/worldstate",
1139 "tmp_suffix": "_tmp",
1140 "kicked_by_rival": False,
1141 "worldstate_updateable": False
1146 libpr = prep_library()
1147 rand = RandomnessIO()
1148 opts = parse_command_line_arguments()
1150 if None != opts.replay:
1154 except SystemExit as exit:
1155 print("ABORTING: " + exit.args[0])
1157 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")