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
35 libpr.set_maplength.argtypes = [ctypes.c_uint16]
36 libpr.mv_yx_in_dir_legal_wrap.argtypes = [ctypes.c_char, ctypes.c_uint8,
38 libpr.mv_yx_in_dir_legal_wrap.restype = ctypes.c_uint8
42 def strong_write(file, string):
43 """Apply write(string), flush(), and os.fsync() to file."""
49 def setup_server_io():
50 """Fill IO files DB with proper file( path)s. Write process IO test string.
52 Ensure IO files directory at server/. Remove any old input file if found.
53 Set up new input file for reading, and new output file for writing. Start
54 output file with process hash line of format PID + " " + floated UNIX time
55 (io_db["teststring"]). Raise SystemExit if file is found at path of either
56 record or save file plus io_db["tmp_suffix"].
58 def detect_atomic_leftover(path, tmp_suffix):
59 path_tmp = path + tmp_suffix
60 msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
61 "aborted previous attempt to write '" + path + "'. Aborting " \
62 "until matter is resolved by removing it from its current path."
63 if os.access(path_tmp, os.F_OK):
65 io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
66 os.makedirs(io_db["path_server"], exist_ok=True)
67 io_db["file_out"] = open(io_db["path_out"], "w")
68 strong_write(io_db["file_out"], io_db["teststring"] + "\n")
69 if os.access(io_db["path_in"], os.F_OK):
70 os.remove(io_db["path_in"])
71 io_db["file_in"] = open(io_db["path_in"], "w")
72 io_db["file_in"].close()
73 io_db["file_in"] = open(io_db["path_in"], "r")
74 detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
75 detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
78 def cleanup_server_io():
79 """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
80 def helper(file_key, path_key):
82 io_db[file_key].close()
83 if not io_db["kicked_by_rival"] \
84 and os.access(io_db[path_key], os.F_OK):
85 os.remove(io_db[path_key])
86 helper("file_out", "path_out")
87 helper("file_in", "path_in")
88 helper("file_worldstate", "path_worldstate")
89 if "file_record" in io_db:
90 io_db["file_record"].close()
93 def obey(command, prefix, replay=False, do_record=False):
94 """Call function from commands_db mapped to command's first token.
96 Tokenize command string with shlex.split(comments=True). If replay is set,
97 a non-meta command from the commands_db merely triggers obey() on the next
98 command from the records file. If not, non-meta commands set
99 io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
100 do_record is set, are recorded via record(), and save_world() is called.
101 The prefix string is inserted into the server's input message between its
102 beginning 'input ' & ':'. All activity is preceded by a server_test() call.
105 print("input " + prefix + ": " + command)
107 tokens = shlex.split(command, comments=True)
108 except ValueError as err:
109 print("Can't tokenize command string: " + str(err) + ".")
111 if len(tokens) > 0 and tokens[0] in commands_db \
112 and len(tokens) == commands_db[tokens[0]][0] + 1:
113 if commands_db[tokens[0]][1]:
114 commands_db[tokens[0]][2](*tokens[1:])
116 print("Due to replay mode, reading command as 'go on in record'.")
117 line = io_db["file_record"].readline()
119 obey(line.rstrip(), io_db["file_record"].prefix
120 + str(io_db["file_record"].line_n))
121 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
123 print("Reached end of record file.")
125 commands_db[tokens[0]][2](*tokens[1:])
129 io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
130 elif 0 != len(tokens):
131 print("Invalid command/argument, or bad number of tokens.")
134 def atomic_write(path, text, do_append=False):
135 """Atomic write of text to file at path, appended if do_append is set."""
136 path_tmp = path + io_db["tmp_suffix"]
140 if os.access(path, os.F_OK):
141 shutil.copyfile(path, path_tmp)
142 file = open(path_tmp, mode)
143 strong_write(file, text)
145 if os.access(path, os.F_OK):
147 os.rename(path_tmp, path)
151 """Append command string plus newline to record file. (Atomic.)"""
152 # This misses some optimizations from the original record(), namely only
153 # finishing the atomic write with expensive flush() and fsync() every 15
154 # seconds unless explicitely forced. Implement as needed.
155 atomic_write(io_db["path_record"], command + "\n", do_append=True)
159 """Save all commands needed to reconstruct current world state."""
160 # TODO: Misses same optimizations as record() from the original record().
163 string = string.replace("\u005C", '\u005C\u005C')
164 return '"' + string.replace('"', '\u005C"') + '"'
169 if world_db["Things"][id][key]:
170 map = world_db["Things"][id][key]
171 length = world_db["MAP_LENGTH"]
172 for i in range(length):
173 line = map[i * length:(i * length) + length].decode()
174 string = string + key + " " + str(i) + " " + quote(line) \
181 for memthing in world_db["Things"][id]["T_MEMTHING"]:
182 string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
183 str(memthing[1]) + " " + str(memthing[2]) + "\n"
186 def helper(category, id_string, special_keys={}):
188 for id in world_db[category]:
189 string = string + id_string + " " + str(id) + "\n"
190 for key in world_db[category][id]:
191 if not key in special_keys:
192 x = world_db[category][id][key]
193 argument = quote(x) if str == type(x) else str(x)
194 string = string + key + " " + argument + "\n"
195 elif special_keys[key]:
196 string = string + special_keys[key](id)
201 if dict != type(world_db[key]) and key != "MAP":
202 string = string + key + " " + str(world_db[key]) + "\n"
203 string = string + helper("ThingActions", "TA_ID")
204 string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
205 for id in world_db["ThingTypes"]:
206 string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
207 str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
208 string = string + helper("Things", "T_ID",
209 {"T_CARRIES": False, "carried": False,
210 "T_MEMMAP": mapsetter("T_MEMMAP"),
211 "T_MEMTHING": memthing, "fovmap": False,
212 "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
213 for id in world_db["Things"]:
214 if [] != world_db["Things"][id]["T_CARRIES"]:
215 string = string + "T_ID " + str(id) + "\n"
216 for carried_id in world_db["Things"][id]["T_CARRIES"]:
217 string = string + "T_CARRIES " + str(carried_id) + "\n"
218 string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
219 "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
220 atomic_write(io_db["path_save"], string)
223 def obey_lines_in_file(path, name, do_record=False):
224 """Call obey() on each line of path's file, use name in input prefix."""
225 file = open(path, "r")
227 for line in file.readlines():
228 obey(line.rstrip(), name + "file line " + str(line_n),
234 def parse_command_line_arguments():
235 """Return settings values read from command line arguments."""
236 parser = argparse.ArgumentParser()
237 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
239 opts, unknown = parser.parse_known_args()
244 """Ensure valid server out file belonging to current process.
246 This is done by comparing io_db["teststring"] to what's found at the start
247 of the current file at io_db["path_out"]. On failure, set
248 io_db["kicked_by_rival"] and raise SystemExit.
250 if not os.access(io_db["path_out"], os.F_OK):
251 raise SystemExit("Server output file has disappeared.")
252 file = open(io_db["path_out"], "r")
253 test = file.readline().rstrip("\n")
255 if test != io_db["teststring"]:
256 io_db["kicked_by_rival"] = True
257 msg = "Server test string in server output file does not match. This" \
258 " indicates that the current server process has been " \
259 "superseded by another one."
260 raise SystemExit(msg)
264 """Return next newline-delimited command from server in file.
266 Keep building return string until a newline is encountered. Pause between
267 unsuccessful reads, and after too much waiting, run server_test().
274 add = io_db["file_in"].readline()
276 command = command + add
277 if len(command) > 0 and "\n" == command[-1]:
278 command = command[:-1]
281 time.sleep(wait_on_fail)
282 if now + max_wait < time.time():
288 def try_worldstate_update():
289 """Write worldstate file if io_db["worldstate_updateable"] is set."""
290 if io_db["worldstate_updateable"]:
292 def draw_visible_Things(map, run):
293 for id in world_db["Things"]:
294 type = world_db["Things"][id]["T_TYPE"]
295 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
296 alive = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]
297 if (0 == run and not consumable and not alive) \
298 or (1 == run and consumable and not alive) \
299 or (2 == run and alive):
300 y = world_db["Things"][id]["T_POSY"]
301 x = world_db["Things"][id]["T_POSX"]
302 fovflag = world_db["Things"][0]["fovmap"][(y * length) + x]
303 if 'v' == chr(fovflag):
304 c = world_db["ThingTypes"][type]["TT_SYMBOL"]
305 map[(y * length) + x] = ord(c)
307 def write_map(string, map):
308 for i in range(length):
309 line = map[i * length:(i * length) + length].decode()
310 string = string + line + "\n"
314 if [] == world_db["Things"][0]["T_CARRIES"]:
315 inventory = "(none)\n"
317 for id in world_db["Things"][0]["T_CARRIES"]:
318 type_id = world_db["Things"][id]["T_TYPE"]
319 name = world_db["ThingTypes"][type_id]["TT_NAME"]
320 inventory = inventory + name + "\n"
321 string = str(world_db["TURN"]) + "\n" + \
322 str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
323 str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
324 inventory + "%\n" + \
325 str(world_db["Things"][0]["T_POSY"]) + "\n" + \
326 str(world_db["Things"][0]["T_POSX"]) + "\n" + \
327 str(world_db["MAP_LENGTH"]) + "\n"
328 length = world_db["MAP_LENGTH"]
329 fov = bytearray(b' ' * (length ** 2))
330 for pos in range(length ** 2):
331 if 'v' == chr(world_db["Things"][0]["fovmap"][pos]):
332 fov[pos] = world_db["MAP"][pos]
334 draw_visible_Things(fov, i)
335 string = write_map(string, fov)
336 mem = world_db["Things"][0]["T_MEMMAP"][:]
338 for memthing in world_db["Things"][0]["T_MEMTHING"]:
339 type = world_db["Things"][memthing[0]]["T_TYPE"]
340 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
341 if (i == 0 and not consumable) or (i == 1 and consumable):
342 c = world_db["ThingTypes"][type]["TT_SYMBOL"]
343 mem[(memthing[1] * length) + memthing[2]] = ord(c)
344 string = write_map(string, mem)
345 atomic_write(io_db["path_worldstate"], string)
346 strong_write(io_db["file_out"], "WORLD_UPDATED\n")
347 io_db["worldstate_updateable"] = False
351 """Replay game from record file.
353 Use opts.replay as breakpoint turn to which to replay automatically before
354 switching to manual input by non-meta commands in server input file
355 triggering further reads of record file. Ensure opts.replay is at least 1.
356 Run try_worldstate_update() before each interactive obey()/read_command().
360 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
361 " (if so late a turn is to be found).")
362 if not os.access(io_db["path_record"], os.F_OK):
363 raise SystemExit("No record file found to replay.")
364 io_db["file_record"] = open(io_db["path_record"], "r")
365 io_db["file_record"].prefix = "record file line "
366 io_db["file_record"].line_n = 1
367 while world_db["TURN"] < opts.replay:
368 line = io_db["file_record"].readline()
371 obey(line.rstrip(), io_db["file_record"].prefix
372 + str(io_db["file_record"].line_n))
373 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
375 try_worldstate_update()
376 obey(read_command(), "in file", replay=True)
380 """Play game by server input file commands. Before, load save file found.
382 If no save file is found, a new world is generated from the commands in the
383 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
384 command and all that follow via the server input file. Run
385 try_worldstate_update() before each interactive obey()/read_command().
387 if os.access(io_db["path_save"], os.F_OK):
388 obey_lines_in_file(io_db["path_save"], "save")
390 if not os.access(io_db["path_worldconf"], os.F_OK):
391 msg = "No world config file from which to start a new world."
392 raise SystemExit(msg)
393 obey_lines_in_file(io_db["path_worldconf"], "world config ",
395 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
397 try_worldstate_update()
398 obey(read_command(), "in file", do_record=True)
402 """(Re-)make island map.
404 Let "~" represent water, "." land, "X" trees: Build island shape randomly,
405 start with one land cell in the middle, then go into cycle of repeatedly
406 selecting a random sea cell and transforming it into land if it is neighbor
407 to land. The cycle ends when a land cell is due to be created at the map's
408 border. Then put some trees on the map (TODO: more precise algorithm desc).
410 def is_neighbor(coordinates, type):
413 length = world_db["MAP_LENGTH"]
415 diag_west = x + (ind > 0)
416 diag_east = x + (ind < (length - 1))
417 pos = (y * length) + x
418 if (y > 0 and diag_east
419 and type == chr(world_db["MAP"][pos - length + ind])) \
421 and type == chr(world_db["MAP"][pos + 1])) \
422 or (y < (length - 1) and diag_east
423 and type == chr(world_db["MAP"][pos + length + ind])) \
424 or (y > 0 and diag_west
425 and type == chr(world_db["MAP"][pos - length - (not ind)])) \
427 and type == chr(world_db["MAP"][pos - 1])) \
428 or (y < (length - 1) and diag_west
429 and type == chr(world_db["MAP"][pos + length - (not ind)])):
432 store_seed = rand.seed
433 world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
434 length = world_db["MAP_LENGTH"]
435 add_half_width = (not (length % 2)) * int(length / 2)
436 world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
438 y = rand.next() % length
439 x = rand.next() % length
440 pos = (y * length) + x
441 if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
442 if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
444 world_db["MAP"][pos] = ord(".")
445 n_trees = int((length ** 2) / 16)
447 while (i_trees <= n_trees):
448 single_allowed = rand.next() % 32
449 y = rand.next() % length
450 x = rand.next() % length
451 pos = (y * length) + x
452 if "." == chr(world_db["MAP"][pos]) \
453 and ((not single_allowed) or is_neighbor((y, x), "X")):
454 world_db["MAP"][pos] = ord("X")
456 rand.seed = store_seed
457 # This all-too-precise replica of the original C code misses iter_limit().
460 def update_map_memory(t):
461 """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
462 if not t["T_MEMMAP"]:
463 t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
464 if not t["T_MEMDEPTHMAP"]:
465 t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
466 for pos in range(world_db["MAP_LENGTH"] ** 2):
467 if "v" == chr(t["fovmap"][pos]):
468 t["T_MEMDEPTHMAP"][pos] = ord("0")
469 if " " == chr(t["T_MEMMAP"][pos]):
470 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
472 # TODO: Aging of MEMDEPTHMAP.
473 for memthing in t["T_MEMTHING"]:
474 y = world_db["Things"][memthing[0]]["T_POSY"]
475 x = world_db["Things"][memthing[1]]["T_POSY"]
476 if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
477 t["T_MEMTHING"].remove(memthing)
478 for id in world_db["Things"]:
479 type = world_db["Things"][id]["T_TYPE"]
480 if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
481 y = world_db["Things"][id]["T_POSY"]
482 x = world_db["Things"][id]["T_POSY"]
483 if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
484 t["T_MEMTHING"].append((type, y, x))
487 def set_world_inactive():
488 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
490 if os.access(io_db["path_worldstate"], os.F_OK):
491 os.remove(io_db["path_worldstate"])
492 world_db["WORLD_ACTIVE"] = 0
495 def integer_test(val_string, min, max):
496 """Return val_string if possible integer >= min and <= max, else None."""
498 val = int(val_string)
499 if val < min or val > max:
503 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
508 def setter(category, key, min, max):
509 """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
512 val = integer_test(val_string, min, max)
516 if category == "Thing":
517 id_store = command_tid
518 decorator = test_Thing_id
519 elif category == "ThingType":
520 id_store = command_ttid
521 decorator = test_ThingType_id
522 elif category == "ThingAction":
523 id_store = command_taid
524 decorator = test_ThingAction_id
528 val = integer_test(val_string, min, max)
530 world_db[category + "s"][id_store.id][key] = val
534 def build_fov_map(t):
535 """Build Thing's FOV map."""
536 t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
537 # DUMMY so far. Just builds an all-visible map.
541 """Make t do nothing (but loudly, if player avatar)."""
542 if t == world_db["Things"][0]:
543 strong_write(io_db["file_out"], "LOG You wait.\n")
550 def actor_pick_up(t):
551 """Make t pick up (topmost?) Thing from ground into inventory."""
552 # Topmostness is actually not defined so far.
553 ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
554 if not world_db["Things"][id]["carried"]
555 if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
556 if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
558 world_db["Things"][ids[0]]["carried"] = True
559 t["T_CARRIES"].append(ids[0])
560 if t == world_db["Things"][0]:
561 strong_write(io_db["file_out"], "LOG You pick up an object.\n")
562 elif t == world_db["Things"][0]:
563 err = "You try to pick up an object, but there is none."
564 strong_write(io_db["file_out"], "LOG " + err + "\n")
568 """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
569 # TODO: Handle case where T_ARGUMENT matches nothing.
570 if len(t["T_CARRIES"]):
571 id = t["T_CARRIES"][t["T_ARGUMENT"]]
572 t["T_CARRIES"].remove(id)
573 world_db["Things"][id]["carried"] = False
574 if t == world_db["Things"][0]:
575 strong_write(io_db["file_out"], "LOG You drop an object.\n")
576 elif t == world_db["Things"][0]:
577 err = "You try to drop an object, but you own none."
578 strong_write(io_db["file_out"], "LOG " + err + "\n")
582 """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
583 # Original wrongly featured lifepoints increase through consumable!
584 # TODO: Handle case where T_ARGUMENT matches nothing.
585 if len(t["T_CARRIES"]):
586 id = t["T_CARRIES"][t["T_ARGUMENT"]]
587 type = world_db["Things"][id]["T_TYPE"]
588 if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
589 t["T_CARRIES"].remove(id)
590 del world_db["Things"][id]
591 t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
592 strong_write(io_db["file_out"], "LOG You consume this object.\n")
594 strong_write(io_db["file_out"], "LOG You try to use this object," +
597 strong_write(io_db["file_out"], "LOG You try to use an object, but " +
602 """Run game world and its inhabitants until new player input expected."""
605 while world_db["Things"][0]["T_LIFEPOINTS"]:
606 for id in [id for id in world_db["Things"]
607 if world_db["Things"][id]["T_LIFEPOINTS"]]:
608 Thing = world_db["Things"][id]
609 if Thing["T_LIFEPOINTS"]:
610 if not Thing["T_COMMAND"]:
611 update_map_memory(Thing)
616 Thing["T_COMMAND"] = 1
618 Thing["T_PROGRESS"] += 1
619 taid = [a for a in world_db["ThingActions"]
620 if a == Thing["T_COMMAND"]][0]
621 ThingAction = world_db["ThingActions"][taid]
622 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
623 eval("actor_" + ThingAction["TA_NAME"])(Thing)
624 Thing["T_COMMAND"] = 0
625 Thing["T_PROGRESS"] = 0
627 # DUMMY: thingproliferation
630 world_db["TURN"] += 1
633 def new_Thing(type, pos=(0,0)):
634 """Return Thing of type T_TYPE, with fovmap if alive and world active."""
636 "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
648 "T_MEMDEPTHMAP": False,
651 if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
656 def id_setter(id, category, id_store=False, start_at_1=False):
657 """Set ID of object of category to manipulate ID unused? Create new one.
659 The ID is stored as id_store.id (if id_store is set). If the integer of the
660 input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
661 32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
662 >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
663 new object is created, otherwise the new object's ID.
665 min = 0 if start_at_1 else -32768
666 max = 255 if start_at_1 else 32767
668 id = integer_test(id, min, max)
670 if id in world_db[category]:
675 if (start_at_1 and 0 == id) \
676 or ((not start_at_1) and (id < 0 or id > 255)):
680 if id not in world_db[category]:
684 "No unused ID available to add to ID list.")
692 """Send PONG line to server output file."""
693 strong_write(io_db["file_out"], "PONG\n")
697 """Abort server process."""
698 raise SystemExit("received QUIT command")
701 def command_thingshere(str_y, str_x):
702 """Write to out file list of Things known to player at coordinate y, x."""
703 def write_thing_if_here():
704 if y == world_db["Things"][id]["T_POSY"] \
705 and x == world_db["Things"][id]["T_POSX"] \
706 and not world_db["Things"][id]["carried"]:
707 type = world_db["Things"][id]["T_TYPE"]
708 name = world_db["ThingTypes"][type]["TT_NAME"]
709 strong_write(io_db["file_out"], name + "\n")
710 if world_db["WORLD_ACTIVE"]:
711 y = integer_test(str_y, 0, 255)
712 x = integer_test(str_x, 0, 255)
713 length = world_db["MAP_LENGTH"]
714 if None != y and None != x and y < length and x < length:
715 pos = (y * world_db["MAP_LENGTH"]) + x
716 strong_write(io_db["file_out"], "THINGS_HERE START\n")
717 if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
718 for id in world_db["Things"]:
719 write_thing_if_here()
721 for id in world_db["Things"]["T_MEMTHING"]:
722 write_thing_if_here()
723 strong_write(io_db["file_out"], "THINGS_HERE END\n")
725 print("Ignoring: Invalid map coordinates.")
727 print("Ignoring: Command only works on existing worlds.")
730 def play_commander(action, args=False):
731 """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
733 T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
737 id = [x for x in world_db["ThingActions"]
738 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
739 world_db["Things"][0]["T_COMMAND"] = id
742 def set_command_and_argument_int(str_arg):
743 val = integer_test(str_arg, 0, 255)
745 world_db["Things"][0]["T_ARGUMENT"] = val
748 def set_command_and_argument_movestring(str_arg):
749 dirs = {"east": "d", "south-east": "c", "south-west": "x",
750 "west": "s", "north-west": "w", "north-east": "e"}
752 world_db["Things"][0]["T_ARGUMENT"] = dirs[str_arg]
755 print("Ignoring: Argument must be valid direction string.")
758 return set_command_and_argument_movestring
760 return set_command_and_argument_int
765 def command_seedrandomness(seed_string):
766 """Set rand seed to int(seed_string)."""
767 val = integer_test(seed_string, 0, 4294967295)
772 def command_seedmap(seed_string):
773 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
774 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
778 def command_makeworld(seed_string):
779 """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
781 Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
782 "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
783 TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
784 and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
785 according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
786 of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
787 other. Init player's memory map. Write "NEW_WORLD" line to out file.
793 err = "Space to put thing on too hard to find. Map too small?"
795 y = rand.next() % world_db["MAP_LENGTH"]
796 x = rand.next() % world_db["MAP_LENGTH"]
797 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
801 raise SystemExit(err)
802 # Replica of C code, wrongly ignores animatedness of new Thing.
803 pos_clear = (0 == len([id for id in world_db["Things"]
804 if world_db["Things"][id]["T_LIFEPOINTS"]
805 if world_db["Things"][id]["T_POSY"] == y
806 if world_db["Things"][id]["T_POSX"] == x]))
811 val = integer_test(seed_string, 0, 4294967295)
815 world_db["SEED_MAP"] = val
816 player_will_be_generated = False
817 playertype = world_db["PLAYER_TYPE"]
818 for ThingType in world_db["ThingTypes"]:
819 if playertype == ThingType:
820 if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
821 player_will_be_generated = True
823 if not player_will_be_generated:
824 print("Ignoring beyond SEED_MAP: " +
825 "No player type with start number >0 defined.")
828 for ThingAction in world_db["ThingActions"]:
829 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
832 print("Ignoring beyond SEED_MAP: " +
833 "No thing action with name 'wait' defined.")
835 world_db["Things"] = {}
837 world_db["WORLD_ACTIVE"] = 1
839 for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
840 id = id_setter(-1, "Things")
841 world_db["Things"][id] = new_Thing(playertype, free_pos())
842 update_map_memory(world_db["Things"][0])
843 for type in world_db["ThingTypes"]:
844 for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
845 if type != playertype:
846 id = id_setter(-1, "Things")
847 world_db["Things"][id] = new_Thing(type, free_pos())
848 strong_write(io_db["file_out"], "NEW_WORLD\n")
851 def command_maplength(maplength_string):
852 """Redefine map length. Invalidate map, therefore lose all things on it."""
853 val = integer_test(maplength_string, 1, 256)
855 world_db["MAP_LENGTH"] = val
857 world_db["Things"] = {}
858 libpr.set_maplength = val
861 def command_worldactive(worldactive_string):
862 """Toggle world_db["WORLD_ACTIVE"] if possible.
864 An active world can always be set inactive. An inactive world can only be
865 set active with a "wait" ThingAction, and a player Thing (of ID 0). On
866 activation, rebuild all Things' FOVs, and the player's map memory.
868 # In original version, map existence was also tested (unnecessarily?).
869 val = integer_test(worldactive_string, 0, 1)
871 if 0 != world_db["WORLD_ACTIVE"]:
875 print("World already active.")
876 elif 0 == world_db["WORLD_ACTIVE"]:
878 for ThingAction in world_db["ThingActions"]:
879 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
882 player_exists = False
883 for Thing in world_db["Things"]:
887 if wait_exists and player_exists:
888 for id in world_db["Things"]:
889 if world_db["Things"][id]["T_LIFEPOINTS"]:
890 build_fov_map(world_db["Things"][id])
892 update_map_memory(world_db["Things"][id])
893 world_db["WORLD_ACTIVE"] = 1
896 def test_for_id_maker(object, category):
897 """Return decorator testing for object having "id" attribute."""
900 if hasattr(object, "id"):
903 print("Ignoring: No " + category +
904 " defined to manipulate yet.")
909 def command_tid(id_string):
910 """Set ID of Thing to manipulate. ID unused? Create new one.
912 Default new Thing's type to the first available ThingType, others: zero.
914 id = id_setter(id_string, "Things", command_tid)
916 if world_db["ThingTypes"] == {}:
917 print("Ignoring: No ThingType to settle new Thing in.")
919 type = list(world_db["ThingTypes"].keys())[0]
920 world_db["Things"][id] = new_Thing(type)
923 test_Thing_id = test_for_id_maker(command_tid, "Thing")
927 def command_tcommand(str_int):
928 """Set T_COMMAND of selected Thing."""
929 val = integer_test(str_int, 0, 255)
931 if 0 == val or val in world_db["ThingActions"]:
932 world_db["Things"][command_tid.id]["T_COMMAND"] = val
934 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
938 def command_ttype(str_int):
939 """Set T_TYPE of selected Thing."""
940 val = integer_test(str_int, 0, 255)
942 if val in world_db["ThingTypes"]:
943 world_db["Things"][command_tid.id]["T_TYPE"] = val
945 print("Ignoring: ThingType ID belongs to no known ThingType.")
949 def command_tcarries(str_int):
950 """Append int(str_int) to T_CARRIES of selected Thing.
952 The ID int(str_int) must not be of the selected Thing, and must belong to a
953 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
955 val = integer_test(str_int, 0, 255)
957 if val == command_tid.id:
958 print("Ignoring: Thing cannot carry itself.")
959 elif val in world_db["Things"] \
960 and not world_db["Things"][val]["carried"]:
961 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
962 world_db["Things"][val]["carried"] = True
964 print("Ignoring: Thing not available for carrying.")
965 # Note that the whole carrying structure is different from the C version:
966 # Carried-ness is marked by a "carried" flag, not by Things containing
971 def command_tmemthing(str_t, str_y, str_x):
972 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
974 The type must fit to an existing ThingType, and the position into the map.
976 type = integer_test(str_t, 0, 255)
977 posy = integer_test(str_y, 0, 255)
978 posx = integer_test(str_x, 0, 255)
979 if None != type and None != posy and None != posx:
980 if type not in world_db["ThingTypes"] \
981 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
982 print("Ignoring: Illegal value for thing type or position.")
984 memthing = (type, posy, posx)
985 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
988 def setter_map(maptype):
989 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
991 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
994 def helper(str_int, mapline):
995 val = integer_test(str_int, 0, 255)
997 if val >= world_db["MAP_LENGTH"]:
998 print("Illegal value for map line number.")
999 elif len(mapline) != world_db["MAP_LENGTH"]:
1000 print("Map line length is unequal map width.")
1002 length = world_db["MAP_LENGTH"]
1004 if not world_db["Things"][command_tid.id][maptype]:
1005 map = bytearray(b' ' * (length ** 2))
1007 map = world_db["Things"][command_tid.id][maptype]
1008 map[val * length:(val * length) + length] = mapline.encode()
1009 world_db["Things"][command_tid.id][maptype] = map
1013 def setter_tpos(axis):
1014 """Generate setter for T_POSX or T_POSY of selected Thing.
1016 If world is active, rebuilds animate things' fovmap, player's memory map.
1019 def helper(str_int):
1020 val = integer_test(str_int, 0, 255)
1022 if val < world_db["MAP_LENGTH"]:
1023 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1024 if world_db["WORLD_ACTIVE"] \
1025 and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1026 build_fov_map(world_db["Things"][command_tid.id])
1027 if 0 == command_tid.id:
1028 update_map_memory(world_db["Things"][command_tid.id])
1030 print("Ignoring: Position is outside of map.")
1034 def command_ttid(id_string):
1035 """Set ID of ThingType to manipulate. ID unused? Create new one.
1037 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1039 id = id_setter(id_string, "ThingTypes", command_ttid)
1041 world_db["ThingTypes"][id] = {
1042 "TT_NAME": "(none)",
1045 "TT_PROLIFERATE": 0,
1046 "TT_START_NUMBER": 0,
1052 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1056 def command_ttname(name):
1057 """Set TT_NAME of selected ThingType."""
1058 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1062 def command_ttsymbol(char):
1063 """Set TT_SYMBOL of selected ThingType. """
1065 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1067 print("Ignoring: Argument must be single character.")
1071 def command_ttcorpseid(str_int):
1072 """Set TT_CORPSE_ID of selected ThingType."""
1073 val = integer_test(str_int, 0, 255)
1075 if val in world_db["ThingTypes"]:
1076 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1078 print("Ignoring: Corpse ID belongs to no known ThignType.")
1081 def command_taid(id_string):
1082 """Set ID of ThingAction to manipulate. ID unused? Create new one.
1084 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1086 id = id_setter(id_string, "ThingActions", command_taid, True)
1088 world_db["ThingActions"][id] = {
1094 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1097 @test_ThingAction_id
1098 def command_taname(name):
1099 """Set TA_NAME of selected ThingAction.
1101 The name must match a valid thing action function. If after the name
1102 setting no ThingAction with name "wait" remains, call set_world_inactive().
1104 if name == "wait" or name == "move" or name == "use" or name == "drop" \
1105 or name == "pick_up":
1106 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1107 if 1 == world_db["WORLD_ACTIVE"]:
1108 wait_defined = False
1109 for id in world_db["ThingActions"]:
1110 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1113 if not wait_defined:
1114 set_world_inactive()
1116 print("Ignoring: Invalid action name.")
1117 # In contrast to the original,naming won't map a function to a ThingAction.
1120 """Commands database.
1122 Map command start tokens to ([0]) number of expected command arguments, ([1])
1123 the command's meta-ness (i.e. is it to be written to the record file, is it to
1124 be ignored in replay mode if read from server input file), and ([2]) a function
1128 "QUIT": (0, True, command_quit),
1129 "PING": (0, True, command_ping),
1130 "THINGS_HERE": (2, True, command_thingshere),
1131 "MAKE_WORLD": (1, False, command_makeworld),
1132 "SEED_MAP": (1, False, command_seedmap),
1133 "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1134 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1135 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1136 "MAP_LENGTH": (1, False, command_maplength),
1137 "WORLD_ACTIVE": (1, False, command_worldactive),
1138 "TA_ID": (1, False, command_taid),
1139 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1140 "TA_NAME": (1, False, command_taname),
1141 "TT_ID": (1, False, command_ttid),
1142 "TT_NAME": (1, False, command_ttname),
1143 "TT_SYMBOL": (1, False, command_ttsymbol),
1144 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1145 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1147 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1149 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1151 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1152 "T_ID": (1, False, command_tid),
1153 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1154 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1155 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1156 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1157 "T_COMMAND": (1, False, command_tcommand),
1158 "T_TYPE": (1, False, command_ttype),
1159 "T_CARRIES": (1, False, command_tcarries),
1160 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1161 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1162 "T_MEMTHING": (3, False, command_tmemthing),
1163 "T_POSY": (1, False, setter_tpos("Y")),
1164 "T_POSX": (1, False, setter_tpos("X")),
1165 "wait": (0, False, play_commander("wait")),
1166 "move": (1, False, play_commander("move")),
1167 "pick_up": (0, False, play_commander("pick_up")),
1168 "drop": (1, False, play_commander("drop", True)),
1169 "use": (1, False, play_commander("use", True)),
1173 """World state database. With sane default values. (Randomness is in rand.)"""
1186 """File IO database."""
1188 "path_save": "save",
1189 "path_record": "record",
1190 "path_worldconf": "confserver/world",
1191 "path_server": "server/",
1192 "path_in": "server/in",
1193 "path_out": "server/out",
1194 "path_worldstate": "server/worldstate",
1195 "tmp_suffix": "_tmp",
1196 "kicked_by_rival": False,
1197 "worldstate_updateable": False
1202 libpr = prep_library()
1203 rand = RandomnessIO()
1204 opts = parse_command_line_arguments()
1206 if None != opts.replay:
1210 except SystemExit as exit:
1211 print("ABORTING: " + exit.args[0])
1213 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")