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
39 libpr.result_y.restype = ctypes.c_uint8
40 libpr.result_x.restype = ctypes.c_uint8
41 libpr.set_maplength(world_db["MAP_LENGTH"])
45 def strong_write(file, string):
46 """Apply write(string), flush(), and os.fsync() to file."""
52 def setup_server_io():
53 """Fill IO files DB with proper file( path)s. Write process IO test string.
55 Ensure IO files directory at server/. Remove any old input file if found.
56 Set up new input file for reading, and new output file for writing. Start
57 output file with process hash line of format PID + " " + floated UNIX time
58 (io_db["teststring"]). Raise SystemExit if file is found at path of either
59 record or save file plus io_db["tmp_suffix"].
61 def detect_atomic_leftover(path, tmp_suffix):
62 path_tmp = path + tmp_suffix
63 msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
64 "aborted previous attempt to write '" + path + "'. Aborting " \
65 "until matter is resolved by removing it from its current path."
66 if os.access(path_tmp, os.F_OK):
68 io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
69 os.makedirs(io_db["path_server"], exist_ok=True)
70 io_db["file_out"] = open(io_db["path_out"], "w")
71 strong_write(io_db["file_out"], io_db["teststring"] + "\n")
72 if os.access(io_db["path_in"], os.F_OK):
73 os.remove(io_db["path_in"])
74 io_db["file_in"] = open(io_db["path_in"], "w")
75 io_db["file_in"].close()
76 io_db["file_in"] = open(io_db["path_in"], "r")
77 detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
78 detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
81 def cleanup_server_io():
82 """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
83 def helper(file_key, path_key):
85 io_db[file_key].close()
86 if not io_db["kicked_by_rival"] \
87 and os.access(io_db[path_key], os.F_OK):
88 os.remove(io_db[path_key])
89 helper("file_out", "path_out")
90 helper("file_in", "path_in")
91 helper("file_worldstate", "path_worldstate")
92 if "file_record" in io_db:
93 io_db["file_record"].close()
96 def obey(command, prefix, replay=False, do_record=False):
97 """Call function from commands_db mapped to command's first token.
99 Tokenize command string with shlex.split(comments=True). If replay is set,
100 a non-meta command from the commands_db merely triggers obey() on the next
101 command from the records file. If not, non-meta commands set
102 io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
103 do_record is set, are recorded via record(), and save_world() is called.
104 The prefix string is inserted into the server's input message between its
105 beginning 'input ' & ':'. All activity is preceded by a server_test() call.
108 print("input " + prefix + ": " + command)
110 tokens = shlex.split(command, comments=True)
111 except ValueError as err:
112 print("Can't tokenize command string: " + str(err) + ".")
114 if len(tokens) > 0 and tokens[0] in commands_db \
115 and len(tokens) == commands_db[tokens[0]][0] + 1:
116 if commands_db[tokens[0]][1]:
117 commands_db[tokens[0]][2](*tokens[1:])
119 print("Due to replay mode, reading command as 'go on in record'.")
120 line = io_db["file_record"].readline()
122 obey(line.rstrip(), io_db["file_record"].prefix
123 + str(io_db["file_record"].line_n))
124 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
126 print("Reached end of record file.")
128 commands_db[tokens[0]][2](*tokens[1:])
132 io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
133 elif 0 != len(tokens):
134 print("Invalid command/argument, or bad number of tokens.")
137 def atomic_write(path, text, do_append=False):
138 """Atomic write of text to file at path, appended if do_append is set."""
139 path_tmp = path + io_db["tmp_suffix"]
143 if os.access(path, os.F_OK):
144 shutil.copyfile(path, path_tmp)
145 file = open(path_tmp, mode)
146 strong_write(file, text)
148 if os.access(path, os.F_OK):
150 os.rename(path_tmp, path)
154 """Append command string plus newline to record file. (Atomic.)"""
155 # This misses some optimizations from the original record(), namely only
156 # finishing the atomic write with expensive flush() and fsync() every 15
157 # seconds unless explicitely forced. Implement as needed.
158 atomic_write(io_db["path_record"], command + "\n", do_append=True)
162 """Save all commands needed to reconstruct current world state."""
163 # TODO: Misses same optimizations as record() from the original record().
166 string = string.replace("\u005C", '\u005C\u005C')
167 return '"' + string.replace('"', '\u005C"') + '"'
172 if world_db["Things"][id][key]:
173 map = world_db["Things"][id][key]
174 length = world_db["MAP_LENGTH"]
175 for i in range(length):
176 line = map[i * length:(i * length) + length].decode()
177 string = string + key + " " + str(i) + " " + quote(line) \
184 for memthing in world_db["Things"][id]["T_MEMTHING"]:
185 string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
186 str(memthing[1]) + " " + str(memthing[2]) + "\n"
189 def helper(category, id_string, special_keys={}):
191 for id in world_db[category]:
192 string = string + id_string + " " + str(id) + "\n"
193 for key in world_db[category][id]:
194 if not key in special_keys:
195 x = world_db[category][id][key]
196 argument = quote(x) if str == type(x) else str(x)
197 string = string + key + " " + argument + "\n"
198 elif special_keys[key]:
199 string = string + special_keys[key](id)
204 if dict != type(world_db[key]) and key != "MAP":
205 string = string + key + " " + str(world_db[key]) + "\n"
206 string = string + helper("ThingActions", "TA_ID")
207 string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
208 for id in world_db["ThingTypes"]:
209 string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
210 str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
211 string = string + helper("Things", "T_ID",
212 {"T_CARRIES": False, "carried": False,
213 "T_MEMMAP": mapsetter("T_MEMMAP"),
214 "T_MEMTHING": memthing, "fovmap": False,
215 "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
216 for id in world_db["Things"]:
217 if [] != world_db["Things"][id]["T_CARRIES"]:
218 string = string + "T_ID " + str(id) + "\n"
219 for carried_id in world_db["Things"][id]["T_CARRIES"]:
220 string = string + "T_CARRIES " + str(carried_id) + "\n"
221 string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
222 "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
223 atomic_write(io_db["path_save"], string)
226 def obey_lines_in_file(path, name, do_record=False):
227 """Call obey() on each line of path's file, use name in input prefix."""
228 file = open(path, "r")
230 for line in file.readlines():
231 obey(line.rstrip(), name + "file line " + str(line_n),
237 def parse_command_line_arguments():
238 """Return settings values read from command line arguments."""
239 parser = argparse.ArgumentParser()
240 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
242 opts, unknown = parser.parse_known_args()
247 """Ensure valid server out file belonging to current process.
249 This is done by comparing io_db["teststring"] to what's found at the start
250 of the current file at io_db["path_out"]. On failure, set
251 io_db["kicked_by_rival"] and raise SystemExit.
253 if not os.access(io_db["path_out"], os.F_OK):
254 raise SystemExit("Server output file has disappeared.")
255 file = open(io_db["path_out"], "r")
256 test = file.readline().rstrip("\n")
258 if test != io_db["teststring"]:
259 io_db["kicked_by_rival"] = True
260 msg = "Server test string in server output file does not match. This" \
261 " indicates that the current server process has been " \
262 "superseded by another one."
263 raise SystemExit(msg)
267 """Return next newline-delimited command from server in file.
269 Keep building return string until a newline is encountered. Pause between
270 unsuccessful reads, and after too much waiting, run server_test().
277 add = io_db["file_in"].readline()
279 command = command + add
280 if len(command) > 0 and "\n" == command[-1]:
281 command = command[:-1]
284 time.sleep(wait_on_fail)
285 if now + max_wait < time.time():
291 def try_worldstate_update():
292 """Write worldstate file if io_db["worldstate_updateable"] is set."""
293 if io_db["worldstate_updateable"]:
295 def draw_visible_Things(map, run):
296 for id in world_db["Things"]:
297 type = world_db["Things"][id]["T_TYPE"]
298 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
299 alive = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]
300 if (0 == run and not consumable and not alive) \
301 or (1 == run and consumable and not alive) \
302 or (2 == run and alive):
303 y = world_db["Things"][id]["T_POSY"]
304 x = world_db["Things"][id]["T_POSX"]
305 fovflag = world_db["Things"][0]["fovmap"][(y * length) + x]
306 if 'v' == chr(fovflag):
307 c = world_db["ThingTypes"][type]["TT_SYMBOL"]
308 map[(y * length) + x] = ord(c)
310 def write_map(string, map):
311 for i in range(length):
312 line = map[i * length:(i * length) + length].decode()
313 string = string + line + "\n"
317 if [] == world_db["Things"][0]["T_CARRIES"]:
318 inventory = "(none)\n"
320 for id in world_db["Things"][0]["T_CARRIES"]:
321 type_id = world_db["Things"][id]["T_TYPE"]
322 name = world_db["ThingTypes"][type_id]["TT_NAME"]
323 inventory = inventory + name + "\n"
324 string = str(world_db["TURN"]) + "\n" + \
325 str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
326 str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
327 inventory + "%\n" + \
328 str(world_db["Things"][0]["T_POSY"]) + "\n" + \
329 str(world_db["Things"][0]["T_POSX"]) + "\n" + \
330 str(world_db["MAP_LENGTH"]) + "\n"
331 length = world_db["MAP_LENGTH"]
332 fov = bytearray(b' ' * (length ** 2))
333 for pos in range(length ** 2):
334 if 'v' == chr(world_db["Things"][0]["fovmap"][pos]):
335 fov[pos] = world_db["MAP"][pos]
337 draw_visible_Things(fov, i)
338 string = write_map(string, fov)
339 mem = world_db["Things"][0]["T_MEMMAP"][:]
341 for memthing in world_db["Things"][0]["T_MEMTHING"]:
342 type = world_db["Things"][memthing[0]]["T_TYPE"]
343 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
344 if (i == 0 and not consumable) or (i == 1 and consumable):
345 c = world_db["ThingTypes"][type]["TT_SYMBOL"]
346 mem[(memthing[1] * length) + memthing[2]] = ord(c)
347 string = write_map(string, mem)
348 atomic_write(io_db["path_worldstate"], string)
349 strong_write(io_db["file_out"], "WORLD_UPDATED\n")
350 io_db["worldstate_updateable"] = False
354 """Replay game from record file.
356 Use opts.replay as breakpoint turn to which to replay automatically before
357 switching to manual input by non-meta commands in server input file
358 triggering further reads of record file. Ensure opts.replay is at least 1.
359 Run try_worldstate_update() before each interactive obey()/read_command().
363 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
364 " (if so late a turn is to be found).")
365 if not os.access(io_db["path_record"], os.F_OK):
366 raise SystemExit("No record file found to replay.")
367 io_db["file_record"] = open(io_db["path_record"], "r")
368 io_db["file_record"].prefix = "record file line "
369 io_db["file_record"].line_n = 1
370 while world_db["TURN"] < opts.replay:
371 line = io_db["file_record"].readline()
374 obey(line.rstrip(), io_db["file_record"].prefix
375 + str(io_db["file_record"].line_n))
376 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
378 try_worldstate_update()
379 obey(read_command(), "in file", replay=True)
383 """Play game by server input file commands. Before, load save file found.
385 If no save file is found, a new world is generated from the commands in the
386 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
387 command and all that follow via the server input file. Run
388 try_worldstate_update() before each interactive obey()/read_command().
390 if os.access(io_db["path_save"], os.F_OK):
391 obey_lines_in_file(io_db["path_save"], "save")
393 if not os.access(io_db["path_worldconf"], os.F_OK):
394 msg = "No world config file from which to start a new world."
395 raise SystemExit(msg)
396 obey_lines_in_file(io_db["path_worldconf"], "world config ",
398 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
400 try_worldstate_update()
401 obey(read_command(), "in file", do_record=True)
405 """(Re-)make island map.
407 Let "~" represent water, "." land, "X" trees: Build island shape randomly,
408 start with one land cell in the middle, then go into cycle of repeatedly
409 selecting a random sea cell and transforming it into land if it is neighbor
410 to land. The cycle ends when a land cell is due to be created at the map's
411 border. Then put some trees on the map (TODO: more precise algorithm desc).
413 def is_neighbor(coordinates, type):
416 length = world_db["MAP_LENGTH"]
418 diag_west = x + (ind > 0)
419 diag_east = x + (ind < (length - 1))
420 pos = (y * length) + x
421 if (y > 0 and diag_east
422 and type == chr(world_db["MAP"][pos - length + ind])) \
424 and type == chr(world_db["MAP"][pos + 1])) \
425 or (y < (length - 1) and diag_east
426 and type == chr(world_db["MAP"][pos + length + ind])) \
427 or (y > 0 and diag_west
428 and type == chr(world_db["MAP"][pos - length - (not ind)])) \
430 and type == chr(world_db["MAP"][pos - 1])) \
431 or (y < (length - 1) and diag_west
432 and type == chr(world_db["MAP"][pos + length - (not ind)])):
435 store_seed = rand.seed
436 world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
437 length = world_db["MAP_LENGTH"]
438 add_half_width = (not (length % 2)) * int(length / 2)
439 world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
441 y = rand.next() % length
442 x = rand.next() % length
443 pos = (y * length) + x
444 if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
445 if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
447 world_db["MAP"][pos] = ord(".")
448 n_trees = int((length ** 2) / 16)
450 while (i_trees <= n_trees):
451 single_allowed = rand.next() % 32
452 y = rand.next() % length
453 x = rand.next() % length
454 pos = (y * length) + x
455 if "." == chr(world_db["MAP"][pos]) \
456 and ((not single_allowed) or is_neighbor((y, x), "X")):
457 world_db["MAP"][pos] = ord("X")
459 rand.seed = store_seed
460 # This all-too-precise replica of the original C code misses iter_limit().
463 def update_map_memory(t):
464 """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
465 if not t["T_MEMMAP"]:
466 t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
467 if not t["T_MEMDEPTHMAP"]:
468 t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
469 for pos in range(world_db["MAP_LENGTH"] ** 2):
470 if "v" == chr(t["fovmap"][pos]):
471 t["T_MEMDEPTHMAP"][pos] = ord("0")
472 if " " == chr(t["T_MEMMAP"][pos]):
473 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
475 # TODO: Aging of MEMDEPTHMAP.
476 for memthing in t["T_MEMTHING"]:
477 y = world_db["Things"][memthing[0]]["T_POSY"]
478 x = world_db["Things"][memthing[0]]["T_POSX"]
479 if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
480 t["T_MEMTHING"].remove(memthing)
481 for id in world_db["Things"]:
482 type = world_db["Things"][id]["T_TYPE"]
483 if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
484 y = world_db["Things"][id]["T_POSY"]
485 x = world_db["Things"][id]["T_POSY"]
486 if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
487 t["T_MEMTHING"].append((type, y, x))
490 def set_world_inactive():
491 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
493 if os.access(io_db["path_worldstate"], os.F_OK):
494 os.remove(io_db["path_worldstate"])
495 world_db["WORLD_ACTIVE"] = 0
498 def integer_test(val_string, min, max):
499 """Return val_string if possible integer >= min and <= max, else None."""
501 val = int(val_string)
502 if val < min or val > max:
506 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
511 def setter(category, key, min, max):
512 """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
515 val = integer_test(val_string, min, max)
519 if category == "Thing":
520 id_store = command_tid
521 decorator = test_Thing_id
522 elif category == "ThingType":
523 id_store = command_ttid
524 decorator = test_ThingType_id
525 elif category == "ThingAction":
526 id_store = command_taid
527 decorator = test_ThingAction_id
531 val = integer_test(val_string, min, max)
533 world_db[category + "s"][id_store.id][key] = val
537 def build_fov_map(t):
538 """Build Thing's FOV map."""
539 t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
540 # DUMMY so far. Just builds an all-visible map.
543 def decrement_lifepoints(t):
544 """Decrement t's lifepoints by 1, and if to zero, corpse it.
546 If t is the player avatar, only blank its fovmap, so that the client may
547 still display memory data. On non-player things, erase fovmap and memory.
549 t["T_LIFEPOINTS"] -= 1
550 if 0 == t["T_LIFEPOINTS"]:
551 t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
552 if world_db["Things"][0] == t:
553 t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
554 strong_write(io_db["file_out"], "LOG You die.\n")
557 t["T_MEMMAP"] = False
558 t["T_MEMDEPTHMAP"] = False
560 strong_write(io_db["file_out"], "LOG It dies.\n")
564 """Make t do nothing (but loudly, if player avatar)."""
565 if t == world_db["Things"][0]:
566 strong_write(io_db["file_out"], "LOG You wait.\n")
570 """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
571 dir_c = t["T_ARGUMENT"].encode("ascii")[0]
572 legal_move = libpr.mv_yx_in_dir_legal_wrap(dir_c, t["T_POSY"], t["T_POSX"])
575 raise SystemExit("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
576 elif 1 == legal_move:
577 pos = (libpr.result_y() * world_db["MAP_LENGTH"]) + libpr.result_x()
578 passable = "." == chr(world_db["MAP"][pos])
579 hitted = [id for id in world_db["Things"]
580 if world_db["Things"][id] != t
581 if world_db["Things"][id]["T_LIFEPOINTS"]
582 if world_db["Things"][id]["T_POSY"] == libpr.result_y()
583 if world_db["Things"][id]["T_POSX"] == libpr.result_x()]
586 decrement_lifepoints(world_db["Things"][hitted])
587 hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
588 hitter = "You" if t == world_db["Things"][0] else hitter_name
589 hitted_type = world_db["Things"][hitted]["T_TYPE"]
590 hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
591 hitted = "you" if hitted == world_db["Things"][0] else hitted_name
592 verb = " wound " if hitter == "You" else " wounds "
593 strong_write(io_db["file_out"], "LOG " + hitter + verb + hitted + \
596 dir = [dir for dir in directions_db
597 if directions_db[dir] == t["T_ARGUMENT"]][0]
599 t["T_POSY"] = libpr.result_y()
600 t["T_POSX"] = libpr.result_x()
601 for id in t["T_CARRIES"]:
602 world_db["Things"][id]["T_POSY"] = libpr.result_y()
603 world_db["Things"][id]["T_POSX"] = libpr.result_x()
605 strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
607 strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
610 def actor_pick_up(t):
611 """Make t pick up (topmost?) Thing from ground into inventory."""
612 # Topmostness is actually not defined so far.
613 ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
614 if not world_db["Things"][id]["carried"]
615 if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
616 if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
618 world_db["Things"][ids[0]]["carried"] = True
619 t["T_CARRIES"].append(ids[0])
620 if t == world_db["Things"][0]:
621 strong_write(io_db["file_out"], "LOG You pick up an object.\n")
622 elif t == world_db["Things"][0]:
623 err = "You try to pick up an object, but there is none."
624 strong_write(io_db["file_out"], "LOG " + err + "\n")
628 """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
629 # TODO: Handle case where T_ARGUMENT matches nothing.
630 if len(t["T_CARRIES"]):
631 id = t["T_CARRIES"][t["T_ARGUMENT"]]
632 t["T_CARRIES"].remove(id)
633 world_db["Things"][id]["carried"] = False
634 if t == world_db["Things"][0]:
635 strong_write(io_db["file_out"], "LOG You drop an object.\n")
636 elif t == world_db["Things"][0]:
637 err = "You try to drop an object, but you own none."
638 strong_write(io_db["file_out"], "LOG " + err + "\n")
642 """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
643 # Original wrongly featured lifepoints increase through consumable!
644 # TODO: Handle case where T_ARGUMENT matches nothing.
645 if len(t["T_CARRIES"]):
646 id = t["T_CARRIES"][t["T_ARGUMENT"]]
647 type = world_db["Things"][id]["T_TYPE"]
648 if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
649 t["T_CARRIES"].remove(id)
650 del world_db["Things"][id]
651 t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
652 strong_write(io_db["file_out"], "LOG You consume this object.\n")
654 strong_write(io_db["file_out"], "LOG You try to use this object," +
657 strong_write(io_db["file_out"], "LOG You try to use an object, but " +
662 """Run game world and its inhabitants until new player input expected."""
665 while world_db["Things"][0]["T_LIFEPOINTS"]:
666 for id in [id for id in world_db["Things"]
667 if world_db["Things"][id]["T_LIFEPOINTS"]]:
668 Thing = world_db["Things"][id]
669 if Thing["T_LIFEPOINTS"]:
670 if not Thing["T_COMMAND"]:
671 update_map_memory(Thing)
676 Thing["T_COMMAND"] = 1
678 Thing["T_PROGRESS"] += 1
679 taid = [a for a in world_db["ThingActions"]
680 if a == Thing["T_COMMAND"]][0]
681 ThingAction = world_db["ThingActions"][taid]
682 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
683 eval("actor_" + ThingAction["TA_NAME"])(Thing)
684 Thing["T_COMMAND"] = 0
685 Thing["T_PROGRESS"] = 0
687 # DUMMY: thingproliferation
690 world_db["TURN"] += 1
693 def new_Thing(type, pos=(0,0)):
694 """Return Thing of type T_TYPE, with fovmap if alive and world active."""
696 "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
708 "T_MEMDEPTHMAP": False,
711 if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
716 def id_setter(id, category, id_store=False, start_at_1=False):
717 """Set ID of object of category to manipulate ID unused? Create new one.
719 The ID is stored as id_store.id (if id_store is set). If the integer of the
720 input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
721 32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
722 >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
723 new object is created, otherwise the new object's ID.
725 min = 0 if start_at_1 else -32768
726 max = 255 if start_at_1 else 32767
728 id = integer_test(id, min, max)
730 if id in world_db[category]:
735 if (start_at_1 and 0 == id) \
736 or ((not start_at_1) and (id < 0 or id > 255)):
740 if id not in world_db[category]:
744 "No unused ID available to add to ID list.")
752 """Send PONG line to server output file."""
753 strong_write(io_db["file_out"], "PONG\n")
757 """Abort server process."""
758 raise SystemExit("received QUIT command")
761 def command_thingshere(str_y, str_x):
762 """Write to out file list of Things known to player at coordinate y, x."""
763 def write_thing_if_here():
764 if y == world_db["Things"][id]["T_POSY"] \
765 and x == world_db["Things"][id]["T_POSX"] \
766 and not world_db["Things"][id]["carried"]:
767 type = world_db["Things"][id]["T_TYPE"]
768 name = world_db["ThingTypes"][type]["TT_NAME"]
769 strong_write(io_db["file_out"], name + "\n")
770 if world_db["WORLD_ACTIVE"]:
771 y = integer_test(str_y, 0, 255)
772 x = integer_test(str_x, 0, 255)
773 length = world_db["MAP_LENGTH"]
774 if None != y and None != x and y < length and x < length:
775 pos = (y * world_db["MAP_LENGTH"]) + x
776 strong_write(io_db["file_out"], "THINGS_HERE START\n")
777 if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
778 for id in world_db["Things"]:
779 write_thing_if_here()
781 for id in world_db["Things"]["T_MEMTHING"]:
782 write_thing_if_here()
783 strong_write(io_db["file_out"], "THINGS_HERE END\n")
785 print("Ignoring: Invalid map coordinates.")
787 print("Ignoring: Command only works on existing worlds.")
790 def play_commander(action, args=False):
791 """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
793 T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
797 id = [x for x in world_db["ThingActions"]
798 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
799 world_db["Things"][0]["T_COMMAND"] = id
802 def set_command_and_argument_int(str_arg):
803 val = integer_test(str_arg, 0, 255)
805 world_db["Things"][0]["T_ARGUMENT"] = val
808 def set_command_and_argument_movestring(str_arg):
809 if str_arg in directions_db:
810 world_db["Things"][0]["T_ARGUMENT"] = directions_db[str_arg]
813 print("Ignoring: Argument must be valid direction string.")
816 return set_command_and_argument_movestring
818 return set_command_and_argument_int
823 def command_seedrandomness(seed_string):
824 """Set rand seed to int(seed_string)."""
825 val = integer_test(seed_string, 0, 4294967295)
830 def command_seedmap(seed_string):
831 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
832 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
836 def command_makeworld(seed_string):
837 """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
839 Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
840 "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
841 TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
842 and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
843 according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
844 of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
845 other. Init player's memory map. Write "NEW_WORLD" line to out file.
851 err = "Space to put thing on too hard to find. Map too small?"
853 y = rand.next() % world_db["MAP_LENGTH"]
854 x = rand.next() % world_db["MAP_LENGTH"]
855 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
859 raise SystemExit(err)
860 # Replica of C code, wrongly ignores animatedness of new Thing.
861 pos_clear = (0 == len([id for id in world_db["Things"]
862 if world_db["Things"][id]["T_LIFEPOINTS"]
863 if world_db["Things"][id]["T_POSY"] == y
864 if world_db["Things"][id]["T_POSX"] == x]))
869 val = integer_test(seed_string, 0, 4294967295)
873 world_db["SEED_MAP"] = val
874 player_will_be_generated = False
875 playertype = world_db["PLAYER_TYPE"]
876 for ThingType in world_db["ThingTypes"]:
877 if playertype == ThingType:
878 if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
879 player_will_be_generated = True
881 if not player_will_be_generated:
882 print("Ignoring beyond SEED_MAP: " +
883 "No player type with start number >0 defined.")
886 for ThingAction in world_db["ThingActions"]:
887 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
890 print("Ignoring beyond SEED_MAP: " +
891 "No thing action with name 'wait' defined.")
893 world_db["Things"] = {}
895 world_db["WORLD_ACTIVE"] = 1
897 for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
898 id = id_setter(-1, "Things")
899 world_db["Things"][id] = new_Thing(playertype, free_pos())
900 update_map_memory(world_db["Things"][0])
901 for type in world_db["ThingTypes"]:
902 for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
903 if type != playertype:
904 id = id_setter(-1, "Things")
905 world_db["Things"][id] = new_Thing(type, free_pos())
906 strong_write(io_db["file_out"], "NEW_WORLD\n")
909 def command_maplength(maplength_string):
910 """Redefine map length. Invalidate map, therefore lose all things on it."""
911 val = integer_test(maplength_string, 1, 256)
913 world_db["MAP_LENGTH"] = val
915 world_db["Things"] = {}
916 libpr.set_maplength(val)
919 def command_worldactive(worldactive_string):
920 """Toggle world_db["WORLD_ACTIVE"] if possible.
922 An active world can always be set inactive. An inactive world can only be
923 set active with a "wait" ThingAction, and a player Thing (of ID 0). On
924 activation, rebuild all Things' FOVs, and the player's map memory.
926 # In original version, map existence was also tested (unnecessarily?).
927 val = integer_test(worldactive_string, 0, 1)
929 if 0 != world_db["WORLD_ACTIVE"]:
933 print("World already active.")
934 elif 0 == world_db["WORLD_ACTIVE"]:
936 for ThingAction in world_db["ThingActions"]:
937 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
940 player_exists = False
941 for Thing in world_db["Things"]:
945 if wait_exists and player_exists:
946 for id in world_db["Things"]:
947 if world_db["Things"][id]["T_LIFEPOINTS"]:
948 build_fov_map(world_db["Things"][id])
950 update_map_memory(world_db["Things"][id])
951 world_db["WORLD_ACTIVE"] = 1
954 def test_for_id_maker(object, category):
955 """Return decorator testing for object having "id" attribute."""
958 if hasattr(object, "id"):
961 print("Ignoring: No " + category +
962 " defined to manipulate yet.")
967 def command_tid(id_string):
968 """Set ID of Thing to manipulate. ID unused? Create new one.
970 Default new Thing's type to the first available ThingType, others: zero.
972 id = id_setter(id_string, "Things", command_tid)
974 if world_db["ThingTypes"] == {}:
975 print("Ignoring: No ThingType to settle new Thing in.")
977 type = list(world_db["ThingTypes"].keys())[0]
978 world_db["Things"][id] = new_Thing(type)
981 test_Thing_id = test_for_id_maker(command_tid, "Thing")
985 def command_tcommand(str_int):
986 """Set T_COMMAND of selected Thing."""
987 val = integer_test(str_int, 0, 255)
989 if 0 == val or val in world_db["ThingActions"]:
990 world_db["Things"][command_tid.id]["T_COMMAND"] = val
992 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
996 def command_ttype(str_int):
997 """Set T_TYPE of selected Thing."""
998 val = integer_test(str_int, 0, 255)
1000 if val in world_db["ThingTypes"]:
1001 world_db["Things"][command_tid.id]["T_TYPE"] = val
1003 print("Ignoring: ThingType ID belongs to no known ThingType.")
1007 def command_tcarries(str_int):
1008 """Append int(str_int) to T_CARRIES of selected Thing.
1010 The ID int(str_int) must not be of the selected Thing, and must belong to a
1011 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1013 val = integer_test(str_int, 0, 255)
1015 if val == command_tid.id:
1016 print("Ignoring: Thing cannot carry itself.")
1017 elif val in world_db["Things"] \
1018 and not world_db["Things"][val]["carried"]:
1019 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1020 world_db["Things"][val]["carried"] = True
1022 print("Ignoring: Thing not available for carrying.")
1023 # Note that the whole carrying structure is different from the C version:
1024 # Carried-ness is marked by a "carried" flag, not by Things containing
1025 # Things internally.
1029 def command_tmemthing(str_t, str_y, str_x):
1030 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1032 The type must fit to an existing ThingType, and the position into the map.
1034 type = integer_test(str_t, 0, 255)
1035 posy = integer_test(str_y, 0, 255)
1036 posx = integer_test(str_x, 0, 255)
1037 if None != type and None != posy and None != posx:
1038 if type not in world_db["ThingTypes"] \
1039 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1040 print("Ignoring: Illegal value for thing type or position.")
1042 memthing = (type, posy, posx)
1043 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1046 def setter_map(maptype):
1047 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
1049 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
1052 def helper(str_int, mapline):
1053 val = integer_test(str_int, 0, 255)
1055 if val >= world_db["MAP_LENGTH"]:
1056 print("Illegal value for map line number.")
1057 elif len(mapline) != world_db["MAP_LENGTH"]:
1058 print("Map line length is unequal map width.")
1060 length = world_db["MAP_LENGTH"]
1062 if not world_db["Things"][command_tid.id][maptype]:
1063 map = bytearray(b' ' * (length ** 2))
1065 map = world_db["Things"][command_tid.id][maptype]
1066 map[val * length:(val * length) + length] = mapline.encode()
1067 world_db["Things"][command_tid.id][maptype] = map
1071 def setter_tpos(axis):
1072 """Generate setter for T_POSX or T_POSY of selected Thing.
1074 If world is active, rebuilds animate things' fovmap, player's memory map.
1077 def helper(str_int):
1078 val = integer_test(str_int, 0, 255)
1080 if val < world_db["MAP_LENGTH"]:
1081 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1082 if world_db["WORLD_ACTIVE"] \
1083 and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1084 build_fov_map(world_db["Things"][command_tid.id])
1085 if 0 == command_tid.id:
1086 update_map_memory(world_db["Things"][command_tid.id])
1088 print("Ignoring: Position is outside of map.")
1092 def command_ttid(id_string):
1093 """Set ID of ThingType to manipulate. ID unused? Create new one.
1095 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1097 id = id_setter(id_string, "ThingTypes", command_ttid)
1099 world_db["ThingTypes"][id] = {
1100 "TT_NAME": "(none)",
1103 "TT_PROLIFERATE": 0,
1104 "TT_START_NUMBER": 0,
1110 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1114 def command_ttname(name):
1115 """Set TT_NAME of selected ThingType."""
1116 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1120 def command_ttsymbol(char):
1121 """Set TT_SYMBOL of selected ThingType. """
1123 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1125 print("Ignoring: Argument must be single character.")
1129 def command_ttcorpseid(str_int):
1130 """Set TT_CORPSE_ID of selected ThingType."""
1131 val = integer_test(str_int, 0, 255)
1133 if val in world_db["ThingTypes"]:
1134 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1136 print("Ignoring: Corpse ID belongs to no known ThignType.")
1139 def command_taid(id_string):
1140 """Set ID of ThingAction to manipulate. ID unused? Create new one.
1142 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1144 id = id_setter(id_string, "ThingActions", command_taid, True)
1146 world_db["ThingActions"][id] = {
1152 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1155 @test_ThingAction_id
1156 def command_taname(name):
1157 """Set TA_NAME of selected ThingAction.
1159 The name must match a valid thing action function. If after the name
1160 setting no ThingAction with name "wait" remains, call set_world_inactive().
1162 if name == "wait" or name == "move" or name == "use" or name == "drop" \
1163 or name == "pick_up":
1164 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1165 if 1 == world_db["WORLD_ACTIVE"]:
1166 wait_defined = False
1167 for id in world_db["ThingActions"]:
1168 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1171 if not wait_defined:
1172 set_world_inactive()
1174 print("Ignoring: Invalid action name.")
1175 # In contrast to the original,naming won't map a function to a ThingAction.
1178 """Commands database.
1180 Map command start tokens to ([0]) number of expected command arguments, ([1])
1181 the command's meta-ness (i.e. is it to be written to the record file, is it to
1182 be ignored in replay mode if read from server input file), and ([2]) a function
1186 "QUIT": (0, True, command_quit),
1187 "PING": (0, True, command_ping),
1188 "THINGS_HERE": (2, True, command_thingshere),
1189 "MAKE_WORLD": (1, False, command_makeworld),
1190 "SEED_MAP": (1, False, command_seedmap),
1191 "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1192 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1193 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1194 "MAP_LENGTH": (1, False, command_maplength),
1195 "WORLD_ACTIVE": (1, False, command_worldactive),
1196 "TA_ID": (1, False, command_taid),
1197 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1198 "TA_NAME": (1, False, command_taname),
1199 "TT_ID": (1, False, command_ttid),
1200 "TT_NAME": (1, False, command_ttname),
1201 "TT_SYMBOL": (1, False, command_ttsymbol),
1202 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1203 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1205 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1207 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1209 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1210 "T_ID": (1, False, command_tid),
1211 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1212 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1213 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1214 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1215 "T_COMMAND": (1, False, command_tcommand),
1216 "T_TYPE": (1, False, command_ttype),
1217 "T_CARRIES": (1, False, command_tcarries),
1218 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1219 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1220 "T_MEMTHING": (3, False, command_tmemthing),
1221 "T_POSY": (1, False, setter_tpos("Y")),
1222 "T_POSX": (1, False, setter_tpos("X")),
1223 "wait": (0, False, play_commander("wait")),
1224 "move": (1, False, play_commander("move")),
1225 "pick_up": (0, False, play_commander("pick_up")),
1226 "drop": (1, False, play_commander("drop", True)),
1227 "use": (1, False, play_commander("use", True)),
1231 """World state database. With sane default values. (Randomness is in rand.)"""
1243 """Mapping of direction names to internal direction chars."""
1244 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1245 "west": "s", "north-west": "w", "north-east": "e"}
1247 """File IO database."""
1249 "path_save": "save",
1250 "path_record": "record",
1251 "path_worldconf": "confserver/world",
1252 "path_server": "server/",
1253 "path_in": "server/in",
1254 "path_out": "server/out",
1255 "path_worldstate": "server/worldstate",
1256 "tmp_suffix": "_tmp",
1257 "kicked_by_rival": False,
1258 "worldstate_updateable": False
1263 libpr = prep_library()
1264 rand = RandomnessIO()
1265 opts = parse_command_line_arguments()
1267 if None != opts.replay:
1271 except SystemExit as exit:
1272 print("ABORTING: " + exit.args[0])
1274 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")