3 # This file is part of PlomRogue. PlomRogue is licensed under the GPL version 3
4 # or any later version. For details on its copyright, license, and warranties,
5 # see the file NOTICE in the root directory of the PlomRogue source package.
19 """"Interface to libplomrogue's pseudo-randomness generator."""
21 def set_seed(self, seed):
22 libpr.seed_rrand(1, seed)
25 return libpr.seed_rrand(0, 0)
30 seed = property(get_seed, set_seed)
34 """Prepare ctypes library at ./libplomrogue.so"""
35 libpath = ("./libplomrogue.so")
36 if not os.access(libpath, os.F_OK):
37 raise SystemExit("No library " + libpath + ", run ./redo first?")
38 libpr = ctypes.cdll.LoadLibrary(libpath)
39 libpr.seed_rrand.restype = ctypes.c_uint32
43 def c_pointer_to_bytearray(ba):
44 """Return C char * pointer to ba."""
45 type = ctypes.c_char * len(ba)
46 return type.from_buffer(ba)
49 def strong_write(file, string):
50 """Apply write(string), then flush()."""
55 def setup_server_io():
56 """Fill IO files DB with proper file( path)s. Write process IO test string.
58 Ensure IO files directory at server/. Remove any old input file if found.
59 Set up new input file for reading, and new output file for writing. Start
60 output file with process hash line of format PID + " " + floated UNIX time
61 (io_db["teststring"]). Raise SystemExit if file is found at path of either
62 record or save file plus io_db["tmp_suffix"].
64 def detect_atomic_leftover(path, tmp_suffix):
65 path_tmp = path + tmp_suffix
66 msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
67 "aborted previous attempt to write '" + path + "'. Aborting " \
68 "until matter is resolved by removing it from its current path."
69 if os.access(path_tmp, os.F_OK):
71 io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
72 io_db["save_wait"] = 0
73 io_db["verbose"] = False
74 io_db["record_chunk"] = ""
75 os.makedirs(io_db["path_server"], exist_ok=True)
76 io_db["file_out"] = open(io_db["path_out"], "w")
77 strong_write(io_db["file_out"], io_db["teststring"] + "\n")
78 if os.access(io_db["path_in"], os.F_OK):
79 os.remove(io_db["path_in"])
80 io_db["file_in"] = open(io_db["path_in"], "w")
81 io_db["file_in"].close()
82 io_db["file_in"] = open(io_db["path_in"], "r")
83 detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
84 detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
87 def cleanup_server_io():
88 """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
89 def helper(file_key, path_key):
91 io_db[file_key].close()
92 if not io_db["kicked_by_rival"] \
93 and os.access(io_db[path_key], os.F_OK):
94 os.remove(io_db[path_key])
95 helper("file_in", "path_in")
96 helper("file_out", "path_out")
97 helper("file_worldstate", "path_worldstate")
98 if "file_record" in io_db:
99 io_db["file_record"].close()
102 def obey(command, prefix, replay=False, do_record=False):
103 """Call function from commands_db mapped to command's first token.
105 Tokenize command string with shlex.split(comments=True). If replay is set,
106 a non-meta command from the commands_db merely triggers obey() on the next
107 command from the records file. If not, non-meta commands set
108 io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
109 do_record is set, are recorded to io_db["record_chunk"], and save_world()
110 is called (and io_db["record_chunk"] written) if 15 seconds have passed
111 since the last time it was called. The prefix string is inserted into the
112 server's input message between its beginning 'input ' and ':'. All activity
113 is preceded by a server_test() call. Commands that start with a lowercase
114 letter are ignored when world_db["WORLD_ACTIVE"] is False/0.
118 print("input " + prefix + ": " + command)
120 tokens = shlex.split(command, comments=True)
121 except ValueError as err:
122 print("Can't tokenize command string: " + str(err) + ".")
124 if len(tokens) > 0 and tokens[0] in commands_db \
125 and len(tokens) == commands_db[tokens[0]][0] + 1:
126 if commands_db[tokens[0]][1]:
127 commands_db[tokens[0]][2](*tokens[1:])
128 elif tokens[0][0].islower() and not world_db["WORLD_ACTIVE"]:
129 print("Ignoring lowercase-starting commands when world inactive.")
131 print("Due to replay mode, reading command as 'go on in record'.")
132 line = io_db["file_record"].readline()
134 obey(line.rstrip(), io_db["file_record"].prefix
135 + str(io_db["file_record"].line_n))
136 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
138 print("Reached end of record file.")
140 commands_db[tokens[0]][2](*tokens[1:])
142 io_db["record_chunk"] += command + "\n"
143 if time.time() > io_db["save_wait"] + 15:
144 atomic_write(io_db["path_record"], io_db["record_chunk"],
146 if world_db["WORLD_ACTIVE"]:
148 io_db["record_chunk"] = ""
149 io_db["save_wait"] = time.time()
150 io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
151 elif 0 != len(tokens):
152 print("Invalid command/argument, or bad number of tokens.")
155 def atomic_write(path, text, do_append=False, delete=True):
156 """Atomic write of text to file at path, appended if do_append is set."""
157 path_tmp = path + io_db["tmp_suffix"]
161 if os.access(path, os.F_OK):
162 shutil.copyfile(path, path_tmp)
163 file = open(path_tmp, mode)
164 strong_write(file, text)
166 if delete and os.access(path, os.F_OK):
168 os.rename(path_tmp, path)
172 """Save all commands needed to reconstruct current world state."""
175 string = string.replace("\u005C", '\u005C\u005C')
176 return '"' + string.replace('"', '\u005C"') + '"'
181 if key == "MAP" or world_db["Things"][id][key]:
182 map = world_db["MAP"] if key == "MAP" \
183 else world_db["Things"][id][key]
184 length = world_db["MAP_LENGTH"]
185 for i in range(length):
186 line = map[i * length:(i * length) + length].decode()
187 string = string + key + " " + str(i) + " " + quote(line) \
194 for memthing in world_db["Things"][id]["T_MEMTHING"]:
195 string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
196 str(memthing[1]) + " " + str(memthing[2]) + "\n"
199 def helper(category, id_string, special_keys={}):
201 for id in world_db[category]:
202 string = string + id_string + " " + str(id) + "\n"
203 for key in world_db[category][id]:
204 if not key in special_keys:
205 x = world_db[category][id][key]
206 argument = quote(x) if str == type(x) else str(x)
207 string = string + key + " " + argument + "\n"
208 elif special_keys[key]:
209 string = string + special_keys[key](id)
214 if (dict != type(world_db[key])
215 and key != "MAP" and key != "WORLD_ACTIVE"):
216 string = string + key + " " + str(world_db[key]) + "\n"
217 string = string + mapsetter("MAP")()
218 string = string + helper("ThingActions", "TA_ID")
219 string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
220 for id in world_db["ThingTypes"]:
221 string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
222 str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
223 string = string + helper("Things", "T_ID",
224 {"T_CARRIES": False, "carried": False,
225 "T_MEMMAP": mapsetter("T_MEMMAP"),
226 "T_MEMTHING": memthing, "fovmap": False,
227 "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
228 for id in world_db["Things"]:
229 if [] != world_db["Things"][id]["T_CARRIES"]:
230 string = string + "T_ID " + str(id) + "\n"
231 for carried_id in world_db["Things"][id]["T_CARRIES"]:
232 string = string + "T_CARRIES " + str(carried_id) + "\n"
233 string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
234 "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
235 atomic_write(io_db["path_save"], string)
238 def obey_lines_in_file(path, name, do_record=False):
239 """Call obey() on each line of path's file, use name in input prefix."""
240 file = open(path, "r")
242 for line in file.readlines():
243 obey(line.rstrip(), name + "file line " + str(line_n),
249 def parse_command_line_arguments():
250 """Return settings values read from command line arguments."""
251 parser = argparse.ArgumentParser()
252 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
254 parser.add_argument('-l', nargs="?", const="save", dest='savefile',
256 parser.add_argument('-v', dest='verbose', action='store_true')
257 opts, unknown = parser.parse_known_args()
262 """Ensure valid server out file belonging to current process.
264 This is done by comparing io_db["teststring"] to what's found at the start
265 of the current file at io_db["path_out"]. On failure, set
266 io_db["kicked_by_rival"] and raise SystemExit.
268 if not os.access(io_db["path_out"], os.F_OK):
269 raise SystemExit("Server output file has disappeared.")
270 file = open(io_db["path_out"], "r")
271 test = file.readline().rstrip("\n")
273 if test != io_db["teststring"]:
274 io_db["kicked_by_rival"] = True
275 msg = "Server test string in server output file does not match. This" \
276 " indicates that the current server process has been " \
277 "superseded by another one."
278 raise SystemExit(msg)
282 """Return next newline-delimited command from server in file.
284 Keep building return string until a newline is encountered. Pause between
285 unsuccessful reads, and after too much waiting, run server_test().
287 wait_on_fail = 0.03333
292 add = io_db["file_in"].readline()
294 command = command + add
295 if len(command) > 0 and "\n" == command[-1]:
296 command = command[:-1]
299 time.sleep(wait_on_fail)
300 if now + max_wait < time.time():
306 def try_worldstate_update():
307 """Write worldstate file if io_db["worldstate_updateable"] is set."""
308 if io_db["worldstate_updateable"]:
310 def draw_visible_Things(map, run):
311 for id in world_db["Things"]:
312 type = world_db["Things"][id]["T_TYPE"]
313 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
314 alive = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]
315 if (0 == run and not consumable and not alive) \
316 or (1 == run and consumable and not alive) \
317 or (2 == run and alive):
318 y = world_db["Things"][id]["T_POSY"]
319 x = world_db["Things"][id]["T_POSX"]
320 fovflag = world_db["Things"][0]["fovmap"][(y * length) + x]
321 if 'v' == chr(fovflag):
322 c = world_db["ThingTypes"][type]["TT_SYMBOL"]
323 map[(y * length) + x] = ord(c)
325 def write_map(string, map):
326 for i in range(length):
327 line = map[i * length:(i * length) + length].decode()
328 string = string + line + "\n"
332 if [] == world_db["Things"][0]["T_CARRIES"]:
333 inventory = "(none)\n"
335 for id in world_db["Things"][0]["T_CARRIES"]:
336 type_id = world_db["Things"][id]["T_TYPE"]
337 name = world_db["ThingTypes"][type_id]["TT_NAME"]
338 inventory = inventory + name + "\n"
339 # 7DRL additions: GOD_MOOD, GOD_FAVOR
340 string = str(world_db["TURN"]) + "\n" + \
341 str(world_db["GOD_MOOD"]) + "\n" + \
342 str(world_db["GOD_FAVOR"]) + "\n" + \
343 str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
344 str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
345 inventory + "%\n" + \
346 str(world_db["Things"][0]["T_POSY"]) + "\n" + \
347 str(world_db["Things"][0]["T_POSX"]) + "\n" + \
348 str(world_db["MAP_LENGTH"]) + "\n"
349 length = world_db["MAP_LENGTH"]
350 fov = bytearray(b' ' * (length ** 2))
351 for pos in range(length ** 2):
352 if 'v' == chr(world_db["Things"][0]["fovmap"][pos]):
353 fov[pos] = world_db["MAP"][pos]
355 draw_visible_Things(fov, i)
356 string = write_map(string, fov)
357 mem = world_db["Things"][0]["T_MEMMAP"][:]
359 for mt in world_db["Things"][0]["T_MEMTHING"]:
360 consumable = world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]
361 if (i == 0 and not consumable) or (i == 1 and consumable):
362 c = world_db["ThingTypes"][mt[0]]["TT_SYMBOL"]
363 mem[(mt[1] * length) + mt[2]] = ord(c)
364 string = write_map(string, mem)
365 atomic_write(io_db["path_worldstate"], string, delete=False)
366 strong_write(io_db["file_out"], "WORLD_UPDATED\n")
367 io_db["worldstate_updateable"] = False
371 """Replay game from record file.
373 Use opts.replay as breakpoint turn to which to replay automatically before
374 switching to manual input by non-meta commands in server input file
375 triggering further reads of record file. Ensure opts.replay is at least 1.
376 Run try_worldstate_update() before each interactive obey()/read_command().
380 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
381 " (if so late a turn is to be found).")
382 if not os.access(io_db["path_record"], os.F_OK):
383 raise SystemExit("No record file found to replay.")
384 io_db["file_record"] = open(io_db["path_record"], "r")
385 io_db["file_record"].prefix = "record file line "
386 io_db["file_record"].line_n = 1
387 while world_db["TURN"] < opts.replay:
388 line = io_db["file_record"].readline()
391 obey(line.rstrip(), io_db["file_record"].prefix
392 + str(io_db["file_record"].line_n))
393 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
395 try_worldstate_update()
396 obey(read_command(), "in file", replay=True)
400 """Play game by server input file commands. Before, load save file found.
402 If no save file is found, a new world is generated from the commands in the
403 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
404 command and all that follow via the server input file. Run
405 try_worldstate_update() before each interactive obey()/read_command().
407 if os.access(io_db["path_save"], os.F_OK):
408 obey_lines_in_file(io_db["path_save"], "save")
410 if not os.access(io_db["path_worldconf"], os.F_OK):
411 msg = "No world config file from which to start a new world."
412 raise SystemExit(msg)
413 obey_lines_in_file(io_db["path_worldconf"], "world config ",
415 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
417 try_worldstate_update()
418 obey(read_command(), "in file", do_record=True)
422 """(Re-)make island map.
424 Let "~" represent water, "." land, "X" trees: Build island shape randomly,
425 start with one land cell in the middle, then go into cycle of repeatedly
426 selecting a random sea cell and transforming it into land if it is neighbor
427 to land. The cycle ends when a land cell is due to be created at the map's
428 border. Then put some trees on the map (TODO: more precise algorithm desc).
430 # 7DRL: Also add some ":" cells, and (not surrounded by trees!) "_" altar.
432 def is_neighbor(coordinates, type):
435 length = world_db["MAP_LENGTH"]
437 diag_west = x + (ind > 0)
438 diag_east = x + (ind < (length - 1))
439 pos = (y * length) + x
440 if (y > 0 and diag_east
441 and type == chr(world_db["MAP"][pos - length + ind])) \
443 and type == chr(world_db["MAP"][pos + 1])) \
444 or (y < (length - 1) and diag_east
445 and type == chr(world_db["MAP"][pos + length + ind])) \
446 or (y > 0 and diag_west
447 and type == chr(world_db["MAP"][pos - length - (not ind)])) \
449 and type == chr(world_db["MAP"][pos - 1])) \
450 or (y < (length - 1) and diag_west
451 and type == chr(world_db["MAP"][pos + length - (not ind)])):
455 world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
456 length = world_db["MAP_LENGTH"]
457 add_half_width = (not (length % 2)) * int(length / 2)
458 world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
460 y = rand.next() % length
461 x = rand.next() % length
462 pos = (y * length) + x
463 if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
464 if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
466 world_db["MAP"][pos] = ord(".")
467 n_trees = int((length ** 2) / 16)
469 while (i_trees <= n_trees):
470 single_allowed = rand.next() % 32
471 y = rand.next() % length
472 x = rand.next() % length
473 pos = (y * length) + x
474 if "." == chr(world_db["MAP"][pos]) \
475 and ((not single_allowed) or is_neighbor((y, x), "X")):
476 world_db["MAP"][pos] = ord("X")
478 # This all-too-precise replica of the original C code misses iter_limit().
479 n_colons = int((length ** 2) / 16) # #
481 while (i_colons <= n_colons): # #
482 single_allowed = rand.next() % 256 # #
483 y = rand.next() % length # #
484 x = rand.next() % length # #
485 pos = (y * length) + x # #
486 if ("." == chr(world_db["MAP"][pos]) # #
487 and ((not single_allowed) or is_neighbor((y, x), ":"))): # #
488 world_db["MAP"][pos] = ord(":") # #
490 altar_placed = False # #
491 while not altar_placed: # #
492 y = rand.next() % length # #
493 x = rand.next() % length # #
494 pos = (y * length) + x # #
495 if (("." == chr(world_db["MAP"][pos] # #
496 or ":" == chr(world_db["MAP"][pos]))
497 and not is_neighbor((y, x), 'X'))): # #
498 world_db["MAP"][pos] = ord("_") # #
499 world_db["altar"] = (y, x) # #
500 altar_placed = True # #
503 def update_map_memory(t, age_map=True):
504 """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
506 def age_some_memdepthmap_on_nonfov_cells():
507 # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
511 # for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
512 # if not ord_v == t["fovmap"][pos]
513 # if ord_0 <= t["T_MEMDEPTHMAP"][pos]
514 # if ord_9 > t["T_MEMDEPTHMAP"][pos]
515 # if not rand.next() % (2 **
516 # (t["T_MEMDEPTHMAP"][pos] - 48))]:
517 # t["T_MEMDEPTHMAP"][pos] += 1
518 memdepthmap = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
519 fovmap = c_pointer_to_bytearray(t["fovmap"])
520 libpr.age_some_memdepthmap_on_nonfov_cells(memdepthmap, fovmap)
522 if not t["T_MEMMAP"]:
523 t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
524 if not t["T_MEMDEPTHMAP"]:
525 t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
529 for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
530 if ord_v == t["fovmap"][pos]]:
531 t["T_MEMDEPTHMAP"][pos] = ord_0
532 if ord_space == t["T_MEMMAP"][pos]:
533 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
535 age_some_memdepthmap_on_nonfov_cells()
536 t["T_MEMTHING"] = [mt for mt in t["T_MEMTHING"]
537 if ord_v != t["fovmap"][(mt[1] * world_db["MAP_LENGTH"])
539 for id in [id for id in world_db["Things"]
540 if not world_db["Things"][id]["carried"]]:
541 type = world_db["Things"][id]["T_TYPE"]
542 if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
543 y = world_db["Things"][id]["T_POSY"]
544 x = world_db["Things"][id]["T_POSX"]
545 if ord_v == t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]:
546 t["T_MEMTHING"].append((type, y, x))
549 def set_world_inactive():
550 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
552 if os.access(io_db["path_worldstate"], os.F_OK):
553 os.remove(io_db["path_worldstate"])
554 world_db["WORLD_ACTIVE"] = 0
557 def integer_test(val_string, min, max=None):
558 """Return val_string if possible integer >= min and <= max, else None."""
560 val = int(val_string)
561 if val < min or (max is not None and val > max):
565 msg = "Ignoring: Please use integer >= " + str(min)
567 msg += " and <= " + str(max)
573 def setter(category, key, min, max=None):
574 """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
577 val = integer_test(val_string, min, max)
581 if category == "Thing":
582 id_store = command_tid
583 decorator = test_Thing_id
584 elif category == "ThingType":
585 id_store = command_ttid
586 decorator = test_ThingType_id
587 elif category == "ThingAction":
588 id_store = command_taid
589 decorator = test_ThingAction_id
593 val = integer_test(val_string, min, max)
595 world_db[category + "s"][id_store.id][key] = val
599 def build_fov_map(t):
600 """Build Thing's FOV map."""
601 t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
602 fovmap = c_pointer_to_bytearray(t["fovmap"])
603 map = c_pointer_to_bytearray(world_db["MAP"])
604 if libpr.build_fov_map(t["T_POSY"], t["T_POSX"], fovmap, map):
605 raise RuntimeError("Malloc error in build_fov_Map().")
609 """Send quick usage info to log."""
610 strong_write(io_db["file_out"], "LOG "
611 + "Use 'w'/'e'/'s'/'d'/'x'/'c' to move, and 'w' to wait.\n")
612 strong_write(io_db["file_out"], "LOG "
613 + "Use 'p' to pick up objects, and 'D' to drop them.\n")
614 strong_write(io_db["file_out"], "LOG "
615 + "Some objects can be used (such as: eaten) by 'u' if "
616 + "they are in your inventory. "
617 + "Use 'UP'/'DOWN' to navigate the inventory.\n")
618 strong_write(io_db["file_out"], "LOG "
619 + "Use 'l' to toggle 'look' mode (move an exploration cursor "
620 + "instead of the player over the map).\n")
621 strong_write(io_db["file_out"], "LOG See README file for more details.\n")
622 strong_write(io_db["file_out"], "LOG \n")
625 def decrement_lifepoints(t):
626 """Decrement t's lifepoints by 1, and if to zero, corpse it.
628 If t is the player avatar, only blank its fovmap, so that the client may
629 still display memory data. On non-player things, erase fovmap and memory.
630 Dying actors drop all their things.
632 # 7DRL: Also decrements God's mood; deaths heavily so.
633 # 7DRL: Return 1 if death, else 0.
634 t["T_LIFEPOINTS"] -= 1
635 world_db["GOD_MOOD"] -= 1 # #
636 if 0 == t["T_LIFEPOINTS"]:
637 sadness = world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"] # #
638 world_db["GOD_MOOD"] -= sadness # #
639 for id in t["T_CARRIES"]:
640 t["T_CARRIES"].remove(id)
641 world_db["Things"][id]["T_POSY"] = t["T_POSY"]
642 world_db["Things"][id]["T_POSX"] = t["T_POSX"]
643 world_db["Things"][id]["carried"] = False
644 t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
645 if world_db["Things"][0] == t:
646 t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
647 strong_write(io_db["file_out"], "LOG You die.\n")
648 strong_write(io_db["file_out"],
649 "LOG See README on how to start over.\n")
652 t["T_MEMMAP"] = False
653 t["T_MEMDEPTHMAP"] = False
659 def add_gods_favor(i): # #
660 """"Add to GOD_FAVOR, multiplied with factor growing log. with GOD_MOOD."""
661 def favor_multiplier(i):
663 threshold = math.e * x
664 mood = world_db["GOD_MOOD"]
667 i = i * math.log(mood / x)
668 elif -mood > threshold:
669 i = i / math.log(-mood / x)
671 if -mood > threshold:
672 i = i * math.log(-mood / x)
674 i = i / math.log(mood / x)
676 world_db["GOD_FAVOR"] += favor_multiplier(i)
679 def mv_yx_in_dir_legal(dir, y, x):
680 """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
681 dir_c = dir.encode("ascii")[0]
682 test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
684 raise RuntimeError("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
685 return (test, libpr.result_y(), libpr.result_x())
688 def enter_altar(): # #
689 """What happens when the player enters the altar."""
690 if world_db["GAME_WON"]:
691 strong_write(io_db["file_out"],
692 "LOG You step on a soul-less slab of stone.\n")
694 strong_write(io_db["file_out"], "LOG YOU ENTER SACRED GROUND.\n")
695 if world_db["GOD_FAVOR"] > 9000:
696 world_db["GAME_WON"] = 1
697 strong_write(io_db["file_out"], "LOG The Island God speaks to you: "
698 + "\"You have proven yourself worthy of my respect. "
699 + "You were a good citizen to the island, and sometimes "
700 + "a better steward to its inhabitants than me. The "
701 + "island shall miss you when you leave. But you have "
702 + "earned the right to do so. Take this "
703 + world_db["ThingTypes"][world_db["SLIPPERS"]]["TT_NAME"]
704 + " and USE it when you please. It will take you to "
705 + "where you came from. (But do feel free to stay here "
706 + "as long as you like.)\"\n")
707 id = id_setter(-1, "Things")
708 world_db["Things"][id] = new_Thing(world_db["SLIPPERS"],
713 """Make t do nothing (but loudly, if player avatar)."""
714 if t == world_db["Things"][0]:
715 strong_write(io_db["file_out"], "LOG You wait.\n")
719 """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
720 # 7DRL: Player wounding (worse: killing) others will lower God's favor.
721 # 7DRL: Player entering the altar triggers enter_altar().
723 move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
724 t["T_POSY"], t["T_POSX"])
725 if 1 == move_result[0]:
726 pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
727 passable = ("." == chr(world_db["MAP"][pos]) or
728 ":" == chr(world_db["MAP"][pos]) or # #
729 "_" == chr(world_db["MAP"][pos])) # #
730 hitted = [id for id in world_db["Things"]
731 if world_db["Things"][id] != t
732 if world_db["Things"][id]["T_LIFEPOINTS"]
733 if world_db["Things"][id]["T_POSY"] == move_result[1]
734 if world_db["Things"][id]["T_POSX"] == move_result[2]]
737 if t == world_db["Things"][0]:
738 hitted_type = world_db["Things"][hit_id]["T_TYPE"]
739 hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
740 strong_write(io_db["file_out"], "LOG You wound " + hitted_name
742 add_gods_favor(-1) # #
744 hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
745 strong_write(io_db["file_out"], "LOG " + hitter_name +
747 test = decrement_lifepoints(world_db["Things"][hit_id]) # #(test=)
748 if test and t == world_db["Things"][0]: # #
749 add_gods_favor(-test) # #
751 dir = [dir for dir in directions_db
752 if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
754 t["T_POSY"] = move_result[1]
755 t["T_POSX"] = move_result[2]
756 for id in t["T_CARRIES"]:
757 world_db["Things"][id]["T_POSY"] = move_result[1]
758 world_db["Things"][id]["T_POSX"] = move_result[2]
760 if t == world_db["Things"][0]:
761 strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
762 if (move_result[1] == world_db["altar"][0] and # #
763 move_result[2] == world_db["altar"][1]): # #
765 elif t == world_db["Things"][0]:
766 strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
769 def actor_pick_up(t):
770 """Make t pick up (topmost?) Thing from ground into inventory."""
771 # Topmostness is actually not defined so far. Picks most nutritious Thing.
772 # 7DRL: Non-player picking up player-dropped consumable -> GOD_FAVOR gain.
773 used_slots = len(t["T_CARRIES"]) # #
774 if used_slots < world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]: # #
775 ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
776 if not world_db["Things"][id]["carried"]
777 if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
778 if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
783 type = world_db["Things"][id]["T_TYPE"]
784 if world_db["ThingTypes"][type]["TT_CONSUMABLE"] > nutritious:
785 nutritious = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
787 world_db["Things"][highest_id]["carried"] = True
788 if (t != world_db["Things"][0] and # #
789 world_db["Things"][highest_id]["T_PLAYERDROP"]): # #
790 x = world_db["Things"][highest_id]["T_TYPE"]
791 score = world_db["ThingTypes"][x]["TT_CONSUMABLE"] / 32 # #
792 add_gods_favor(score) # #
793 world_db["Things"][highest_id]["T_PLAYERDROP"] = 0 # #
794 t["T_CARRIES"].append(highest_id)
795 if t == world_db["Things"][0]:
796 strong_write(io_db["file_out"], "LOG You pick up an object.\n")
797 elif t == world_db["Things"][0]:
798 err = "You try to pick up an object, but there is none."
799 strong_write(io_db["file_out"], "LOG " + err + "\n")
800 elif t == world_db["Things"][0]: # #
801 strong_write(io_db["file_out"], "LOG Can't pick up object: " + # #
802 "No storage room to carry more.\n") # #
806 """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
807 # TODO: Handle case where T_ARGUMENT matches nothing.
808 if len(t["T_CARRIES"]):
809 id = t["T_CARRIES"][t["T_ARGUMENT"]]
810 t["T_CARRIES"].remove(id)
811 world_db["Things"][id]["carried"] = False
812 if t == world_db["Things"][0]:
813 strong_write(io_db["file_out"], "LOG You drop an object.\n")
814 world_db["Things"][id]["T_PLAYERDROP"] = 1 # #
815 elif t == world_db["Things"][0]:
816 err = "You try to drop an object, but you own none."
817 strong_write(io_db["file_out"], "LOG " + err + "\n")
821 """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
822 # TODO: Handle case where T_ARGUMENT matches nothing.
823 # 7DLR: Handle SLIPPERS-type Thing use.
824 if len(t["T_CARRIES"]):
825 id = t["T_CARRIES"][t["T_ARGUMENT"]]
826 type = world_db["Things"][id]["T_TYPE"]
827 if type == world_db["SLIPPERS"]: # #
828 if t == world_db["Things"][0]: # #
829 strong_write(io_db["file_out"], "LOG You use the " # #
830 + world_db["ThingTypes"][type]["TT_NAME"] # #
831 + ". It glows in wondrous colors, and emits " # #
832 + "a sound as if from a dying cat. The " # #
833 + "Island God laughs.\n") # #
834 t["T_LIFEPOINTS"] = 1 # #
835 decrement_lifepoints(t) # #
836 elif world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
837 t["T_CARRIES"].remove(id)
838 del world_db["Things"][id]
839 t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
840 if t == world_db["Things"][0]:
841 strong_write(io_db["file_out"],
842 "LOG You consume this object.\n")
843 elif t == world_db["Things"][0]:
844 strong_write(io_db["file_out"],
845 "LOG You try to use this object, but fail.\n")
846 elif t == world_db["Things"][0]:
847 strong_write(io_db["file_out"],
848 "LOG You try to use an object, but you own none.\n")
851 def thingproliferation(t, prol_map):
852 """To chance of 1/TT_PROLIFERATE,create t offspring in open neighbor cell.
854 Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be be
855 marked "." in prol_map. If there are several map cell candidates, one is
858 # 7DRL: success increments God's mood
859 # 7DRL: Things proliferate only on ":" ground.
860 prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
861 if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)):
863 for dir in [directions_db[key] for key in directions_db]:
864 mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
865 if mv_result[0] and ord(":") == prol_map[mv_result[1] # #
866 # if mv_result[0] and ord(".") == prol_map[mv_result[1]
867 * world_db["MAP_LENGTH"]
869 candidates.append((mv_result[1], mv_result[2]))
871 i = rand.next() % len(candidates)
872 id = id_setter(-1, "Things")
873 newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
874 world_db["Things"][id] = newT
875 world_db["GOD_MOOD"] += 1 # #
879 """Grow t's HP to a 1/32 chance if < HP max, satiation > 0, and waiting.
881 On success, decrease satiation score by 32.
883 # 7DRL: Successful heals increment God's mood.
884 if t["T_SATIATION"] > 0 \
885 and t["T_LIFEPOINTS"] < \
886 world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"] \
887 and 0 == (rand.next() % 31) \
888 and t["T_COMMAND"] == [id for id in world_db["ThingActions"]
889 if world_db["ThingActions"][id]["TA_NAME"] ==
891 t["T_LIFEPOINTS"] += 1
892 world_db["GOD_MOOD"] += 1 # #
893 t["T_SATIATION"] -= 32
894 if t == world_db["Things"][0]:
895 strong_write(io_db["file_out"], "LOG You heal.\n")
899 """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
900 if t["T_SATIATION"] > -32768:
901 t["T_SATIATION"] -= 1
902 testbase = t["T_SATIATION"] if t["T_SATIATION"] >= 0 else -t["T_SATIATION"]
903 if not world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]:
904 raise RuntimeError("A thing that should not hunger is hungering.")
905 stomach = int(32767 / world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"])
906 if int(int(testbase / stomach) / ((rand.next() % stomach) + 1)):
907 if t == world_db["Things"][0]:
908 strong_write(io_db["file_out"], "LOG You suffer from hunger.\n")
909 decrement_lifepoints(t)
912 def get_dir_to_target(t, filter):
913 """Try to set T_COMMAND/T_ARGUMENT for move to "filter"-determined target.
915 The path-wise nearest target is chosen, via the shortest available path.
916 Target must not be t. On succcess, return positive value, else False.
918 "a": Thing in FOV is below a certain distance, animate, but of ThingType
919 that is not t's, and starts out weaker than t is; build path as
920 avoiding things of t's ThingType
921 "f": neighbor cell (not inhabited by any animate Thing) further away from
922 animate Thing not further than x steps away and in FOV and of a
923 ThingType that is not t's, and starts out stronger or as strong as t
924 is currently; or (cornered), if no such flight cell, but Thing of
925 above criteria is too near,1 a cell closer to it, or, if less near,
927 "c": Thing in memorized map is consumable
928 "s": memory map cell with greatest-reachable degree of unexploredness
931 def zero_score_map_where_char_on_memdepthmap(c):
932 # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
933 # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
934 # if t["T_MEMDEPTHMAP"][i] == mem_depth_c[0]]:
935 # set_map_score(i, 0)
936 map = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
937 if libpr.zero_score_map_where_char_on_memdepthmap(c, map):
938 raise RuntimeError("No score map allocated for "
939 "zero_score_map_where_char_on_memdepthmap().")
941 def set_map_score(pos, score):
942 test = libpr.set_map_score(pos, score)
944 raise RuntimeError("No score map allocated for set_map_score().")
946 def get_map_score(pos):
947 result = libpr.get_map_score(pos)
949 raise RuntimeError("No score map allocated for get_map_score().")
953 if t["fovmap"] and ("a" == filter or "f" == filter):
954 for id in world_db["Things"]:
955 Thing = world_db["Things"][id]
956 if Thing != t and Thing["T_LIFEPOINTS"] and \
957 t["T_TYPE"] != Thing["T_TYPE"] and \
958 'v' == chr(t["fovmap"][(Thing["T_POSY"]
959 * world_db["MAP_LENGTH"])
961 ThingType = world_db["ThingTypes"][Thing["T_TYPE"]]
962 if ("f" == filter and ThingType["TT_LIFEPOINTS"] >=
964 or ("a" == filter and ThingType["TT_LIFEPOINTS"] <
967 elif t["T_MEMMAP"] and "c" == filter:
968 for mt in t["T_MEMTHING"]:
969 if ' ' != chr(t["T_MEMMAP"][(mt[1] * world_db["MAP_LENGTH"])
971 and world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]:
975 def set_cells_passable_on_memmap_to_65534_on_scoremap():
976 # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
978 # memmap = t["T_MEMMAP"]
979 # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
980 # if ord_dot == memmap[i]]:
981 # set_map_score(i, 65534) # i.e. 65535-1
982 map = c_pointer_to_bytearray(t["T_MEMMAP"])
983 if libpr.set_cells_passable_on_memmap_to_65534_on_scoremap(map):
984 raise RuntimeError("No score map allocated for "
985 "set_cells_passable_on_memmap_to_65534_on_scoremap().")
987 def init_score_map():
988 test = libpr.init_score_map()
990 raise RuntimeError("Malloc error in init_score_map().")
993 set_cells_passable_on_memmap_to_65534_on_scoremap()
995 for id in world_db["Things"]:
996 Thing = world_db["Things"][id]
997 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
999 if t != Thing and Thing["T_LIFEPOINTS"] and \
1000 t["T_TYPE"] != Thing["T_TYPE"] and \
1001 ord_v == t["fovmap"][pos] and \
1002 t["T_LIFEPOINTS"] > \
1003 world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
1004 set_map_score(pos, 0)
1005 elif t["T_TYPE"] == Thing["T_TYPE"]:
1006 set_map_score(pos, 65535)
1008 for id in [id for id in world_db["Things"]
1009 if world_db["Things"][id]["T_LIFEPOINTS"]]:
1010 Thing = world_db["Things"][id]
1011 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
1013 if t["T_TYPE"] != Thing["T_TYPE"] and \
1014 ord_v == t["fovmap"][pos] and \
1015 t["T_LIFEPOINTS"] <= \
1016 world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
1017 set_map_score(pos, 0)
1019 for mt in [mt for mt in t["T_MEMTHING"]
1020 if ord_blank != t["T_MEMMAP"][mt[1]
1021 * world_db["MAP_LENGTH"]
1023 if world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]]:
1024 set_map_score(mt[1] * world_db["MAP_LENGTH"] + mt[2], 0)
1026 zero_score_map_where_char_on_memdepthmap(mem_depth_c[0])
1028 def rand_target_dir(neighbors, cmp, dirs):
1031 for i in range(len(dirs)):
1032 if cmp == neighbors[i]:
1033 candidates.append(dirs[i])
1035 return candidates[rand.next() % n_candidates] if n_candidates else 0
1037 def get_neighbor_scores(dirs, eye_pos):
1039 if libpr.ready_neighbor_scores(eye_pos):
1040 raise RuntimeError("No score map allocated for " +
1041 "ready_neighbor_scores.()")
1042 for i in range(len(dirs)):
1043 scores.append(libpr.get_neighbor_score(i))
1046 def get_dir_from_neighbors():
1047 dir_to_target = False
1049 eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
1050 neighbors = get_neighbor_scores(dirs, eye_pos)
1052 inhabited = [world_db["Things"][id]["T_POSY"]
1053 * world_db["MAP_LENGTH"]
1054 + world_db["Things"][id]["T_POSX"]
1055 for id in world_db["Things"]
1056 if world_db["Things"][id]["T_LIFEPOINTS"]]
1057 for i in range(len(dirs)):
1058 mv_yx_in_dir_legal(dirs[i], t["T_POSY"], t["T_POSX"])
1059 pos_cmp = libpr.result_y() * world_db["MAP_LENGTH"] \
1061 for pos in [pos for pos in inhabited if pos == pos_cmp]:
1062 neighbors[i] = 65535
1064 minmax_start = 0 if "f" == filter else 65535 - 1
1065 minmax_neighbor = minmax_start
1066 for i in range(len(dirs)):
1067 if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
1068 minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
1069 or ("f" != filter and minmax_neighbor > neighbors[i]):
1070 minmax_neighbor = neighbors[i]
1071 if minmax_neighbor != minmax_start:
1072 dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
1074 if not dir_to_target:
1075 if 1 == get_map_score(eye_pos):
1076 dir_to_target = rand_target_dir(neighbors, 0, dirs)
1077 elif 3 >= get_map_score(eye_pos):
1078 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1080 world_db["ThingActions"][id]["TA_NAME"]
1083 elif dir_to_target and 3 < get_map_score(eye_pos):
1085 elif "a" == filter and 10 <= get_map_score(eye_pos):
1087 return dir_to_target
1089 dir_to_target = False
1091 run_i = 9 + 1 if "s" == filter else 1
1092 while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
1095 mem_depth_c = b'9' if b' ' == mem_depth_c \
1096 else bytes([mem_depth_c[0] - 1])
1097 if libpr.dijkstra_map():
1098 raise RuntimeError("No score map allocated for dijkstra_map().")
1099 dir_to_target = get_dir_from_neighbors()
1100 libpr.free_score_map()
1101 if dir_to_target and str == type(dir_to_target):
1102 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1103 if world_db["ThingActions"][id]["TA_NAME"]
1105 t["T_ARGUMENT"] = ord(dir_to_target)
1106 return dir_to_target
1109 def standing_on_consumable(t):
1110 """Return True/False whether t is standing on a consumable."""
1111 for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
1112 if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
1113 if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
1114 if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
1120 def get_inventory_slot_to_consume(t):
1121 """Return slot Id of strongest consumable in t's inventory, else -1."""
1122 cmp_consumability = 0
1125 for id in t["T_CARRIES"]:
1126 type = world_db["Things"][id]["T_TYPE"]
1127 if world_db["ThingTypes"][type]["TT_CONSUMABLE"] > cmp_consumability:
1128 cmp_consumability = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
1135 """Determine next command/argment for actor t via AI algorithms.
1137 AI will look for, and move towards, enemies (animate Things not of their
1138 own ThingType); if they see none, they will consume consumables in their
1139 inventory; if there are none, they will pick up what they stand on if they
1140 stand on consumables; if they stand on none, they will move towards the
1141 next consumable they see or remember on the map; if they see or remember
1142 none, they will explore parts of the map unseen since ever or for at least
1143 one turn; if there is nothing to explore, they will simply wait.
1145 # 7DRL add: Don't pick up or search things when inventory is full.
1146 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1147 if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1148 if not get_dir_to_target(t, "f"):
1149 sel = get_inventory_slot_to_consume(t)
1151 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1152 if world_db["ThingActions"][id]["TA_NAME"]
1154 t["T_ARGUMENT"] = sel
1155 elif standing_on_consumable(t) \
1156 and (len(t["T_CARRIES"]) < # #
1157 world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]): # #
1158 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1159 if world_db["ThingActions"][id]["TA_NAME"]
1162 (len(t["T_CARRIES"]) < # #
1163 world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"] # #
1164 and get_dir_to_target(t, "c"))) and \
1165 (not get_dir_to_target(t, "a")):
1166 get_dir_to_target(t, "s")
1170 """Run game world and its inhabitants until new player input expected."""
1172 whilebreaker = False
1173 while world_db["Things"][0]["T_LIFEPOINTS"]:
1174 proliferable_map = world_db["MAP"][:]
1175 for id in [id for id in world_db["Things"]
1176 if not world_db["Things"][id]["carried"]]:
1177 y = world_db["Things"][id]["T_POSY"]
1178 x = world_db["Things"][id]["T_POSX"]
1179 proliferable_map[y * world_db["MAP_LENGTH"] + x] = ord('X')
1180 for id in [id for id in world_db["Things"]]: # Only what's from start!
1181 if not id in world_db["Things"] or \
1182 world_db["Things"][id]["carried"]: # May have been consumed or
1183 continue # picked up during turn …
1184 Thing = world_db["Things"][id]
1185 if Thing["T_LIFEPOINTS"]:
1186 if not Thing["T_COMMAND"]:
1187 update_map_memory(Thing)
1194 if Thing["T_LIFEPOINTS"]:
1195 Thing["T_PROGRESS"] += 1
1196 taid = [a for a in world_db["ThingActions"]
1197 if a == Thing["T_COMMAND"]][0]
1198 ThingAction = world_db["ThingActions"][taid]
1199 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
1200 eval("actor_" + ThingAction["TA_NAME"])(Thing)
1201 Thing["T_COMMAND"] = 0
1202 Thing["T_PROGRESS"] = 0
1203 thingproliferation(Thing, proliferable_map)
1206 world_db["TURN"] += 1
1209 def new_Thing(type, pos=(0, 0)):
1210 """Return Thing of type T_TYPE, with fovmap if alive and world active."""
1212 "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
1217 "T_PLAYERDROP": 0, # #
1225 "T_MEMDEPTHMAP": False,
1228 if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
1229 build_fov_map(thing)
1233 def id_setter(id, category, id_store=False, start_at_1=False):
1234 """Set ID of object of category to manipulate ID unused? Create new one.
1236 The ID is stored as id_store.id (if id_store is set). If the integer of the
1237 input is valid (if start_at_1, >= 0, else >= -1), but <0 or (if start_at_1)
1238 <1, calculate new ID: lowest unused ID >=0 or (if start_at_1) >= 1. None is
1239 always returned when no new object is created, otherwise the new object's
1242 min = 0 if start_at_1 else -1
1244 id = integer_test(id, min)
1246 if id in world_db[category]:
1251 if (start_at_1 and 0 == id) \
1252 or ((not start_at_1) and (id < 0)):
1253 id = 0 if start_at_1 else -1
1256 if id not in world_db[category]:
1264 """Send PONG line to server output file."""
1265 strong_write(io_db["file_out"], "PONG\n")
1269 """Abort server process."""
1270 if None == opts.replay:
1271 if world_db["WORLD_ACTIVE"]:
1273 atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True)
1274 raise SystemExit("received QUIT command")
1277 def command_thingshere(str_y, str_x):
1278 """Write to out file list of Things known to player at coordinate y, x."""
1279 if world_db["WORLD_ACTIVE"]:
1280 y = integer_test(str_y, 0, 255)
1281 x = integer_test(str_x, 0, 255)
1282 length = world_db["MAP_LENGTH"]
1283 if None != y and None != x and y < length and x < length:
1284 pos = (y * world_db["MAP_LENGTH"]) + x
1285 strong_write(io_db["file_out"], "THINGS_HERE START\n")
1286 if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
1287 for id in world_db["Things"]:
1288 if y == world_db["Things"][id]["T_POSY"] \
1289 and x == world_db["Things"][id]["T_POSX"] \
1290 and not world_db["Things"][id]["carried"]:
1291 type = world_db["Things"][id]["T_TYPE"]
1292 name = world_db["ThingTypes"][type]["TT_NAME"]
1293 strong_write(io_db["file_out"], name + "\n")
1295 for mt in world_db["Things"][0]["T_MEMTHING"]:
1296 if y == mt[1] and x == mt[2]:
1297 name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
1298 strong_write(io_db["file_out"], name + "\n")
1299 strong_write(io_db["file_out"], "THINGS_HERE END\n")
1301 print("Ignoring: Invalid map coordinates.")
1303 print("Ignoring: Command only works on existing worlds.")
1306 def play_commander(action, args=False):
1307 """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
1309 T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
1313 id = [x for x in world_db["ThingActions"]
1314 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
1315 world_db["Things"][0]["T_COMMAND"] = id
1318 def set_command_and_argument_int(str_arg):
1319 val = integer_test(str_arg, 0, 255)
1321 world_db["Things"][0]["T_ARGUMENT"] = val
1324 def set_command_and_argument_movestring(str_arg):
1325 if str_arg in directions_db:
1326 world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
1329 print("Ignoring: Argument must be valid direction string.")
1331 if action == "move":
1332 return set_command_and_argument_movestring
1334 return set_command_and_argument_int
1339 def command_seedrandomness(seed_string):
1340 """Set rand seed to int(seed_string)."""
1341 val = integer_test(seed_string, 0, 4294967295)
1346 def command_makeworld(seed_string):
1347 """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
1349 Seed rand with seed. Do more only with a "wait" ThingAction and
1350 world["PLAYER_TYPE"] matching ThingType of TT_START_NUMBER > 0. Then,
1351 world_db["Things"] emptied, call make_map() and set
1352 world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1353 according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1354 of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1355 other. Init player's memory map. Write "NEW_WORLD" line to out file.
1359 # def free_pos(plant=False):
1360 def free_pos(plant=False): # #
1363 err = "Space to put thing on too hard to find. Map too small?"
1365 y = rand.next() % world_db["MAP_LENGTH"]
1366 x = rand.next() % world_db["MAP_LENGTH"]
1367 pos = y * world_db["MAP_LENGTH"] + x;
1369 and "." == chr(world_db["MAP"][pos])) \
1370 or ":" == chr(world_db["MAP"][pos]): # #
1374 raise SystemExit(err)
1375 # Replica of C code, wrongly ignores animatedness of new Thing.
1376 pos_clear = (0 == len([id for id in world_db["Things"]
1377 if world_db["Things"][id]["T_LIFEPOINTS"]
1378 if world_db["Things"][id]["T_POSY"] == y
1379 if world_db["Things"][id]["T_POSX"] == x]))
1384 val = integer_test(seed_string, 0, 4294967295)
1388 player_will_be_generated = False
1389 playertype = world_db["PLAYER_TYPE"]
1390 for ThingType in world_db["ThingTypes"]:
1391 if playertype == ThingType:
1392 if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1393 player_will_be_generated = True
1395 if not player_will_be_generated:
1396 print("Ignoring: No player type with start number >0 defined.")
1399 for ThingAction in world_db["ThingActions"]:
1400 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1403 print("Ignoring: No thing action with name 'wait' defined.")
1405 if not world_db["SLIPPERS"] in world_db["ThingTypes"]: # #
1406 print("Ignoring: No valid SLIPPERS set.") # #
1408 world_db["Things"] = {}
1410 world_db["WORLD_ACTIVE"] = 1
1411 world_db["TURN"] = 1
1412 for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1413 id = id_setter(-1, "Things")
1414 world_db["Things"][id] = new_Thing(playertype, free_pos())
1415 if not world_db["Things"][0]["fovmap"]:
1416 empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1417 world_db["Things"][0]["fovmap"] = empty_fovmap
1418 update_map_memory(world_db["Things"][0])
1419 for type in world_db["ThingTypes"]:
1420 for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1421 if type != playertype:
1422 id = id_setter(-1, "Things")
1423 plantness = world_db["ThingTypes"][type]["TT_PROLIFERATE"] # #
1424 world_db["Things"][id] = new_Thing(type, free_pos(plantness))
1425 strong_write(io_db["file_out"], "NEW_WORLD\n")
1429 def command_maplength(maplength_string):
1430 """Redefine map length. Invalidate map, therefore lose all things on it."""
1431 val = integer_test(maplength_string, 1, 256)
1433 world_db["MAP_LENGTH"] = val
1434 world_db["MAP"] = False
1435 set_world_inactive()
1436 world_db["Things"] = {}
1437 libpr.set_maplength(val)
1440 def command_worldactive(worldactive_string):
1441 """Toggle world_db["WORLD_ACTIVE"] if possible.
1443 An active world can always be set inactive. An inactive world can only be
1444 set active with a "wait" ThingAction, and a player Thing (of ID 0), and a
1445 map. On activation, rebuild all Things' FOVs, and the player's map memory.
1446 Also call log_help().
1448 # 7DRL: altar must be on map, and (valid) SLIPPERS must be set for world
1450 val = integer_test(worldactive_string, 0, 1)
1452 if 0 != world_db["WORLD_ACTIVE"]:
1454 set_world_inactive()
1456 print("World already active.")
1457 elif 0 == world_db["WORLD_ACTIVE"]:
1459 for ThingAction in world_db["ThingActions"]:
1460 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1463 player_exists = False
1464 for Thing in world_db["Things"]:
1466 player_exists = True
1468 valid_slippers = world_db["SLIPPERS"] in world_db["ThingTypes"] # #
1469 altar_found = False # #
1470 if world_db["MAP"]: # #
1471 pos = world_db["MAP"].find(b'_') # #
1473 y = int(pos / world_db["MAP_LENGTH"]) # #
1474 x = pos % world_db["MAP_LENGTH"] # #
1475 world_db["altar"] = (y, x) # #
1476 altar_found = True # #
1477 if wait_exists and player_exists and world_db["MAP"] \
1478 and altar_found and valid_slippers: # #
1479 for id in world_db["Things"]:
1480 if world_db["Things"][id]["T_LIFEPOINTS"]:
1481 build_fov_map(world_db["Things"][id])
1483 update_map_memory(world_db["Things"][id], False)
1484 if not world_db["Things"][0]["T_LIFEPOINTS"]:
1485 empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1486 world_db["Things"][0]["fovmap"] = empty_fovmap
1487 world_db["WORLD_ACTIVE"] = 1
1490 print("Ignoring: Not all conditions for world activation met.")
1493 def command_slippers(str_int): # #
1494 """Set SLIPPERS, but deactivate world if not in ThingTypes."""
1495 val = integer_test(str_int, 0)
1497 world_db["SLIPPERS"] = val
1498 if world_db["WORLD_ACTIVE"] and \
1499 world_db["SLIPPERS"] not in world_db["ThingTypes"]:
1500 world_db["WORLD_ACTIVE"] = 0
1501 print("SLIPPERS matches no known ThingTypes, deactivating world.")
1504 def test_for_id_maker(object, category):
1505 """Return decorator testing for object having "id" attribute."""
1508 if hasattr(object, "id"):
1511 print("Ignoring: No " + category +
1512 " defined to manipulate yet.")
1517 def command_tid(id_string):
1518 """Set ID of Thing to manipulate. ID unused? Create new one.
1520 Default new Thing's type to the first available ThingType, others: zero.
1522 id = id_setter(id_string, "Things", command_tid)
1524 if world_db["ThingTypes"] == {}:
1525 print("Ignoring: No ThingType to settle new Thing in.")
1527 type = list(world_db["ThingTypes"].keys())[0]
1528 world_db["Things"][id] = new_Thing(type)
1531 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1535 def command_tcommand(str_int):
1536 """Set T_COMMAND of selected Thing."""
1537 val = integer_test(str_int, 0)
1539 if 0 == val or val in world_db["ThingActions"]:
1540 world_db["Things"][command_tid.id]["T_COMMAND"] = val
1542 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1546 def command_ttype(str_int):
1547 """Set T_TYPE of selected Thing."""
1548 val = integer_test(str_int, 0)
1550 if val in world_db["ThingTypes"]:
1551 world_db["Things"][command_tid.id]["T_TYPE"] = val
1553 print("Ignoring: ThingType ID belongs to no known ThingType.")
1557 def command_tcarries(str_int):
1558 """Append int(str_int) to T_CARRIES of selected Thing.
1560 The ID int(str_int) must not be of the selected Thing, and must belong to a
1561 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1563 val = integer_test(str_int, 0)
1565 if val == command_tid.id:
1566 print("Ignoring: Thing cannot carry itself.")
1567 elif val in world_db["Things"] \
1568 and not world_db["Things"][val]["carried"]:
1569 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1570 world_db["Things"][val]["carried"] = True
1572 print("Ignoring: Thing not available for carrying.")
1573 # Note that the whole carrying structure is different from the C version:
1574 # Carried-ness is marked by a "carried" flag, not by Things containing
1575 # Things internally.
1579 def command_tmemthing(str_t, str_y, str_x):
1580 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1582 The type must fit to an existing ThingType, and the position into the map.
1584 type = integer_test(str_t, 0)
1585 posy = integer_test(str_y, 0, 255)
1586 posx = integer_test(str_x, 0, 255)
1587 if None != type and None != posy and None != posx:
1588 if type not in world_db["ThingTypes"] \
1589 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1590 print("Ignoring: Illegal value for thing type or position.")
1592 memthing = (type, posy, posx)
1593 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1596 def setter_map(maptype):
1597 """Set (world or Thing's) map of maptype's int(str_int)-th line to mapline.
1599 If no map of maptype exists yet, initialize it with ' ' bytes first.
1602 def valid_map_line(str_int, mapline):
1603 val = integer_test(str_int, 0, 255)
1605 if val >= world_db["MAP_LENGTH"]:
1606 print("Illegal value for map line number.")
1607 elif len(mapline) != world_db["MAP_LENGTH"]:
1608 print("Map line length is unequal map width.")
1613 def nonThingMap_helper(str_int, mapline):
1614 val = valid_map_line(str_int, mapline)
1616 length = world_db["MAP_LENGTH"]
1617 if not world_db["MAP"]:
1618 map = bytearray(b' ' * (length ** 2))
1620 map = world_db["MAP"]
1621 map[val * length:(val * length) + length] = mapline.encode()
1622 if not world_db["MAP"]:
1623 world_db["MAP"] = map
1626 def ThingMap_helper(str_int, mapline):
1627 val = valid_map_line(str_int, mapline)
1629 length = world_db["MAP_LENGTH"]
1630 if not world_db["Things"][command_tid.id][maptype]:
1631 map = bytearray(b' ' * (length ** 2))
1633 map = world_db["Things"][command_tid.id][maptype]
1634 map[val * length:(val * length) + length] = mapline.encode()
1635 if not world_db["Things"][command_tid.id][maptype]:
1636 world_db["Things"][command_tid.id][maptype] = map
1638 return nonThingMap_helper if maptype == "MAP" else ThingMap_helper
1641 def setter_tpos(axis):
1642 """Generate setter for T_POSX or T_POSY of selected Thing.
1644 If world is active, rebuilds animate things' fovmap, player's memory map.
1647 def helper(str_int):
1648 val = integer_test(str_int, 0, 255)
1650 if val < world_db["MAP_LENGTH"]:
1651 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1652 if world_db["WORLD_ACTIVE"] \
1653 and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1654 build_fov_map(world_db["Things"][command_tid.id])
1655 if 0 == command_tid.id:
1656 update_map_memory(world_db["Things"][command_tid.id])
1658 print("Ignoring: Position is outside of map.")
1662 def command_ttid(id_string):
1663 """Set ID of ThingType to manipulate. ID unused? Create new one.
1665 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1667 id = id_setter(id_string, "ThingTypes", command_ttid)
1669 world_db["ThingTypes"][id] = {
1670 "TT_NAME": "(none)",
1673 "TT_PROLIFERATE": 0,
1674 "TT_START_NUMBER": 0,
1675 "TT_STORAGE": 0, # #
1681 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1685 def command_ttname(name):
1686 """Set TT_NAME of selected ThingType."""
1687 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1691 def command_ttsymbol(char):
1692 """Set TT_SYMBOL of selected ThingType. """
1694 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1696 print("Ignoring: Argument must be single character.")
1700 def command_ttcorpseid(str_int):
1701 """Set TT_CORPSE_ID of selected ThingType."""
1702 val = integer_test(str_int, 0)
1704 if val in world_db["ThingTypes"]:
1705 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1707 print("Ignoring: Corpse ID belongs to no known ThignType.")
1710 def command_taid(id_string):
1711 """Set ID of ThingAction to manipulate. ID unused? Create new one.
1713 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1715 id = id_setter(id_string, "ThingActions", command_taid, True)
1717 world_db["ThingActions"][id] = {
1723 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1726 @test_ThingAction_id
1727 def command_taname(name):
1728 """Set TA_NAME of selected ThingAction.
1730 The name must match a valid thing action function. If after the name
1731 setting no ThingAction with name "wait" remains, call set_world_inactive().
1733 if name == "wait" or name == "move" or name == "use" or name == "drop" \
1734 or name == "pick_up":
1735 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1736 if 1 == world_db["WORLD_ACTIVE"]:
1737 wait_defined = False
1738 for id in world_db["ThingActions"]:
1739 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1742 if not wait_defined:
1743 set_world_inactive()
1745 print("Ignoring: Invalid action name.")
1746 # In contrast to the original,naming won't map a function to a ThingAction.
1750 """Call ai() on player Thing, then turn_over()."""
1751 ai(world_db["Things"][0])
1755 """Commands database.
1757 Map command start tokens to ([0]) number of expected command arguments, ([1])
1758 the command's meta-ness (i.e. is it to be written to the record file, is it to
1759 be ignored in replay mode if read from server input file), and ([2]) a function
1763 "QUIT": (0, True, command_quit),
1764 "PING": (0, True, command_ping),
1765 "THINGS_HERE": (2, True, command_thingshere),
1766 "MAKE_WORLD": (1, False, command_makeworld),
1767 "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1768 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1769 "GOD_MOOD": (1, False, setter(None, "GOD_MOOD", -32768, 32767)), # #
1770 "GOD_FAVOR": (1, False, setter(None, "GOD_FAVOR", -32768, 32767)), # #
1771 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0)),
1772 "MAP_LENGTH": (1, False, command_maplength),
1773 "WORLD_ACTIVE": (1, False, command_worldactive),
1774 "MAP": (2, False, setter_map("MAP")),
1775 "GAME_WON": (1, False, setter(None, "GAME_WON", 0, 1)), # #
1776 "SLIPPERS": (1, False, command_slippers), # #
1777 "TA_ID": (1, False, command_taid),
1778 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1779 "TA_NAME": (1, False, command_taname),
1780 "TT_ID": (1, False, command_ttid),
1781 "TT_NAME": (1, False, command_ttname),
1782 "TT_SYMBOL": (1, False, command_ttsymbol),
1783 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1784 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1786 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1788 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1790 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1791 "TT_STORAGE": (1, False, setter("ThingType", "TT_STORAGE", 0, 255)), # #
1792 "T_ID": (1, False, command_tid),
1793 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1794 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1795 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1796 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1797 "T_COMMAND": (1, False, command_tcommand),
1798 "T_TYPE": (1, False, command_ttype),
1799 "T_CARRIES": (1, False, command_tcarries),
1800 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1801 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1802 "T_MEMTHING": (3, False, command_tmemthing),
1803 "T_POSY": (1, False, setter_tpos("Y")),
1804 "T_POSX": (1, False, setter_tpos("X")),
1805 "T_PLAYERDROP": (1, False, setter("Thing", "T_PLAYERDROP", 0, 1)), # #
1806 "wait": (0, False, play_commander("wait")),
1807 "move": (1, False, play_commander("move")),
1808 "pick_up": (0, False, play_commander("pick_up")),
1809 "drop": (1, False, play_commander("drop", True)),
1810 "use": (1, False, play_commander("use", True)),
1811 "ai": (0, False, command_ai)
1813 # TODO: Unhandled cases: (Un-)killing animates (esp. player!) with T_LIFEPOINTS.
1816 """World state database. With sane default values. (Randomness is in rand.)"""
1832 """Mapping of direction names to internal direction chars."""
1833 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1834 "west": "s", "north-west": "w", "north-east": "e"}
1836 """File IO database."""
1838 "path_save": "save",
1839 "path_record": "record_save",
1840 "path_worldconf": "confserver/world",
1841 "path_server": "server/",
1842 "path_in": "server/in",
1843 "path_out": "server/out",
1844 "path_worldstate": "server/worldstate",
1845 "tmp_suffix": "_tmp",
1846 "kicked_by_rival": False,
1847 "worldstate_updateable": False
1852 libpr = prep_library()
1853 rand = RandomnessIO()
1854 opts = parse_command_line_arguments()
1856 io_db["path_save"] = opts.savefile
1857 io_db["path_record"] = "record_" + opts.savefile
1860 io_db["verbose"] = True
1861 if None != opts.replay:
1865 except SystemExit as exit:
1866 print("ABORTING: " + exit.args[0])
1868 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")