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 """If t's HP < max, increment them if well-nourished, maybe waiting."""
748 if t["T_LIFEPOINTS"] < \
749 world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]:
750 wait_id = [id for id in world_db["ThingActions"]
751 if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
752 wait_divider = 8 if t["T_COMMAND"] == wait_id else 1
753 testval = int(abs(t["T_SATIATION"]) / wait_divider)
754 if (testval <= 1 or 1 == (rand.next() % testval)):
755 t["T_LIFEPOINTS"] += 1
756 if t == world_db["Things"][0]:
757 strong_write(io_db["file_out"], "LOG You heal.\n")
760 """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
761 if t["T_SATIATION"] > -32768:
762 max_hp = world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]
763 t["T_SATIATION"] -= int(math.sqrt(max_hp))
764 if 0 != t["T_SATIATION"] and 0 == int(rand.next() / abs(t["T_SATIATION"])):
765 if t == world_db["Things"][0]:
766 if t["T_SATIATION"] < 0:
767 strong_write(io_db["file_out"], "LOG You suffer from hunger.\n")
769 strong_write(io_db["file_out"],
770 "LOG You suffer from over-eating.\n")
771 decrement_lifepoints(t)
774 def get_dir_to_target(t, filter):
775 """Try to set T_COMMAND/T_ARGUMENT for move to "filter"-determined target.
777 The path-wise nearest target is chosen, via the shortest available path.
778 Target must not be t. On succcess, return positive value, else False.
780 "a": Thing in FOV is below a certain distance, animate, but of ThingType
781 that is not t's, and starts out weaker than t is; build path as
782 avoiding things of t's ThingType
783 "f": neighbor cell (not inhabited by any animate Thing) further away from
784 animate Thing not further than x steps away and in FOV and of a
785 ThingType that is not t's, and starts out stronger or as strong as t
786 is currently; or (cornered), if no such flight cell, but Thing of
787 above criteria is too near,1 a cell closer to it, or, if less near,
789 "c": Thing in memorized map is consumable
790 "s": memory map cell with greatest-reachable degree of unexploredness
793 def zero_score_map_where_char_on_memdepthmap(c):
794 # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
795 # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
796 # if t["T_MEMDEPTHMAP"][i] == mem_depth_c[0]]:
797 # set_map_score(i, 0)
798 map = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
799 if libpr.zero_score_map_where_char_on_memdepthmap(c, map):
800 raise RuntimeError("No score map allocated for "
801 "zero_score_map_where_char_on_memdepthmap().")
803 def set_map_score(pos, score):
804 test = libpr.set_map_score(pos, score)
806 raise RuntimeError("No score map allocated for set_map_score().")
808 def get_map_score(pos):
809 result = libpr.get_map_score(pos)
811 raise RuntimeError("No score map allocated for get_map_score().")
815 if t["fovmap"] and ("a" == filter or "f" == filter):
816 for id in world_db["Things"]:
817 Thing = world_db["Things"][id]
818 if Thing != t and Thing["T_LIFEPOINTS"] and \
819 t["T_TYPE"] != Thing["T_TYPE"] and \
820 'v' == chr(t["fovmap"][(Thing["T_POSY"]
821 * world_db["MAP_LENGTH"])
823 ThingType = world_db["ThingTypes"][Thing["T_TYPE"]]
824 if ("f" == filter and ThingType["TT_LIFEPOINTS"] >=
826 or ("a" == filter and ThingType["TT_LIFEPOINTS"] <
829 elif t["T_MEMMAP"] and "c" == filter:
830 for mt in t["T_MEMTHING"]:
831 if ' ' != chr(t["T_MEMMAP"][(mt[1] * world_db["MAP_LENGTH"])
833 and world_db["ThingTypes"][mt[0]]["TT_TOOL"] == "food":
837 def set_cells_passable_on_memmap_to_65534_on_scoremap():
838 # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
840 # memmap = t["T_MEMMAP"]
841 # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
842 # if ord_dot == memmap[i]]:
843 # set_map_score(i, 65534) # i.e. 65535-1
844 map = c_pointer_to_bytearray(t["T_MEMMAP"])
845 if libpr.set_cells_passable_on_memmap_to_65534_on_scoremap(map):
846 raise RuntimeError("No score map allocated for set_cells_passable"
847 "_on_memmap_to_65534_on_scoremap().")
849 def init_score_map():
850 test = libpr.init_score_map()
852 raise RuntimeError("Malloc error in init_score_map().")
855 set_cells_passable_on_memmap_to_65534_on_scoremap()
857 for id in world_db["Things"]:
858 Thing = world_db["Things"][id]
859 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
861 if t != Thing and Thing["T_LIFEPOINTS"] and \
862 t["T_TYPE"] != Thing["T_TYPE"] and \
863 ord_v == t["fovmap"][pos] and \
864 t["T_LIFEPOINTS"] > \
865 world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
866 set_map_score(pos, 0)
867 elif t["T_TYPE"] == Thing["T_TYPE"]:
868 set_map_score(pos, 65535)
870 for id in [id for id in world_db["Things"]
871 if world_db["Things"][id]["T_LIFEPOINTS"]]:
872 Thing = world_db["Things"][id]
873 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
875 if t["T_TYPE"] != Thing["T_TYPE"] and \
876 ord_v == t["fovmap"][pos] and \
877 t["T_LIFEPOINTS"] <= \
878 world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
879 set_map_score(pos, 0)
881 for mt in [mt for mt in t["T_MEMTHING"]
882 if ord_blank != t["T_MEMMAP"][mt[1]
883 * world_db["MAP_LENGTH"]
885 if world_db["ThingTypes"][mt[0]]["TT_TOOL"] == "food"]:
886 set_map_score(mt[1] * world_db["MAP_LENGTH"] + mt[2], 0)
888 zero_score_map_where_char_on_memdepthmap(mem_depth_c[0])
890 def rand_target_dir(neighbors, cmp, dirs):
893 for i in range(len(dirs)):
894 if cmp == neighbors[i]:
895 candidates.append(dirs[i])
897 return candidates[rand.next() % n_candidates] if n_candidates else 0
899 def get_neighbor_scores(dirs, eye_pos):
901 if libpr.ready_neighbor_scores(eye_pos):
902 raise RuntimeError("No score map allocated for " +
903 "ready_neighbor_scores.()")
904 for i in range(len(dirs)):
905 scores.append(libpr.get_neighbor_score(i))
908 def get_dir_from_neighbors():
909 dir_to_target = False
911 eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
912 neighbors = get_neighbor_scores(dirs, eye_pos)
914 inhabited = [world_db["Things"][id]["T_POSY"]
915 * world_db["MAP_LENGTH"]
916 + world_db["Things"][id]["T_POSX"]
917 for id in world_db["Things"]
918 if world_db["Things"][id]["T_LIFEPOINTS"]]
919 for i in range(len(dirs)):
920 mv_yx_in_dir_legal(dirs[i], t["T_POSY"], t["T_POSX"])
921 pos_cmp = libpr.result_y() * world_db["MAP_LENGTH"] \
923 for pos in [pos for pos in inhabited if pos == pos_cmp]:
926 minmax_start = 0 if "f" == filter else 65535 - 1
927 minmax_neighbor = minmax_start
928 for i in range(len(dirs)):
929 if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
930 minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
931 or ("f" != filter and minmax_neighbor > neighbors[i]):
932 minmax_neighbor = neighbors[i]
933 if minmax_neighbor != minmax_start:
934 dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
936 if not dir_to_target:
937 if 1 == get_map_score(eye_pos):
938 dir_to_target = rand_target_dir(neighbors, 0, dirs)
939 elif 3 >= get_map_score(eye_pos):
940 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
942 world_db["ThingActions"][id]["TA_NAME"]
945 elif dir_to_target and 3 < get_map_score(eye_pos):
947 elif "a" == filter and 10 <= get_map_score(eye_pos):
951 dir_to_target = False
953 run_i = 9 + 1 if "s" == filter else 1
954 while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
957 mem_depth_c = b'9' if b' ' == mem_depth_c \
958 else bytes([mem_depth_c[0] - 1])
959 if libpr.dijkstra_map():
960 raise RuntimeError("No score map allocated for dijkstra_map().")
961 dir_to_target = get_dir_from_neighbors()
962 libpr.free_score_map()
963 if dir_to_target and str == type(dir_to_target):
964 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
965 if world_db["ThingActions"][id]["TA_NAME"]
967 t["T_ARGUMENT"] = ord(dir_to_target)
971 def standing_on_food(t):
972 """Return True/False whether t is standing on a consumable."""
973 for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
974 if not world_db["Things"][id]["carried"]
975 if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
976 if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
977 if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
978 ["TT_TOOL"] == "food"]:
983 def get_inventory_slot_to_consume(t):
984 """Return slot Id of strongest consumable in t's inventory, else -1."""
988 for id in t["T_CARRIES"]:
989 type = world_db["Things"][id]["T_TYPE"]
990 if world_db["ThingTypes"][type]["TT_TOOL"] == "food" \
991 and world_db["ThingTypes"][type]["TT_TOOLPOWER"] > cmp_food:
992 cmp_food = world_db["ThingTypes"][type]["TT_TOOLPOWER"]
999 """Determine next command/argment for actor t via AI algorithms.
1001 AI will look for, and move towards, enemies (animate Things not of their
1002 own ThingType); if they see none, they will consume consumables in their
1003 inventory; if there are none, they will pick up what they stand on if they
1004 stand on consumables; if they stand on none, they will move towards the
1005 next consumable they see or remember on the map; if they see or remember
1006 none, they will explore parts of the map unseen since ever or for at least
1007 one turn; if there is nothing to explore, they will simply wait.
1009 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1010 if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1011 if not get_dir_to_target(t, "f"):
1012 sel = get_inventory_slot_to_consume(t)
1014 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1015 if world_db["ThingActions"][id]["TA_NAME"]
1017 t["T_ARGUMENT"] = sel
1018 elif standing_on_food(t):
1019 t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1020 if world_db["ThingActions"][id]["TA_NAME"]
1022 elif (not get_dir_to_target(t, "c")) and \
1023 (not get_dir_to_target(t, "a")):
1024 get_dir_to_target(t, "s")
1028 """Run game world and its inhabitants until new player input expected."""
1030 whilebreaker = False
1031 while world_db["Things"][0]["T_LIFEPOINTS"]:
1032 proliferable_map = world_db["MAP"][:]
1033 for id in [id for id in world_db["Things"]
1034 if not world_db["Things"][id]["carried"]]:
1035 y = world_db["Things"][id]["T_POSY"]
1036 x = world_db["Things"][id]["T_POSX"]
1037 proliferable_map[y * world_db["MAP_LENGTH"] + x] = ord('X')
1038 for id in [id for id in world_db["Things"]]: # Only what's from start!
1039 if not id in world_db["Things"] or \
1040 world_db["Things"][id]["carried"]: # May have been consumed or
1041 continue # picked up during turn …
1042 Thing = world_db["Things"][id]
1043 if Thing["T_LIFEPOINTS"]:
1044 if not Thing["T_COMMAND"]:
1045 update_map_memory(Thing)
1052 if Thing["T_LIFEPOINTS"]:
1053 Thing["T_PROGRESS"] += 1
1054 taid = [a for a in world_db["ThingActions"]
1055 if a == Thing["T_COMMAND"]][0]
1056 ThingAction = world_db["ThingActions"][taid]
1057 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
1058 eval("actor_" + ThingAction["TA_NAME"])(Thing)
1059 Thing["T_COMMAND"] = 0
1060 Thing["T_PROGRESS"] = 0
1061 thingproliferation(Thing, proliferable_map)
1064 world_db["TURN"] += 1
1067 def new_Thing(type, pos=(0, 0)):
1068 """Return Thing of type T_TYPE, with fovmap if alive and world active."""
1070 "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
1082 "T_MEMDEPTHMAP": False,
1085 if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
1086 build_fov_map(thing)
1090 def id_setter(id, category, id_store=False, start_at_1=False):
1091 """Set ID of object of category to manipulate ID unused? Create new one.
1092 The ID is stored as id_store.id (if id_store is set). If the integer of the
1093 input is valid (if start_at_1, >= 0, else >= -1), but <0 or (if start_at_1)
1094 <1, calculate new ID: lowest unused ID >=0 or (if start_at_1) >= 1. None is
1095 always returned when no new object is created, else the new object's ID.
1097 min = 0 if start_at_1 else -1
1099 id = integer_test(id, min)
1101 if id in world_db[category]:
1106 if (start_at_1 and 0 == id) \
1107 or ((not start_at_1) and (id < 0)):
1108 id = 0 if start_at_1 else -1
1111 if id not in world_db[category]:
1119 """Send PONG line to server output file."""
1120 strong_write(io_db["file_out"], "PONG\n")
1124 """Abort server process."""
1125 if None == opts.replay:
1126 if world_db["WORLD_ACTIVE"]:
1128 atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True)
1129 raise SystemExit("received QUIT command")
1132 def command_thingshere(str_y, str_x):
1133 """Write to out file list of Things known to player at coordinate y, x."""
1134 if world_db["WORLD_ACTIVE"]:
1135 y = integer_test(str_y, 0, 255)
1136 x = integer_test(str_x, 0, 255)
1137 length = world_db["MAP_LENGTH"]
1138 if None != y and None != x and y < length and x < length:
1139 pos = (y * world_db["MAP_LENGTH"]) + x
1140 strong_write(io_db["file_out"], "THINGS_HERE START\n")
1141 if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
1142 for id in [id for tid in sorted(list(world_db["ThingTypes"]))
1143 for id in world_db["Things"]
1144 if not world_db["Things"][id]["carried"]
1145 if world_db["Things"][id]["T_TYPE"] == tid
1146 if y == world_db["Things"][id]["T_POSY"]
1147 if x == world_db["Things"][id]["T_POSX"]]:
1148 type = world_db["Things"][id]["T_TYPE"]
1149 name = world_db["ThingTypes"][type]["TT_NAME"]
1150 strong_write(io_db["file_out"], name + "\n")
1152 for mt in [mt for tid in sorted(list(world_db["ThingTypes"]))
1153 for mt in world_db["Things"][0]["T_MEMTHING"]
1154 if mt[0] == tid if y == mt[1] if x == mt[2]]:
1155 name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
1156 strong_write(io_db["file_out"], name + "\n")
1157 strong_write(io_db["file_out"], "THINGS_HERE END\n")
1159 print("Ignoring: Invalid map coordinates.")
1161 print("Ignoring: Command only works on existing worlds.")
1164 def play_commander(action, args=False):
1165 """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
1167 T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
1171 id = [x for x in world_db["ThingActions"]
1172 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
1173 world_db["Things"][0]["T_COMMAND"] = id
1176 def set_command_and_argument_int(str_arg):
1177 val = integer_test(str_arg, 0, 255)
1179 world_db["Things"][0]["T_ARGUMENT"] = val
1182 def set_command_and_argument_movestring(str_arg):
1183 if str_arg in directions_db:
1184 world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
1187 print("Ignoring: Argument must be valid direction string.")
1189 if action == "move":
1190 return set_command_and_argument_movestring
1192 return set_command_and_argument_int
1197 def command_seedrandomness(seed_string):
1198 """Set rand seed to int(seed_string)."""
1199 val = integer_test(seed_string, 0, 4294967295)
1204 def command_makeworld(seed_string):
1205 """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
1207 Seed rand with seed. Do more only with a "wait" ThingAction and
1208 world["PLAYER_TYPE"] matching ThingType of TT_START_NUMBER > 0. Then,
1209 world_db["Things"] emptied, call make_map() and set
1210 world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1211 according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1212 of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1213 other. Init player's memory map. Write "NEW_WORLD" line to out file.
1220 err = "Space to put thing on too hard to find. Map too small?"
1222 y = rand.next() % world_db["MAP_LENGTH"]
1223 x = rand.next() % world_db["MAP_LENGTH"]
1224 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
1228 raise SystemExit(err)
1229 # Replica of C code, wrongly ignores animatedness of new Thing.
1230 pos_clear = (0 == len([id for id in world_db["Things"]
1231 if world_db["Things"][id]["T_LIFEPOINTS"]
1232 if world_db["Things"][id]["T_POSY"] == y
1233 if world_db["Things"][id]["T_POSX"] == x]))
1238 val = integer_test(seed_string, 0, 4294967295)
1242 player_will_be_generated = False
1243 playertype = world_db["PLAYER_TYPE"]
1244 for ThingType in world_db["ThingTypes"]:
1245 if playertype == ThingType:
1246 if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1247 player_will_be_generated = True
1249 if not player_will_be_generated:
1250 print("Ignoring: No player type with start number >0 defined.")
1253 for ThingAction in world_db["ThingActions"]:
1254 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1257 print("Ignoring beyond SEED_MAP: " +
1258 "No thing action with name 'wait' defined.")
1260 world_db["Things"] = {}
1262 world_db["WORLD_ACTIVE"] = 1
1263 world_db["TURN"] = 1
1264 for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1265 id = id_setter(-1, "Things")
1266 world_db["Things"][id] = new_Thing(playertype, free_pos())
1267 if not world_db["Things"][0]["fovmap"]:
1268 empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1269 world_db["Things"][0]["fovmap"] = empty_fovmap
1270 update_map_memory(world_db["Things"][0])
1271 for type in world_db["ThingTypes"]:
1272 for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1273 if type != playertype:
1274 id = id_setter(-1, "Things")
1275 world_db["Things"][id] = new_Thing(type, free_pos())
1276 strong_write(io_db["file_out"], "NEW_WORLD\n")
1280 def command_maplength(maplength_string):
1281 """Redefine map length. Invalidate map, therefore lose all things on it."""
1282 val = integer_test(maplength_string, 1, 256)
1284 world_db["MAP_LENGTH"] = val
1285 world_db["MAP"] = False
1286 set_world_inactive()
1287 world_db["Things"] = {}
1288 libpr.set_maplength(val)
1291 def command_worldactive(worldactive_string):
1292 """Toggle world_db["WORLD_ACTIVE"] if possible.
1294 An active world can always be set inactive. An inactive world can only be
1295 set active with a "wait" ThingAction, and a player Thing (of ID 0), and a
1296 map. On activation, rebuild all Things' FOVs, and the player's map memory.
1297 Also call log_help().
1299 val = integer_test(worldactive_string, 0, 1)
1301 if 0 != world_db["WORLD_ACTIVE"]:
1303 set_world_inactive()
1305 print("World already active.")
1306 elif 0 == world_db["WORLD_ACTIVE"]:
1308 for ThingAction in world_db["ThingActions"]:
1309 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1312 player_exists = False
1313 for Thing in world_db["Things"]:
1315 player_exists = True
1317 if wait_exists and player_exists and world_db["MAP"]:
1318 for id in world_db["Things"]:
1319 if world_db["Things"][id]["T_LIFEPOINTS"]:
1320 build_fov_map(world_db["Things"][id])
1322 update_map_memory(world_db["Things"][id], False)
1323 if not world_db["Things"][0]["T_LIFEPOINTS"]:
1324 empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1325 world_db["Things"][0]["fovmap"] = empty_fovmap
1326 world_db["WORLD_ACTIVE"] = 1
1329 print("Ignoring: Not all conditions for world activation met.")
1332 def test_for_id_maker(object, category):
1333 """Return decorator testing for object having "id" attribute."""
1336 if hasattr(object, "id"):
1339 print("Ignoring: No " + category +
1340 " defined to manipulate yet.")
1345 def command_tid(id_string):
1346 """Set ID of Thing to manipulate. ID unused? Create new one.
1348 Default new Thing's type to the first available ThingType, others: zero.
1350 id = id_setter(id_string, "Things", command_tid)
1352 if world_db["ThingTypes"] == {}:
1353 print("Ignoring: No ThingType to settle new Thing in.")
1355 type = list(world_db["ThingTypes"].keys())[0]
1356 world_db["Things"][id] = new_Thing(type)
1359 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1363 def command_tcommand(str_int):
1364 """Set T_COMMAND of selected Thing."""
1365 val = integer_test(str_int, 0)
1367 if 0 == val or val in world_db["ThingActions"]:
1368 world_db["Things"][command_tid.id]["T_COMMAND"] = val
1370 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1374 def command_ttype(str_int):
1375 """Set T_TYPE of selected Thing."""
1376 val = integer_test(str_int, 0)
1378 if val in world_db["ThingTypes"]:
1379 world_db["Things"][command_tid.id]["T_TYPE"] = val
1381 print("Ignoring: ThingType ID belongs to no known ThingType.")
1385 def command_tcarries(str_int):
1386 """Append int(str_int) to T_CARRIES of selected Thing.
1388 The ID int(str_int) must not be of the selected Thing, and must belong to a
1389 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1391 val = integer_test(str_int, 0)
1393 if val == command_tid.id:
1394 print("Ignoring: Thing cannot carry itself.")
1395 elif val in world_db["Things"] \
1396 and not world_db["Things"][val]["carried"]:
1397 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1398 world_db["Things"][val]["carried"] = True
1400 print("Ignoring: Thing not available for carrying.")
1401 # Note that the whole carrying structure is different from the C version:
1402 # Carried-ness is marked by a "carried" flag, not by Things containing
1403 # Things internally.
1407 def command_tmemthing(str_t, str_y, str_x):
1408 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1410 The type must fit to an existing ThingType, and the position into the map.
1412 type = integer_test(str_t, 0)
1413 posy = integer_test(str_y, 0, 255)
1414 posx = integer_test(str_x, 0, 255)
1415 if None != type and None != posy and None != posx:
1416 if type not in world_db["ThingTypes"] \
1417 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1418 print("Ignoring: Illegal value for thing type or position.")
1420 memthing = (type, posy, posx)
1421 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1424 def setter_map(maptype):
1425 """Set (world or Thing's) map of maptype's int(str_int)-th line to mapline.
1427 If no map of maptype exists yet, initialize it with ' ' bytes first.
1430 def valid_map_line(str_int, mapline):
1431 val = integer_test(str_int, 0, 255)
1433 if val >= world_db["MAP_LENGTH"]:
1434 print("Illegal value for map line number.")
1435 elif len(mapline) != world_db["MAP_LENGTH"]:
1436 print("Map line length is unequal map width.")
1441 def nonThingMap_helper(str_int, mapline):
1442 val = valid_map_line(str_int, mapline)
1444 length = world_db["MAP_LENGTH"]
1445 if not world_db["MAP"]:
1446 map = bytearray(b' ' * (length ** 2))
1448 map = world_db["MAP"]
1449 map[val * length:(val * length) + length] = mapline.encode()
1450 if not world_db["MAP"]:
1451 world_db["MAP"] = map
1454 def ThingMap_helper(str_int, mapline):
1455 val = valid_map_line(str_int, mapline)
1457 length = world_db["MAP_LENGTH"]
1458 if not world_db["Things"][command_tid.id][maptype]:
1459 map = bytearray(b' ' * (length ** 2))
1461 map = world_db["Things"][command_tid.id][maptype]
1462 map[val * length:(val * length) + length] = mapline.encode()
1463 if not world_db["Things"][command_tid.id][maptype]:
1464 world_db["Things"][command_tid.id][maptype] = map
1466 return nonThingMap_helper if maptype == "MAP" else ThingMap_helper
1470 def setter_tpos(axis):
1471 """Generate setter for T_POSX or T_POSY of selected Thing.
1473 If world is active, rebuilds animate things' fovmap, player's memory map.
1476 def helper(str_int):
1477 val = integer_test(str_int, 0, 255)
1479 if val < world_db["MAP_LENGTH"]:
1480 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1481 if world_db["WORLD_ACTIVE"] \
1482 and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1483 build_fov_map(world_db["Things"][command_tid.id])
1484 if 0 == command_tid.id:
1485 update_map_memory(world_db["Things"][command_tid.id])
1487 print("Ignoring: Position is outside of map.")
1491 def command_ttid(id_string):
1492 """Set ID of ThingType to manipulate. ID unused? Create new one.
1494 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, TT_TOOL to
1497 id = id_setter(id_string, "ThingTypes", command_ttid)
1499 world_db["ThingTypes"][id] = {
1500 "TT_NAME": "(none)",
1503 "TT_PROLIFERATE": 0,
1504 "TT_START_NUMBER": 0,
1511 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1515 def command_ttname(name):
1516 """Set TT_NAME of selected ThingType."""
1517 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1521 def command_tttool(name):
1522 """Set TT_TOOL of selected ThingType."""
1523 world_db["ThingTypes"][command_ttid.id]["TT_TOOL"] = name
1527 def command_ttsymbol(char):
1528 """Set TT_SYMBOL of selected ThingType. """
1530 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1532 print("Ignoring: Argument must be single character.")
1536 def command_ttcorpseid(str_int):
1537 """Set TT_CORPSE_ID of selected ThingType."""
1538 val = integer_test(str_int, 0)
1540 if val in world_db["ThingTypes"]:
1541 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1543 print("Ignoring: Corpse ID belongs to no known ThignType.")
1546 def command_taid(id_string):
1547 """Set ID of ThingAction to manipulate. ID unused? Create new one.
1549 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1551 id = id_setter(id_string, "ThingActions", command_taid, True)
1553 world_db["ThingActions"][id] = {
1559 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1562 @test_ThingAction_id
1563 def command_taname(name):
1564 """Set TA_NAME of selected ThingAction.
1566 The name must match a valid thing action function. If after the name
1567 setting no ThingAction with name "wait" remains, call set_world_inactive().
1569 if name == "wait" or name == "move" or name == "use" or name == "drop" \
1570 or name == "pick_up":
1571 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1572 if 1 == world_db["WORLD_ACTIVE"]:
1573 wait_defined = False
1574 for id in world_db["ThingActions"]:
1575 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1578 if not wait_defined:
1579 set_world_inactive()
1581 print("Ignoring: Invalid action name.")
1582 # In contrast to the original,naming won't map a function to a ThingAction.
1586 """Call ai() on player Thing, then turn_over()."""
1587 ai(world_db["Things"][0])
1591 """Commands database.
1593 Map command start tokens to ([0]) number of expected command arguments, ([1])
1594 the command's meta-ness (i.e. is it to be written to the record file, is it to
1595 be ignored in replay mode if read from server input file), and ([2]) a function
1599 "QUIT": (0, True, command_quit),
1600 "PING": (0, True, command_ping),
1601 "THINGS_HERE": (2, True, command_thingshere),
1602 "MAKE_WORLD": (1, False, command_makeworld),
1603 "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1604 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1605 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0)),
1606 "MAP_LENGTH": (1, False, command_maplength),
1607 "WORLD_ACTIVE": (1, False, command_worldactive),
1608 "MAP": (2, False, setter_map("MAP")),
1609 "TA_ID": (1, False, command_taid),
1610 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1611 "TA_NAME": (1, False, command_taname),
1612 "TT_ID": (1, False, command_ttid),
1613 "TT_NAME": (1, False, command_ttname),
1614 "TT_TOOL": (1, False, command_tttool),
1615 "TT_SYMBOL": (1, False, command_ttsymbol),
1616 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1617 "TT_TOOLPOWER": (1, False, setter("ThingType", "TT_TOOLPOWER", 0, 65535)),
1618 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1620 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1622 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1623 "T_ID": (1, False, command_tid),
1624 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1625 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1626 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1627 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1628 "T_COMMAND": (1, False, command_tcommand),
1629 "T_TYPE": (1, False, command_ttype),
1630 "T_CARRIES": (1, False, command_tcarries),
1631 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1632 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1633 "T_MEMTHING": (3, False, command_tmemthing),
1634 "T_POSY": (1, False, setter_tpos("Y")),
1635 "T_POSX": (1, False, setter_tpos("X")),
1636 "wait": (0, False, play_commander("wait")),
1637 "move": (1, False, play_commander("move")),
1638 "pick_up": (0, False, play_commander("pick_up")),
1639 "drop": (1, False, play_commander("drop", True)),
1640 "use": (1, False, play_commander("use", True)),
1641 "ai": (0, False, command_ai)
1643 # TODO: Unhandled cases: (Un-)killing animates (esp. player!) with T_LIFEPOINTS.
1646 """World state database. With sane default values. (Randomness is in rand.)"""
1658 """Mapping of direction names to internal direction chars."""
1659 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1660 "west": "s", "north-west": "w", "north-east": "e"}
1662 """File IO database."""
1664 "path_save": "save",
1665 "path_record": "record_save",
1666 "path_worldconf": "confserver/world",
1667 "path_server": "server/",
1668 "path_in": "server/in",
1669 "path_out": "server/out",
1670 "path_worldstate": "server/worldstate",
1671 "tmp_suffix": "_tmp",
1672 "kicked_by_rival": False,
1673 "worldstate_updateable": False
1678 libpr = prep_library()
1679 rand = RandomnessIO()
1680 opts = parse_command_line_arguments()
1682 io_db["path_save"] = opts.savefile
1683 io_db["path_record"] = "record_" + opts.savefile
1686 io_db["verbose"] = True
1687 if None != opts.replay:
1691 except SystemExit as exit:
1692 print("ABORTING: " + exit.args[0])
1694 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")