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_ACTIVE"], 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 sorted(world_db[category].keys()):
202 string = string + id_string + " " + str(id) + "\n"
203 for key in sorted(world_db[category][id].keys()):
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)
213 for key in sorted(world_db.keys()):
214 if (not isinstance(world_db[key], dict)) and key != "MAP" and \
215 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 sorted(world_db["ThingTypes"].keys()):
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 sorted(world_db["Things"].keys()):
229 if [] != world_db["Things"][id]["T_CARRIES"]:
230 string = string + "T_ID " + str(id) + "\n"
231 for carried in sorted(world_db["Things"][id]["T_CARRIES"]):
232 string = string + "T_CARRIES " + str(carried) + "\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 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))
334 for pos in [pos for pos in range(length ** 2)
335 if ord_v == world_db["Things"][0]["fovmap"][pos]]:
336 fov[pos] = world_db["MAP"][pos]
337 length = world_db["MAP_LENGTH"]
338 for id in [id for tid in reversed(sorted(list(world_db["ThingTypes"])))
339 for id in world_db["Things"]
340 if not world_db["Things"][id]["carried"]
341 if world_db["Things"][id]["T_TYPE"] == tid
342 if world_db["Things"][0]["fovmap"][
343 world_db["Things"][id]["T_POSY"] * length
344 + world_db["Things"][id]["T_POSX"]] == ord_v]:
345 type = world_db["Things"][id]["T_TYPE"]
346 c = ord(world_db["ThingTypes"][type]["TT_SYMBOL"])
347 fov[world_db["Things"][id]["T_POSY"] * length
348 + world_db["Things"][id]["T_POSX"]] = c
349 string = write_map(string, fov)
350 mem = world_db["Things"][0]["T_MEMMAP"][:]
351 for mt in [mt for tid in reversed(sorted(list(world_db["ThingTypes"])))
352 for mt in world_db["Things"][0]["T_MEMTHING"]
354 c = world_db["ThingTypes"][mt[0]]["TT_SYMBOL"]
355 mem[(mt[1] * length) + mt[2]] = ord(c)
356 string = write_map(string, mem)
357 atomic_write(io_db["path_worldstate"], string, delete=False)
358 strong_write(io_db["file_out"], "WORLD_UPDATED\n")
359 io_db["worldstate_updateable"] = False
363 """Replay game from record file.
365 Use opts.replay as breakpoint turn to which to replay automatically before
366 switching to manual input by non-meta commands in server input file
367 triggering further reads of record file. Ensure opts.replay is at least 1.
368 Run try_worldstate_update() before each interactive obey()/read_command().
372 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
373 " (if so late a turn is to be found).")
374 if not os.access(io_db["path_record"], os.F_OK):
375 raise SystemExit("No record file found to replay.")
376 io_db["file_record"] = open(io_db["path_record"], "r")
377 io_db["file_record"].prefix = "record file line "
378 io_db["file_record"].line_n = 1
379 while world_db["TURN"] < opts.replay:
380 line = io_db["file_record"].readline()
383 obey(line.rstrip(), io_db["file_record"].prefix
384 + str(io_db["file_record"].line_n))
385 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
387 try_worldstate_update()
388 obey(read_command(), "in file", replay=True)
392 """Play game by server input file commands. Before, load save file found.
394 If no save file is found, a new world is generated from the commands in the
395 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
396 command and all that follow via the server input file. Run
397 try_worldstate_update() before each interactive obey()/read_command().
399 if os.access(io_db["path_save"], os.F_OK):
400 obey_lines_in_file(io_db["path_save"], "save")
402 if not os.access(io_db["path_worldconf"], os.F_OK):
403 msg = "No world config file from which to start a new world."
404 raise SystemExit(msg)
405 obey_lines_in_file(io_db["path_worldconf"], "world config ",
407 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
409 try_worldstate_update()
410 obey(read_command(), "in file", do_record=True)
414 """(Re-)make island map.
416 Let "~" represent water, "." land, "X" trees: Build island shape randomly,
417 start with one land cell in the middle, then go into cycle of repeatedly
418 selecting a random sea cell and transforming it into land if it is neighbor
419 to land. The cycle ends when a land cell is due to be created at the map's
420 border. Then put some trees on the map (TODO: more precise algorithm desc).
423 def is_neighbor(coordinates, type):
426 length = world_db["MAP_LENGTH"]
428 diag_west = x + (ind > 0)
429 diag_east = x + (ind < (length - 1))
430 pos = (y * length) + x
431 if (y > 0 and diag_east
432 and type == chr(world_db["MAP"][pos - length + ind])) \
434 and type == chr(world_db["MAP"][pos + 1])) \
435 or (y < (length - 1) and diag_east
436 and type == chr(world_db["MAP"][pos + length + ind])) \
437 or (y > 0 and diag_west
438 and type == chr(world_db["MAP"][pos - length - (not ind)])) \
440 and type == chr(world_db["MAP"][pos - 1])) \
441 or (y < (length - 1) and diag_west
442 and type == chr(world_db["MAP"][pos + length - (not ind)])):
446 world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
447 length = world_db["MAP_LENGTH"]
448 add_half_width = (not (length % 2)) * int(length / 2)
449 world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
451 y = rand.next() % length
452 x = rand.next() % length
453 pos = (y * length) + x
454 if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
455 if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
457 world_db["MAP"][pos] = ord(".")
458 n_trees = int((length ** 2) / 16)
460 while (i_trees <= n_trees):
461 single_allowed = rand.next() % 32
462 y = rand.next() % length
463 x = rand.next() % length
464 pos = (y * length) + x
465 if "." == chr(world_db["MAP"][pos]) \
466 and ((not single_allowed) or is_neighbor((y, x), "X")):
467 world_db["MAP"][pos] = ord("X")
469 # This all-too-precise replica of the original C code misses iter_limit().
472 def update_map_memory(t, age_map=True):
473 """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
475 def age_some_memdepthmap_on_nonfov_cells():
476 # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
480 # for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
481 # if not ord_v == t["fovmap"][pos]
482 # if ord_0 <= t["T_MEMDEPTHMAP"][pos]
483 # if ord_9 > t["T_MEMDEPTHMAP"][pos]
484 # if not rand.next() % (2 **
485 # (t["T_MEMDEPTHMAP"][pos] - 48))]:
486 # t["T_MEMDEPTHMAP"][pos] += 1
487 memdepthmap = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
488 fovmap = c_pointer_to_bytearray(t["fovmap"])
489 libpr.age_some_memdepthmap_on_nonfov_cells(memdepthmap, fovmap)
491 if not t["T_MEMMAP"]:
492 t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
493 if not t["T_MEMDEPTHMAP"]:
494 t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
497 for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
498 if ord_v == t["fovmap"][pos]]:
499 t["T_MEMDEPTHMAP"][pos] = ord_0
500 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
502 age_some_memdepthmap_on_nonfov_cells()
503 t["T_MEMTHING"] = [mt for mt in t["T_MEMTHING"]
504 if ord_v != t["fovmap"][(mt[1] * world_db["MAP_LENGTH"])
506 for id in [id for id in world_db["Things"]
507 if not world_db["Things"][id]["carried"]]:
508 type = world_db["Things"][id]["T_TYPE"]
509 if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
510 y = world_db["Things"][id]["T_POSY"]
511 x = world_db["Things"][id]["T_POSX"]
512 if ord_v == t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]:
513 t["T_MEMTHING"].append((type, y, x))
516 def set_world_inactive():
517 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
519 if os.access(io_db["path_worldstate"], os.F_OK):
520 os.remove(io_db["path_worldstate"])
521 world_db["WORLD_ACTIVE"] = 0
524 def integer_test(val_string, min, max=None):
525 """Return val_string if integer >= min & (if max set) <= max, else None."""
527 val = int(val_string)
528 if val < min or (max is not None and val > max):
532 msg = "Ignoring: Please use integer >= " + str(min)
534 msg += " and <= " + str(max)
540 def setter(category, key, min, max=None):
541 """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
544 val = integer_test(val_string, min, max)
548 if category == "Thing":
549 id_store = command_tid
550 decorator = test_Thing_id
551 elif category == "ThingType":
552 id_store = command_ttid
553 decorator = test_ThingType_id
554 elif category == "ThingAction":
555 id_store = command_taid
556 decorator = test_ThingAction_id
560 val = integer_test(val_string, min, max)
562 world_db[category + "s"][id_store.id][key] = val
566 def build_fov_map(t):
567 """Build Thing's FOV map."""
568 t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
569 fovmap = c_pointer_to_bytearray(t["fovmap"])
570 map = c_pointer_to_bytearray(world_db["MAP"])
571 if libpr.build_fov_map(t["T_POSY"], t["T_POSX"], fovmap, map):
572 raise RuntimeError("Malloc error in build_fov_Map().")
576 """Send quick usage info to log."""
577 strong_write(io_db["file_out"], "LOG See README file for help.\n")
580 def decrement_lifepoints(t):
581 """Decrement t's lifepoints by 1, and if to zero, corpse it.
583 If t is the player avatar, only blank its fovmap, so that the client may
584 still display memory data. On non-player things, erase fovmap and memory.
585 Dying actors drop all their things.
587 t["T_LIFEPOINTS"] -= 1
588 if 0 == t["T_LIFEPOINTS"]:
589 for id in t["T_CARRIES"]:
590 t["T_CARRIES"].remove(id)
591 world_db["Things"][id]["T_POSY"] = t["T_POSY"]
592 world_db["Things"][id]["T_POSX"] = t["T_POSX"]
593 world_db["Things"][id]["carried"] = False
594 t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
595 if world_db["Things"][0] == t:
596 t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
597 strong_write(io_db["file_out"], "LOG You die.\n")
598 strong_write(io_db["file_out"],
599 "LOG See README on how to start over.\n")
602 t["T_MEMMAP"] = False
603 t["T_MEMDEPTHMAP"] = False
607 def mv_yx_in_dir_legal(dir, y, x):
608 """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
609 dir_c = dir.encode("ascii")[0]
610 test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
612 raise RuntimeError("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
613 return (test, libpr.result_y(), libpr.result_x())
617 """Make t do nothing (but loudly, if player avatar)."""
618 if t == world_db["Things"][0]:
619 strong_write(io_db["file_out"], "LOG You wait.\n")
623 """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
625 move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
626 t["T_POSY"], t["T_POSX"])
627 if 1 == move_result[0]:
628 pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
629 hitted = [id for id in world_db["Things"]
630 if world_db["Things"][id] != t
631 if world_db["Things"][id]["T_LIFEPOINTS"]
632 if world_db["Things"][id]["T_POSY"] == move_result[1]
633 if world_db["Things"][id]["T_POSX"] == move_result[2]]
636 if t == world_db["Things"][0]:
637 hitted_type = world_db["Things"][hit_id]["T_TYPE"]
638 hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
639 strong_write(io_db["file_out"], "LOG You wound "
640 + hitted_name + ".\n")
642 hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
643 strong_write(io_db["file_out"], "LOG " + hitter_name +
645 decrement_lifepoints(world_db["Things"][hit_id])
647 passable = "." == chr(world_db["MAP"][pos])
648 dir = [dir for dir in directions_db
649 if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
651 t["T_POSY"] = move_result[1]
652 t["T_POSX"] = move_result[2]
653 for id in t["T_CARRIES"]:
654 world_db["Things"][id]["T_POSY"] = move_result[1]
655 world_db["Things"][id]["T_POSX"] = move_result[2]
657 if t == world_db["Things"][0]:
658 strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
659 elif t == world_db["Things"][0]:
660 strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
663 def actor_pick_up(t):
664 """Make t pick up (topmost?) Thing from ground into inventory.
666 Define topmostness by how low the thing's type ID is.
668 ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
669 if not world_db["Things"][id]["carried"]
670 if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
671 if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
675 tid = world_db["Things"][iid]["T_TYPE"]
676 if lowest_tid == -1 or tid < lowest_tid:
679 world_db["Things"][id]["carried"] = True
680 t["T_CARRIES"].append(id)
681 if t == world_db["Things"][0]:
682 strong_write(io_db["file_out"], "LOG You pick up an object.\n")
683 elif t == world_db["Things"][0]:
684 err = "You try to pick up an object, but there is none."
685 strong_write(io_db["file_out"], "LOG " + err + "\n")
689 """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
690 # TODO: Handle case where T_ARGUMENT matches nothing.
691 if len(t["T_CARRIES"]):
692 id = t["T_CARRIES"][t["T_ARGUMENT"]]
693 t["T_CARRIES"].remove(id)
694 world_db["Things"][id]["carried"] = False
695 if t == world_db["Things"][0]:
696 strong_write(io_db["file_out"], "LOG You drop an object.\n")
697 elif t == world_db["Things"][0]:
698 err = "You try to drop an object, but you own none."
699 strong_write(io_db["file_out"], "LOG " + err + "\n")
703 """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
704 # TODO: Handle case where T_ARGUMENT matches nothing.
705 if len(t["T_CARRIES"]):
706 id = t["T_CARRIES"][t["T_ARGUMENT"]]
707 type = world_db["Things"][id]["T_TYPE"]
708 if world_db["ThingTypes"][type]["TT_TOOL"] == "food":
709 t["T_CARRIES"].remove(id)
710 del world_db["Things"][id]
711 t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_TOOLPOWER"]
712 if t == world_db["Things"][0]:
713 strong_write(io_db["file_out"],
714 "LOG You consume this object.\n")
715 elif t == world_db["Things"][0]:
716 strong_write(io_db["file_out"],
717 "LOG You try to use this object, but fail.\n")
718 elif t == world_db["Things"][0]:
719 strong_write(io_db["file_out"],
720 "LOG You try to use an object, but you own none.\n")
723 def thingproliferation(t, prol_map):
724 """To chance of 1/TT_PROLIFERATE, create t offspring in open neighbor cell.
726 Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be be
727 marked '.' in prol_map. If there are several map cell candidates, one is
730 prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
731 if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)):
733 for dir in [directions_db[key] for key in sorted(directions_db.keys())]:
734 mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
735 if mv_result[0] and ord('.') == prol_map[mv_result[1]
736 * world_db["MAP_LENGTH"]
738 candidates.append((mv_result[1], mv_result[2]))
740 i = rand.next() % len(candidates)
741 id = id_setter(-1, "Things")
742 newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
743 world_db["Things"][id] = newT
747 """Grow t's HP to a 1/32 chance if < HP max, satiation > 0, and waiting.
749 On success, decrease satiation score by 32.
751 if t["T_SATIATION"] > 0 \
752 and t["T_LIFEPOINTS"] < \
753 world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"] \
754 and 0 == (rand.next() % 31) \
755 and t["T_COMMAND"] == [id for id in world_db["ThingActions"]
756 if world_db["ThingActions"][id]["TA_NAME"] ==
758 t["T_LIFEPOINTS"] += 1
759 t["T_SATIATION"] -= 32
760 if t == world_db["Things"][0]:
761 strong_write(io_db["file_out"], "LOG You heal.\n")
765 """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
766 if t["T_SATIATION"] > -32768:
767 max_hp = world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]
768 t["T_SATIATION"] -= int(math.sqrt(max_hp))
769 if 0 != t["T_SATIATION"] and 0 == int(rand.next() / abs(t["T_SATIATION"])):
770 if t == world_db["Things"][0]:
771 if t["T_SATIATION"] < 0:
772 strong_write(io_db["file_out"], "LOG You suffer from hunger.\n")
774 strong_write(io_db["file_out"],
775 "LOG You suffer from over-eating.\n")
776 decrement_lifepoints(t)
779 def get_dir_to_target(t, filter):
780 """Try to set T_COMMAND/T_ARGUMENT for move to "filter"-determined target.
782 The path-wise nearest target is chosen, via the shortest available path.
783 Target must not be t. On succcess, return positive value, else False.
785 "a": Thing in FOV is below a certain distance, animate, but of ThingType
786 that is not t's, and starts out weaker than t is; build path as
787 avoiding things of t's ThingType
788 "f": neighbor cell (not inhabited by any animate Thing) further away from
789 animate Thing not further than x steps away and in FOV and of a
790 ThingType that is not t's, and starts out stronger or as strong as t
791 is currently; or (cornered), if no such flight cell, but Thing of
792 above criteria is too near,1 a cell closer to it, or, if less near,
794 "c": Thing in memorized map is consumable
795 "s": memory map cell with greatest-reachable degree of unexploredness
798 def zero_score_map_where_char_on_memdepthmap(c):
799 # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
800 # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
801 # if t["T_MEMDEPTHMAP"][i] == mem_depth_c[0]]:
802 # set_map_score(i, 0)
803 map = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
804 if libpr.zero_score_map_where_char_on_memdepthmap(c, map):
805 raise RuntimeError("No score map allocated for "
806 "zero_score_map_where_char_on_memdepthmap().")
808 def set_map_score(pos, score):
809 test = libpr.set_map_score(pos, score)
811 raise RuntimeError("No score map allocated for set_map_score().")
813 def get_map_score(pos):
814 result = libpr.get_map_score(pos)
816 raise RuntimeError("No score map allocated for get_map_score().")
820 if t["fovmap"] and ("a" == filter or "f" == filter):
821 for id in world_db["Things"]:
822 Thing = world_db["Things"][id]
823 if Thing != t and Thing["T_LIFEPOINTS"] and \
824 t["T_TYPE"] != Thing["T_TYPE"] and \
825 'v' == chr(t["fovmap"][(Thing["T_POSY"]
826 * world_db["MAP_LENGTH"])
828 ThingType = world_db["ThingTypes"][Thing["T_TYPE"]]
829 if ("f" == filter and ThingType["TT_LIFEPOINTS"] >=
831 or ("a" == filter and ThingType["TT_LIFEPOINTS"] <
834 elif t["T_MEMMAP"] and "c" == filter:
835 for mt in t["T_MEMTHING"]:
836 if ' ' != chr(t["T_MEMMAP"][(mt[1] * world_db["MAP_LENGTH"])
838 and world_db["ThingTypes"][mt[0]]["TT_TOOL"] == "food":
842 def set_cells_passable_on_memmap_to_65534_on_scoremap():
843 # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
845 # memmap = t["T_MEMMAP"]
846 # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
847 # if ord_dot == memmap[i]]:
848 # set_map_score(i, 65534) # i.e. 65535-1
849 map = c_pointer_to_bytearray(t["T_MEMMAP"])
850 if libpr.set_cells_passable_on_memmap_to_65534_on_scoremap(map):
851 raise RuntimeError("No score map allocated for set_cells_passable"
852 "_on_memmap_to_65534_on_scoremap().")
854 def init_score_map():
855 test = libpr.init_score_map()
857 raise RuntimeError("Malloc error in init_score_map().")
860 set_cells_passable_on_memmap_to_65534_on_scoremap()
862 for id in world_db["Things"]:
863 Thing = world_db["Things"][id]
864 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
866 if t != Thing and Thing["T_LIFEPOINTS"] and \
867 t["T_TYPE"] != Thing["T_TYPE"] and \
868 ord_v == t["fovmap"][pos] and \
869 t["T_LIFEPOINTS"] > \
870 world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
871 set_map_score(pos, 0)
872 elif t["T_TYPE"] == Thing["T_TYPE"]:
873 set_map_score(pos, 65535)
875 for id in [id for id in world_db["Things"]
876 if world_db["Things"][id]["T_LIFEPOINTS"]]:
877 Thing = world_db["Things"][id]
878 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
880 if t["T_TYPE"] != Thing["T_TYPE"] and \
881 ord_v == t["fovmap"][pos] and \
882 t["T_LIFEPOINTS"] <= \
883 world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
884 set_map_score(pos, 0)
886 for mt in [mt for mt in t["T_MEMTHING"]
887 if ord_blank != t["T_MEMMAP"][mt[1]
888 * world_db["MAP_LENGTH"]
890 if world_db["ThingTypes"][mt[0]]["TT_TOOL"] == "food"]:
891 set_map_score(mt[1] * world_db["MAP_LENGTH"] + mt[2], 0)
893 zero_score_map_where_char_on_memdepthmap(mem_depth_c[0])
895 def rand_target_dir(neighbors, cmp, dirs):
898 for i in range(len(dirs)):
899 if cmp == neighbors[i]:
900 candidates.append(dirs[i])
902 return candidates[rand.next() % n_candidates] if n_candidates else 0
904 def get_neighbor_scores(dirs, eye_pos):
906 if libpr.ready_neighbor_scores(eye_pos):
907 raise RuntimeError("No score map allocated for " +
908 "ready_neighbor_scores.()")
909 for i in range(len(dirs)):
910 scores.append(libpr.get_neighbor_score(i))
913 def get_dir_from_neighbors():
914 dir_to_target = False
916 eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
917 neighbors = get_neighbor_scores(dirs, eye_pos)
919 inhabited = [world_db["Things"][id]["T_POSY"]
920 * world_db["MAP_LENGTH"]
921 + world_db["Things"][id]["T_POSX"]
922 for id in world_db["Things"]
923 if world_db["Things"][id]["T_LIFEPOINTS"]]
924 for i in range(len(dirs)):
925 mv_yx_in_dir_legal(dirs[i], t["T_POSY"], t["T_POSX"])
926 pos_cmp = libpr.result_y() * world_db["MAP_LENGTH"] \
928 for pos in [pos for pos in inhabited if pos == pos_cmp]:
931 minmax_start = 0 if "f" == filter else 65535 - 1
932 minmax_neighbor = minmax_start
933 for i in range(len(dirs)):
934 if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
935 minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
936 or ("f" != filter and minmax_neighbor > neighbors[i]):
937 minmax_neighbor = neighbors[i]
938 if minmax_neighbor != minmax_start:
939 dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
941 if not dir_to_target:
942 if 1 == get_map_score(eye_pos):
943 dir_to_target = rand_target_dir(neighbors, 0, dirs)
944 elif 3 >= get_map_score(eye_pos):
945 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
947 world_db["ThingActions"][id]["TA_NAME"]
950 elif dir_to_target and 3 < get_map_score(eye_pos):
952 elif "a" == filter and 10 <= get_map_score(eye_pos):
956 dir_to_target = False
958 run_i = 9 + 1 if "s" == filter else 1
959 while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
962 mem_depth_c = b'9' if b' ' == mem_depth_c \
963 else bytes([mem_depth_c[0] - 1])
964 if libpr.dijkstra_map():
965 raise RuntimeError("No score map allocated for dijkstra_map().")
966 dir_to_target = get_dir_from_neighbors()
967 libpr.free_score_map()
968 if dir_to_target and str == type(dir_to_target):
969 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
970 if world_db["ThingActions"][id]["TA_NAME"]
972 t["T_ARGUMENT"] = ord(dir_to_target)
976 def standing_on_food(t):
977 """Return True/False whether t is standing on a consumable."""
978 for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
979 if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
980 if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
981 if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
982 ["TT_TOOL"] == "food"]:
987 def get_inventory_slot_to_consume(t):
988 """Return slot Id of strongest consumable in t's inventory, else -1."""
992 for id in t["T_CARRIES"]:
993 type = world_db["Things"][id]["T_TYPE"]
994 if world_db["ThingTypes"][type]["TT_TOOL"] == "food" \
995 and world_db["ThingTypes"][type]["TT_TOOLPOWER"] > cmp_food:
996 cmp_food = world_db["ThingTypes"][type]["TT_TOOLPOWER"]
1003 """Determine next command/argment for actor t via AI algorithms.
1005 AI will look for, and move towards, enemies (animate Things not of their
1006 own ThingType); if they see none, they will consume consumables in their
1007 inventory; if there are none, they will pick up what they stand on if they
1008 stand on consumables; if they stand on none, they will move towards the
1009 next consumable they see or remember on the map; if they see or remember
1010 none, they will explore parts of the map unseen since ever or for at least
1011 one turn; if there is nothing to explore, they will simply wait.
1013 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1014 if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1015 if not get_dir_to_target(t, "f"):
1016 sel = get_inventory_slot_to_consume(t)
1018 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1019 if world_db["ThingActions"][id]["TA_NAME"]
1021 t["T_ARGUMENT"] = sel
1022 elif standing_on_food(t):
1023 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1024 if world_db["ThingActions"][id]["TA_NAME"]
1026 elif (not get_dir_to_target(t, "c")) and \
1027 (not get_dir_to_target(t, "a")):
1028 get_dir_to_target(t, "s")
1032 """Run game world and its inhabitants until new player input expected."""
1034 whilebreaker = False
1035 while world_db["Things"][0]["T_LIFEPOINTS"]:
1036 proliferable_map = world_db["MAP"][:]
1037 for id in [id for id in world_db["Things"]
1038 if not world_db["Things"][id]["carried"]]:
1039 y = world_db["Things"][id]["T_POSY"]
1040 x = world_db["Things"][id]["T_POSX"]
1041 proliferable_map[y * world_db["MAP_LENGTH"] + x] = ord('X')
1042 for id in [id for id in world_db["Things"]]: # Only what's from start!
1043 if not id in world_db["Things"] or \
1044 world_db["Things"][id]["carried"]: # May have been consumed or
1045 continue # picked up during turn …
1046 Thing = world_db["Things"][id]
1047 if Thing["T_LIFEPOINTS"]:
1048 if not Thing["T_COMMAND"]:
1049 update_map_memory(Thing)
1056 if Thing["T_LIFEPOINTS"]:
1057 Thing["T_PROGRESS"] += 1
1058 taid = [a for a in world_db["ThingActions"]
1059 if a == Thing["T_COMMAND"]][0]
1060 ThingAction = world_db["ThingActions"][taid]
1061 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
1062 eval("actor_" + ThingAction["TA_NAME"])(Thing)
1063 Thing["T_COMMAND"] = 0
1064 Thing["T_PROGRESS"] = 0
1065 thingproliferation(Thing, proliferable_map)
1068 world_db["TURN"] += 1
1071 def new_Thing(type, pos=(0, 0)):
1072 """Return Thing of type T_TYPE, with fovmap if alive and world active."""
1074 "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
1086 "T_MEMDEPTHMAP": False,
1089 if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
1090 build_fov_map(thing)
1094 def id_setter(id, category, id_store=False, start_at_1=False):
1095 """Set ID of object of category to manipulate ID unused? Create new one.
1096 The ID is stored as id_store.id (if id_store is set). If the integer of the
1097 input is valid (if start_at_1, >= 0, else >= -1), but <0 or (if start_at_1)
1098 <1, calculate new ID: lowest unused ID >=0 or (if start_at_1) >= 1. None is
1099 always returned when no new object is created, else the new object's ID.
1101 min = 0 if start_at_1 else -1
1103 id = integer_test(id, min)
1105 if id in world_db[category]:
1110 if (start_at_1 and 0 == id) \
1111 or ((not start_at_1) and (id < 0)):
1112 id = 0 if start_at_1 else -1
1115 if id not in world_db[category]:
1123 """Send PONG line to server output file."""
1124 strong_write(io_db["file_out"], "PONG\n")
1128 """Abort server process."""
1129 if None == opts.replay:
1130 if world_db["WORLD_ACTIVE"]:
1132 atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True)
1133 raise SystemExit("received QUIT command")
1136 def command_thingshere(str_y, str_x):
1137 """Write to out file list of Things known to player at coordinate y, x."""
1138 if world_db["WORLD_ACTIVE"]:
1139 y = integer_test(str_y, 0, 255)
1140 x = integer_test(str_x, 0, 255)
1141 length = world_db["MAP_LENGTH"]
1142 if None != y and None != x and y < length and x < length:
1143 pos = (y * world_db["MAP_LENGTH"]) + x
1144 strong_write(io_db["file_out"], "THINGS_HERE START\n")
1145 if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
1146 for id in [id for tid in sorted(list(world_db["ThingTypes"]))
1147 for id in world_db["Things"]
1148 if not world_db["Things"][id]["carried"]
1149 if world_db["Things"][id]["T_TYPE"] == tid
1150 if y == world_db["Things"][id]["T_POSY"]
1151 if x == world_db["Things"][id]["T_POSX"]]:
1152 type = world_db["Things"][id]["T_TYPE"]
1153 name = world_db["ThingTypes"][type]["TT_NAME"]
1154 strong_write(io_db["file_out"], name + "\n")
1156 for mt in [mt for tid in sorted(list(world_db["ThingTypes"]))
1157 for mt in world_db["Things"][0]["T_MEMTHING"]
1158 if mt[0] == tid if y == mt[1] if x == mt[2]]:
1159 name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
1160 strong_write(io_db["file_out"], name + "\n")
1161 strong_write(io_db["file_out"], "THINGS_HERE END\n")
1163 print("Ignoring: Invalid map coordinates.")
1165 print("Ignoring: Command only works on existing worlds.")
1168 def play_commander(action, args=False):
1169 """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
1171 T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
1175 id = [x for x in world_db["ThingActions"]
1176 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
1177 world_db["Things"][0]["T_COMMAND"] = id
1180 def set_command_and_argument_int(str_arg):
1181 val = integer_test(str_arg, 0, 255)
1183 world_db["Things"][0]["T_ARGUMENT"] = val
1186 def set_command_and_argument_movestring(str_arg):
1187 if str_arg in directions_db:
1188 world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
1191 print("Ignoring: Argument must be valid direction string.")
1193 if action == "move":
1194 return set_command_and_argument_movestring
1196 return set_command_and_argument_int
1201 def command_seedrandomness(seed_string):
1202 """Set rand seed to int(seed_string)."""
1203 val = integer_test(seed_string, 0, 4294967295)
1208 def command_makeworld(seed_string):
1209 """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
1211 Seed rand with seed. Do more only with a "wait" ThingAction and
1212 world["PLAYER_TYPE"] matching ThingType of TT_START_NUMBER > 0. Then,
1213 world_db["Things"] emptied, call make_map() and set
1214 world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1215 according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1216 of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1217 other. Init player's memory map. Write "NEW_WORLD" line to out file.
1224 err = "Space to put thing on too hard to find. Map too small?"
1226 y = rand.next() % world_db["MAP_LENGTH"]
1227 x = rand.next() % world_db["MAP_LENGTH"]
1228 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
1232 raise SystemExit(err)
1233 # Replica of C code, wrongly ignores animatedness of new Thing.
1234 pos_clear = (0 == len([id for id in world_db["Things"]
1235 if world_db["Things"][id]["T_LIFEPOINTS"]
1236 if world_db["Things"][id]["T_POSY"] == y
1237 if world_db["Things"][id]["T_POSX"] == x]))
1242 val = integer_test(seed_string, 0, 4294967295)
1246 player_will_be_generated = False
1247 playertype = world_db["PLAYER_TYPE"]
1248 for ThingType in world_db["ThingTypes"]:
1249 if playertype == ThingType:
1250 if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1251 player_will_be_generated = True
1253 if not player_will_be_generated:
1254 print("Ignoring: No player type with start number >0 defined.")
1257 for ThingAction in world_db["ThingActions"]:
1258 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1261 print("Ignoring beyond SEED_MAP: " +
1262 "No thing action with name 'wait' defined.")
1264 world_db["Things"] = {}
1266 world_db["WORLD_ACTIVE"] = 1
1267 world_db["TURN"] = 1
1268 for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1269 id = id_setter(-1, "Things")
1270 world_db["Things"][id] = new_Thing(playertype, free_pos())
1271 if not world_db["Things"][0]["fovmap"]:
1272 empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1273 world_db["Things"][0]["fovmap"] = empty_fovmap
1274 update_map_memory(world_db["Things"][0])
1275 for type in world_db["ThingTypes"]:
1276 for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1277 if type != playertype:
1278 id = id_setter(-1, "Things")
1279 world_db["Things"][id] = new_Thing(type, free_pos())
1280 strong_write(io_db["file_out"], "NEW_WORLD\n")
1284 def command_maplength(maplength_string):
1285 """Redefine map length. Invalidate map, therefore lose all things on it."""
1286 val = integer_test(maplength_string, 1, 256)
1288 world_db["MAP_LENGTH"] = val
1289 world_db["MAP"] = False
1290 set_world_inactive()
1291 world_db["Things"] = {}
1292 libpr.set_maplength(val)
1295 def command_worldactive(worldactive_string):
1296 """Toggle world_db["WORLD_ACTIVE"] if possible.
1298 An active world can always be set inactive. An inactive world can only be
1299 set active with a "wait" ThingAction, and a player Thing (of ID 0), and a
1300 map. On activation, rebuild all Things' FOVs, and the player's map memory.
1301 Also call log_help().
1303 val = integer_test(worldactive_string, 0, 1)
1305 if 0 != world_db["WORLD_ACTIVE"]:
1307 set_world_inactive()
1309 print("World already active.")
1310 elif 0 == world_db["WORLD_ACTIVE"]:
1312 for ThingAction in world_db["ThingActions"]:
1313 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1316 player_exists = False
1317 for Thing in world_db["Things"]:
1319 player_exists = True
1321 if wait_exists and player_exists and world_db["MAP"]:
1322 for id in world_db["Things"]:
1323 if world_db["Things"][id]["T_LIFEPOINTS"]:
1324 build_fov_map(world_db["Things"][id])
1326 update_map_memory(world_db["Things"][id], False)
1327 if not world_db["Things"][0]["T_LIFEPOINTS"]:
1328 empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1329 world_db["Things"][0]["fovmap"] = empty_fovmap
1330 world_db["WORLD_ACTIVE"] = 1
1333 print("Ignoring: Not all conditions for world activation met.")
1336 def test_for_id_maker(object, category):
1337 """Return decorator testing for object having "id" attribute."""
1340 if hasattr(object, "id"):
1343 print("Ignoring: No " + category +
1344 " defined to manipulate yet.")
1349 def command_tid(id_string):
1350 """Set ID of Thing to manipulate. ID unused? Create new one.
1352 Default new Thing's type to the first available ThingType, others: zero.
1354 id = id_setter(id_string, "Things", command_tid)
1356 if world_db["ThingTypes"] == {}:
1357 print("Ignoring: No ThingType to settle new Thing in.")
1359 type = list(world_db["ThingTypes"].keys())[0]
1360 world_db["Things"][id] = new_Thing(type)
1363 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1367 def command_tcommand(str_int):
1368 """Set T_COMMAND of selected Thing."""
1369 val = integer_test(str_int, 0)
1371 if 0 == val or val in world_db["ThingActions"]:
1372 world_db["Things"][command_tid.id]["T_COMMAND"] = val
1374 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1378 def command_ttype(str_int):
1379 """Set T_TYPE of selected Thing."""
1380 val = integer_test(str_int, 0)
1382 if val in world_db["ThingTypes"]:
1383 world_db["Things"][command_tid.id]["T_TYPE"] = val
1385 print("Ignoring: ThingType ID belongs to no known ThingType.")
1389 def command_tcarries(str_int):
1390 """Append int(str_int) to T_CARRIES of selected Thing.
1392 The ID int(str_int) must not be of the selected Thing, and must belong to a
1393 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1395 val = integer_test(str_int, 0)
1397 if val == command_tid.id:
1398 print("Ignoring: Thing cannot carry itself.")
1399 elif val in world_db["Things"] \
1400 and not world_db["Things"][val]["carried"]:
1401 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1402 world_db["Things"][val]["carried"] = True
1404 print("Ignoring: Thing not available for carrying.")
1405 # Note that the whole carrying structure is different from the C version:
1406 # Carried-ness is marked by a "carried" flag, not by Things containing
1407 # Things internally.
1411 def command_tmemthing(str_t, str_y, str_x):
1412 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1414 The type must fit to an existing ThingType, and the position into the map.
1416 type = integer_test(str_t, 0)
1417 posy = integer_test(str_y, 0, 255)
1418 posx = integer_test(str_x, 0, 255)
1419 if None != type and None != posy and None != posx:
1420 if type not in world_db["ThingTypes"] \
1421 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1422 print("Ignoring: Illegal value for thing type or position.")
1424 memthing = (type, posy, posx)
1425 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1428 def setter_map(maptype):
1429 """Set (world or Thing's) map of maptype's int(str_int)-th line to mapline.
1431 If no map of maptype exists yet, initialize it with ' ' bytes first.
1434 def valid_map_line(str_int, mapline):
1435 val = integer_test(str_int, 0, 255)
1437 if val >= world_db["MAP_LENGTH"]:
1438 print("Illegal value for map line number.")
1439 elif len(mapline) != world_db["MAP_LENGTH"]:
1440 print("Map line length is unequal map width.")
1445 def nonThingMap_helper(str_int, mapline):
1446 val = valid_map_line(str_int, mapline)
1448 length = world_db["MAP_LENGTH"]
1449 if not world_db["MAP"]:
1450 map = bytearray(b' ' * (length ** 2))
1452 map = world_db["MAP"]
1453 map[val * length:(val * length) + length] = mapline.encode()
1454 if not world_db["MAP"]:
1455 world_db["MAP"] = map
1458 def ThingMap_helper(str_int, mapline):
1459 val = valid_map_line(str_int, mapline)
1461 length = world_db["MAP_LENGTH"]
1462 if not world_db["Things"][command_tid.id][maptype]:
1463 map = bytearray(b' ' * (length ** 2))
1465 map = world_db["Things"][command_tid.id][maptype]
1466 map[val * length:(val * length) + length] = mapline.encode()
1467 if not world_db["Things"][command_tid.id][maptype]:
1468 world_db["Things"][command_tid.id][maptype] = map
1470 return nonThingMap_helper if maptype == "MAP" else ThingMap_helper
1474 def setter_tpos(axis):
1475 """Generate setter for T_POSX or T_POSY of selected Thing.
1477 If world is active, rebuilds animate things' fovmap, player's memory map.
1480 def helper(str_int):
1481 val = integer_test(str_int, 0, 255)
1483 if val < world_db["MAP_LENGTH"]:
1484 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1485 if world_db["WORLD_ACTIVE"] \
1486 and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1487 build_fov_map(world_db["Things"][command_tid.id])
1488 if 0 == command_tid.id:
1489 update_map_memory(world_db["Things"][command_tid.id])
1491 print("Ignoring: Position is outside of map.")
1495 def command_ttid(id_string):
1496 """Set ID of ThingType to manipulate. ID unused? Create new one.
1498 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, TT_TOOL to
1501 id = id_setter(id_string, "ThingTypes", command_ttid)
1503 world_db["ThingTypes"][id] = {
1504 "TT_NAME": "(none)",
1507 "TT_PROLIFERATE": 0,
1508 "TT_START_NUMBER": 0,
1515 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1519 def command_ttname(name):
1520 """Set TT_NAME of selected ThingType."""
1521 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1525 def command_tttool(name):
1526 """Set TT_TOOL of selected ThingType."""
1527 world_db["ThingTypes"][command_ttid.id]["TT_TOOL"] = name
1531 def command_ttsymbol(char):
1532 """Set TT_SYMBOL of selected ThingType. """
1534 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1536 print("Ignoring: Argument must be single character.")
1540 def command_ttcorpseid(str_int):
1541 """Set TT_CORPSE_ID of selected ThingType."""
1542 val = integer_test(str_int, 0)
1544 if val in world_db["ThingTypes"]:
1545 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1547 print("Ignoring: Corpse ID belongs to no known ThignType.")
1550 def command_taid(id_string):
1551 """Set ID of ThingAction to manipulate. ID unused? Create new one.
1553 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1555 id = id_setter(id_string, "ThingActions", command_taid, True)
1557 world_db["ThingActions"][id] = {
1563 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1566 @test_ThingAction_id
1567 def command_taname(name):
1568 """Set TA_NAME of selected ThingAction.
1570 The name must match a valid thing action function. If after the name
1571 setting no ThingAction with name "wait" remains, call set_world_inactive().
1573 if name == "wait" or name == "move" or name == "use" or name == "drop" \
1574 or name == "pick_up":
1575 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1576 if 1 == world_db["WORLD_ACTIVE"]:
1577 wait_defined = False
1578 for id in world_db["ThingActions"]:
1579 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1582 if not wait_defined:
1583 set_world_inactive()
1585 print("Ignoring: Invalid action name.")
1586 # In contrast to the original,naming won't map a function to a ThingAction.
1590 """Call ai() on player Thing, then turn_over()."""
1591 ai(world_db["Things"][0])
1595 """Commands database.
1597 Map command start tokens to ([0]) number of expected command arguments, ([1])
1598 the command's meta-ness (i.e. is it to be written to the record file, is it to
1599 be ignored in replay mode if read from server input file), and ([2]) a function
1603 "QUIT": (0, True, command_quit),
1604 "PING": (0, True, command_ping),
1605 "THINGS_HERE": (2, True, command_thingshere),
1606 "MAKE_WORLD": (1, False, command_makeworld),
1607 "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1608 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1609 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0)),
1610 "MAP_LENGTH": (1, False, command_maplength),
1611 "WORLD_ACTIVE": (1, False, command_worldactive),
1612 "MAP": (2, False, setter_map("MAP")),
1613 "TA_ID": (1, False, command_taid),
1614 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1615 "TA_NAME": (1, False, command_taname),
1616 "TT_ID": (1, False, command_ttid),
1617 "TT_NAME": (1, False, command_ttname),
1618 "TT_TOOL": (1, False, command_tttool),
1619 "TT_SYMBOL": (1, False, command_ttsymbol),
1620 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1621 "TT_TOOLPOWER": (1, False, setter("ThingType", "TT_TOOLPOWER", 0, 65535)),
1622 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1624 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1626 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1627 "T_ID": (1, False, command_tid),
1628 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1629 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1630 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1631 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1632 "T_COMMAND": (1, False, command_tcommand),
1633 "T_TYPE": (1, False, command_ttype),
1634 "T_CARRIES": (1, False, command_tcarries),
1635 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1636 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1637 "T_MEMTHING": (3, False, command_tmemthing),
1638 "T_POSY": (1, False, setter_tpos("Y")),
1639 "T_POSX": (1, False, setter_tpos("X")),
1640 "wait": (0, False, play_commander("wait")),
1641 "move": (1, False, play_commander("move")),
1642 "pick_up": (0, False, play_commander("pick_up")),
1643 "drop": (1, False, play_commander("drop", True)),
1644 "use": (1, False, play_commander("use", True)),
1645 "ai": (0, False, command_ai)
1647 # TODO: Unhandled cases: (Un-)killing animates (esp. player!) with T_LIFEPOINTS.
1650 """World state database. With sane default values. (Randomness is in rand.)"""
1662 """Mapping of direction names to internal direction chars."""
1663 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1664 "west": "s", "north-west": "w", "north-east": "e"}
1666 """File IO database."""
1668 "path_save": "save",
1669 "path_record": "record_save",
1670 "path_worldconf": "confserver/world",
1671 "path_server": "server/",
1672 "path_in": "server/in",
1673 "path_out": "server/out",
1674 "path_worldstate": "server/worldstate",
1675 "tmp_suffix": "_tmp",
1676 "kicked_by_rival": False,
1677 "worldstate_updateable": False
1682 libpr = prep_library()
1683 rand = RandomnessIO()
1684 opts = parse_command_line_arguments()
1686 io_db["path_save"] = opts.savefile
1687 io_db["path_record"] = "record_" + opts.savefile
1690 io_db["verbose"] = True
1691 if None != opts.replay:
1695 except SystemExit as exit:
1696 print("ABORTING: " + exit.args[0])
1698 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")