11 """"Interface to libplomrogue's pseudo-randomness generator."""
13 def set_seed(self, seed):
14 libpr.seed_rrand(1, seed)
17 return libpr.seed_rrand(0, 0)
22 seed = property(get_seed, set_seed)
26 """Prepare ctypes library at ./libplomrogue.so"""
27 libpath = ("./libplomrogue.so")
28 if not os.access(libpath, os.F_OK):
29 raise SystemExit("No library " + libpath + ", run ./compile.sh first?")
30 libpr = ctypes.cdll.LoadLibrary(libpath)
31 libpr.seed_rrand.argtypes = [ctypes.c_uint8, ctypes.c_uint32]
32 libpr.seed_rrand.restype = ctypes.c_uint32
33 libpr.rrand.argtypes = []
34 libpr.rrand.restype = ctypes.c_uint16
35 libpr.set_maplength.argtypes = [ctypes.c_uint16]
36 libpr.mv_yx_in_dir_legal_wrap.argtypes = [ctypes.c_char, ctypes.c_uint8,
38 libpr.mv_yx_in_dir_legal_wrap.restype = ctypes.c_uint8
39 libpr.result_y.restype = ctypes.c_uint8
40 libpr.result_x.restype = ctypes.c_uint8
41 libpr.set_maplength(world_db["MAP_LENGTH"])
42 libpr.build_fov_map.argtypes = [ctypes.c_uint8, ctypes.c_uint8,
43 ctypes.c_char_p, ctypes.c_char_p]
44 libpr.build_fov_map.restype = ctypes.c_uint8
48 def strong_write(file, string):
49 """Apply write(string), flush(), and os.fsync() to file."""
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 os.makedirs(io_db["path_server"], exist_ok=True)
73 io_db["file_out"] = open(io_db["path_out"], "w")
74 strong_write(io_db["file_out"], io_db["teststring"] + "\n")
75 if os.access(io_db["path_in"], os.F_OK):
76 os.remove(io_db["path_in"])
77 io_db["file_in"] = open(io_db["path_in"], "w")
78 io_db["file_in"].close()
79 io_db["file_in"] = open(io_db["path_in"], "r")
80 detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
81 detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
84 def cleanup_server_io():
85 """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
86 def helper(file_key, path_key):
88 io_db[file_key].close()
89 if not io_db["kicked_by_rival"] \
90 and os.access(io_db[path_key], os.F_OK):
91 os.remove(io_db[path_key])
92 helper("file_out", "path_out")
93 helper("file_in", "path_in")
94 helper("file_worldstate", "path_worldstate")
95 if "file_record" in io_db:
96 io_db["file_record"].close()
99 def obey(command, prefix, replay=False, do_record=False):
100 """Call function from commands_db mapped to command's first token.
102 Tokenize command string with shlex.split(comments=True). If replay is set,
103 a non-meta command from the commands_db merely triggers obey() on the next
104 command from the records file. If not, non-meta commands set
105 io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
106 do_record is set, are recorded via record(), and save_world() is called.
107 The prefix string is inserted into the server's input message between its
108 beginning 'input ' & ':'. All activity is preceded by a server_test() call.
111 print("input " + prefix + ": " + command)
113 tokens = shlex.split(command, comments=True)
114 except ValueError as err:
115 print("Can't tokenize command string: " + str(err) + ".")
117 if len(tokens) > 0 and tokens[0] in commands_db \
118 and len(tokens) == commands_db[tokens[0]][0] + 1:
119 if commands_db[tokens[0]][1]:
120 commands_db[tokens[0]][2](*tokens[1:])
122 print("Due to replay mode, reading command as 'go on in record'.")
123 line = io_db["file_record"].readline()
125 obey(line.rstrip(), io_db["file_record"].prefix
126 + str(io_db["file_record"].line_n))
127 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
129 print("Reached end of record file.")
131 commands_db[tokens[0]][2](*tokens[1:])
135 io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
136 elif 0 != len(tokens):
137 print("Invalid command/argument, or bad number of tokens.")
140 def atomic_write(path, text, do_append=False):
141 """Atomic write of text to file at path, appended if do_append is set."""
142 path_tmp = path + io_db["tmp_suffix"]
146 if os.access(path, os.F_OK):
147 shutil.copyfile(path, path_tmp)
148 file = open(path_tmp, mode)
149 strong_write(file, text)
151 if os.access(path, os.F_OK):
153 os.rename(path_tmp, path)
157 """Append command string plus newline to record file. (Atomic.)"""
158 # This misses some optimizations from the original record(), namely only
159 # finishing the atomic write with expensive flush() and fsync() every 15
160 # seconds unless explicitely forced. Implement as needed.
161 atomic_write(io_db["path_record"], command + "\n", do_append=True)
165 """Save all commands needed to reconstruct current world state."""
166 # TODO: Misses same optimizations as record() from the original record().
169 string = string.replace("\u005C", '\u005C\u005C')
170 return '"' + string.replace('"', '\u005C"') + '"'
175 if world_db["Things"][id][key]:
176 map = world_db["Things"][id][key]
177 length = world_db["MAP_LENGTH"]
178 for i in range(length):
179 line = map[i * length:(i * length) + length].decode()
180 string = string + key + " " + str(i) + " " + quote(line) \
187 for memthing in world_db["Things"][id]["T_MEMTHING"]:
188 string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
189 str(memthing[1]) + " " + str(memthing[2]) + "\n"
192 def helper(category, id_string, special_keys={}):
194 for id in world_db[category]:
195 string = string + id_string + " " + str(id) + "\n"
196 for key in world_db[category][id]:
197 if not key in special_keys:
198 x = world_db[category][id][key]
199 argument = quote(x) if str == type(x) else str(x)
200 string = string + key + " " + argument + "\n"
201 elif special_keys[key]:
202 string = string + special_keys[key](id)
207 if dict != type(world_db[key]) and key != "MAP":
208 string = string + key + " " + str(world_db[key]) + "\n"
209 string = string + helper("ThingActions", "TA_ID")
210 string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
211 for id in world_db["ThingTypes"]:
212 string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
213 str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
214 string = string + helper("Things", "T_ID",
215 {"T_CARRIES": False, "carried": False,
216 "T_MEMMAP": mapsetter("T_MEMMAP"),
217 "T_MEMTHING": memthing, "fovmap": False,
218 "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
219 for id in world_db["Things"]:
220 if [] != world_db["Things"][id]["T_CARRIES"]:
221 string = string + "T_ID " + str(id) + "\n"
222 for carried_id in world_db["Things"][id]["T_CARRIES"]:
223 string = string + "T_CARRIES " + str(carried_id) + "\n"
224 string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
225 "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
226 atomic_write(io_db["path_save"], string)
229 def obey_lines_in_file(path, name, do_record=False):
230 """Call obey() on each line of path's file, use name in input prefix."""
231 file = open(path, "r")
233 for line in file.readlines():
234 obey(line.rstrip(), name + "file line " + str(line_n),
240 def parse_command_line_arguments():
241 """Return settings values read from command line arguments."""
242 parser = argparse.ArgumentParser()
243 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
245 opts, unknown = parser.parse_known_args()
250 """Ensure valid server out file belonging to current process.
252 This is done by comparing io_db["teststring"] to what's found at the start
253 of the current file at io_db["path_out"]. On failure, set
254 io_db["kicked_by_rival"] and raise SystemExit.
256 if not os.access(io_db["path_out"], os.F_OK):
257 raise SystemExit("Server output file has disappeared.")
258 file = open(io_db["path_out"], "r")
259 test = file.readline().rstrip("\n")
261 if test != io_db["teststring"]:
262 io_db["kicked_by_rival"] = True
263 msg = "Server test string in server output file does not match. This" \
264 " indicates that the current server process has been " \
265 "superseded by another one."
266 raise SystemExit(msg)
270 """Return next newline-delimited command from server in file.
272 Keep building return string until a newline is encountered. Pause between
273 unsuccessful reads, and after too much waiting, run server_test().
275 wait_on_fail = 0.03333
280 add = io_db["file_in"].readline()
282 command = command + add
283 if len(command) > 0 and "\n" == command[-1]:
284 command = command[:-1]
287 time.sleep(wait_on_fail)
288 if now + max_wait < time.time():
294 def try_worldstate_update():
295 """Write worldstate file if io_db["worldstate_updateable"] is set."""
296 if io_db["worldstate_updateable"]:
298 def draw_visible_Things(map, run):
299 for id in world_db["Things"]:
300 type = world_db["Things"][id]["T_TYPE"]
301 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
302 alive = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]
303 if (0 == run and not consumable and not alive) \
304 or (1 == run and consumable and not alive) \
305 or (2 == run and alive):
306 y = world_db["Things"][id]["T_POSY"]
307 x = world_db["Things"][id]["T_POSX"]
308 fovflag = world_db["Things"][0]["fovmap"][(y * length) + x]
309 if 'v' == chr(fovflag):
310 c = world_db["ThingTypes"][type]["TT_SYMBOL"]
311 map[(y * length) + x] = ord(c)
313 def write_map(string, map):
314 for i in range(length):
315 line = map[i * length:(i * length) + length].decode()
316 string = string + line + "\n"
320 if [] == world_db["Things"][0]["T_CARRIES"]:
321 inventory = "(none)\n"
323 for id in world_db["Things"][0]["T_CARRIES"]:
324 type_id = world_db["Things"][id]["T_TYPE"]
325 name = world_db["ThingTypes"][type_id]["TT_NAME"]
326 inventory = inventory + name + "\n"
327 string = str(world_db["TURN"]) + "\n" + \
328 str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
329 str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
330 inventory + "%\n" + \
331 str(world_db["Things"][0]["T_POSY"]) + "\n" + \
332 str(world_db["Things"][0]["T_POSX"]) + "\n" + \
333 str(world_db["MAP_LENGTH"]) + "\n"
334 length = world_db["MAP_LENGTH"]
335 fov = bytearray(b' ' * (length ** 2))
336 for pos in range(length ** 2):
337 if 'v' == chr(world_db["Things"][0]["fovmap"][pos]):
338 fov[pos] = world_db["MAP"][pos]
340 draw_visible_Things(fov, i)
341 string = write_map(string, fov)
342 mem = world_db["Things"][0]["T_MEMMAP"][:]
344 for memthing in world_db["Things"][0]["T_MEMTHING"]:
345 type = world_db["Things"][memthing[0]]["T_TYPE"]
346 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
347 if (i == 0 and not consumable) or (i == 1 and consumable):
348 c = world_db["ThingTypes"][type]["TT_SYMBOL"]
349 mem[(memthing[1] * length) + memthing[2]] = ord(c)
350 string = write_map(string, mem)
351 atomic_write(io_db["path_worldstate"], string)
352 strong_write(io_db["file_out"], "WORLD_UPDATED\n")
353 io_db["worldstate_updateable"] = False
357 """Replay game from record file.
359 Use opts.replay as breakpoint turn to which to replay automatically before
360 switching to manual input by non-meta commands in server input file
361 triggering further reads of record file. Ensure opts.replay is at least 1.
362 Run try_worldstate_update() before each interactive obey()/read_command().
366 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
367 " (if so late a turn is to be found).")
368 if not os.access(io_db["path_record"], os.F_OK):
369 raise SystemExit("No record file found to replay.")
370 io_db["file_record"] = open(io_db["path_record"], "r")
371 io_db["file_record"].prefix = "record file line "
372 io_db["file_record"].line_n = 1
373 while world_db["TURN"] < opts.replay:
374 line = io_db["file_record"].readline()
377 obey(line.rstrip(), io_db["file_record"].prefix
378 + str(io_db["file_record"].line_n))
379 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
381 try_worldstate_update()
382 obey(read_command(), "in file", replay=True)
386 """Play game by server input file commands. Before, load save file found.
388 If no save file is found, a new world is generated from the commands in the
389 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
390 command and all that follow via the server input file. Run
391 try_worldstate_update() before each interactive obey()/read_command().
393 if os.access(io_db["path_save"], os.F_OK):
394 obey_lines_in_file(io_db["path_save"], "save")
396 if not os.access(io_db["path_worldconf"], os.F_OK):
397 msg = "No world config file from which to start a new world."
398 raise SystemExit(msg)
399 obey_lines_in_file(io_db["path_worldconf"], "world config ",
401 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
403 try_worldstate_update()
404 obey(read_command(), "in file", do_record=True)
408 """(Re-)make island map.
410 Let "~" represent water, "." land, "X" trees: Build island shape randomly,
411 start with one land cell in the middle, then go into cycle of repeatedly
412 selecting a random sea cell and transforming it into land if it is neighbor
413 to land. The cycle ends when a land cell is due to be created at the map's
414 border. Then put some trees on the map (TODO: more precise algorithm desc).
416 def is_neighbor(coordinates, type):
419 length = world_db["MAP_LENGTH"]
421 diag_west = x + (ind > 0)
422 diag_east = x + (ind < (length - 1))
423 pos = (y * length) + x
424 if (y > 0 and diag_east
425 and type == chr(world_db["MAP"][pos - length + ind])) \
427 and type == chr(world_db["MAP"][pos + 1])) \
428 or (y < (length - 1) and diag_east
429 and type == chr(world_db["MAP"][pos + length + ind])) \
430 or (y > 0 and diag_west
431 and type == chr(world_db["MAP"][pos - length - (not ind)])) \
433 and type == chr(world_db["MAP"][pos - 1])) \
434 or (y < (length - 1) and diag_west
435 and type == chr(world_db["MAP"][pos + length - (not ind)])):
438 store_seed = rand.seed
439 world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
440 length = world_db["MAP_LENGTH"]
441 add_half_width = (not (length % 2)) * int(length / 2)
442 world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
444 y = rand.next() % length
445 x = rand.next() % length
446 pos = (y * length) + x
447 if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
448 if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
450 world_db["MAP"][pos] = ord(".")
451 n_trees = int((length ** 2) / 16)
453 while (i_trees <= n_trees):
454 single_allowed = rand.next() % 32
455 y = rand.next() % length
456 x = rand.next() % length
457 pos = (y * length) + x
458 if "." == chr(world_db["MAP"][pos]) \
459 and ((not single_allowed) or is_neighbor((y, x), "X")):
460 world_db["MAP"][pos] = ord("X")
462 rand.seed = store_seed
463 # This all-too-precise replica of the original C code misses iter_limit().
466 def update_map_memory(t):
467 """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
468 if not t["T_MEMMAP"]:
469 t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
470 if not t["T_MEMDEPTHMAP"]:
471 t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
472 for pos in range(world_db["MAP_LENGTH"] ** 2):
473 if "v" == chr(t["fovmap"][pos]):
474 t["T_MEMDEPTHMAP"][pos] = ord("0")
475 if " " == chr(t["T_MEMMAP"][pos]):
476 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
478 # TODO: Aging of MEMDEPTHMAP.
479 for memthing in t["T_MEMTHING"]:
480 y = world_db["Things"][memthing[0]]["T_POSY"]
481 x = world_db["Things"][memthing[0]]["T_POSX"]
482 if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
483 t["T_MEMTHING"].remove(memthing)
484 for id in world_db["Things"]:
485 type = world_db["Things"][id]["T_TYPE"]
486 if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
487 y = world_db["Things"][id]["T_POSY"]
488 x = world_db["Things"][id]["T_POSX"]
489 if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
490 t["T_MEMTHING"].append((type, y, x))
493 def set_world_inactive():
494 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
496 if os.access(io_db["path_worldstate"], os.F_OK):
497 os.remove(io_db["path_worldstate"])
498 world_db["WORLD_ACTIVE"] = 0
501 def integer_test(val_string, min, max):
502 """Return val_string if possible integer >= min and <= max, else None."""
504 val = int(val_string)
505 if val < min or val > max:
509 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
514 def setter(category, key, min, max):
515 """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
518 val = integer_test(val_string, min, max)
522 if category == "Thing":
523 id_store = command_tid
524 decorator = test_Thing_id
525 elif category == "ThingType":
526 id_store = command_ttid
527 decorator = test_ThingType_id
528 elif category == "ThingAction":
529 id_store = command_taid
530 decorator = test_ThingAction_id
534 val = integer_test(val_string, min, max)
536 world_db[category + "s"][id_store.id][key] = val
540 def build_fov_map(t):
541 """Build Thing's FOV map."""
542 t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
543 maptype = ctypes.c_char * len(world_db["MAP"])
544 test = libpr.build_fov_map(t["T_POSY"], t["T_POSX"],
545 maptype.from_buffer(t["fovmap"]),
546 maptype.from_buffer(world_db["MAP"]))
548 raise SystemExit("Malloc error in build_fov_Map().")
551 def decrement_lifepoints(t):
552 """Decrement t's lifepoints by 1, and if to zero, corpse it.
554 If t is the player avatar, only blank its fovmap, so that the client may
555 still display memory data. On non-player things, erase fovmap and memory.
557 t["T_LIFEPOINTS"] -= 1
558 if 0 == t["T_LIFEPOINTS"]:
559 t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
560 if world_db["Things"][0] == t:
561 t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
562 strong_write(io_db["file_out"], "LOG You die.\n")
565 t["T_MEMMAP"] = False
566 t["T_MEMDEPTHMAP"] = False
568 strong_write(io_db["file_out"], "LOG It dies.\n")
571 def mv_yx_in_dir_legal(dir, y, x):
572 """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
573 dir_c = dir.encode("ascii")[0]
574 test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
576 raise SystemExit("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
577 return (test, libpr.result_y(), libpr.result_x())
581 """Make t do nothing (but loudly, if player avatar)."""
582 if t == world_db["Things"][0]:
583 strong_write(io_db["file_out"], "LOG You wait.\n")
587 """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
589 move_result = mv_yx_in_dir_legal(t["T_ARGUMENT"], t["T_POSY"], t["T_POSX"])
590 if 1 == move_result[0]:
591 pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
592 passable = "." == chr(world_db["MAP"][pos])
593 hitted = [id for id in world_db["Things"]
594 if world_db["Things"][id] != t
595 if world_db["Things"][id]["T_LIFEPOINTS"]
596 if world_db["Things"][id]["T_POSY"] == move_result[1]
597 if world_db["Things"][id]["T_POSX"] == move_result[2]]
600 hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
601 hitter = "You" if t == world_db["Things"][0] else hitter_name
602 hitted_type = world_db["Things"][hit_id]["T_TYPE"]
603 hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
604 hitted = "you" if hit_id == 0 else hitted_name
605 verb = " wound " if hitter == "You" else " wounds "
606 strong_write(io_db["file_out"], "LOG " + hitter + verb + hitted + \
608 decrement_lifepoints(world_db["Things"][hit_id])
610 dir = [dir for dir in directions_db
611 if directions_db[dir] == t["T_ARGUMENT"]][0]
613 t["T_POSY"] = move_result[1]
614 t["T_POSX"] = move_result[2]
615 for id in t["T_CARRIES"]:
616 world_db["Things"][id]["T_POSY"] = move_result[1]
617 world_db["Things"][id]["T_POSX"] = move_result[2]
619 strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
621 strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
624 def actor_pick_up(t):
625 """Make t pick up (topmost?) Thing from ground into inventory."""
626 # Topmostness is actually not defined so far.
627 ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
628 if not world_db["Things"][id]["carried"]
629 if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
630 if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
632 world_db["Things"][ids[0]]["carried"] = True
633 t["T_CARRIES"].append(ids[0])
634 if t == world_db["Things"][0]:
635 strong_write(io_db["file_out"], "LOG You pick up an object.\n")
636 elif t == world_db["Things"][0]:
637 err = "You try to pick up an object, but there is none."
638 strong_write(io_db["file_out"], "LOG " + err + "\n")
642 """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
643 # TODO: Handle case where T_ARGUMENT matches nothing.
644 if len(t["T_CARRIES"]):
645 id = t["T_CARRIES"][t["T_ARGUMENT"]]
646 t["T_CARRIES"].remove(id)
647 world_db["Things"][id]["carried"] = False
648 if t == world_db["Things"][0]:
649 strong_write(io_db["file_out"], "LOG You drop an object.\n")
650 elif t == world_db["Things"][0]:
651 err = "You try to drop an object, but you own none."
652 strong_write(io_db["file_out"], "LOG " + err + "\n")
656 """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
657 # Original wrongly featured lifepoints increase through consumable!
658 # TODO: Handle case where T_ARGUMENT matches nothing.
659 if len(t["T_CARRIES"]):
660 id = t["T_CARRIES"][t["T_ARGUMENT"]]
661 type = world_db["Things"][id]["T_TYPE"]
662 if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
663 t["T_CARRIES"].remove(id)
664 del world_db["Things"][id]
665 t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
666 strong_write(io_db["file_out"], "LOG You consume this object.\n")
668 strong_write(io_db["file_out"], "LOG You try to use this object," +
671 strong_write(io_db["file_out"], "LOG You try to use an object, but " +
675 def thingproliferation(t):
676 """To chance of 1/TT_PROLIFERATE, create t offspring in neighbor cell.
678 Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be
679 passable and not be inhabited by a Thing of the same type, or, if Thing is
680 animate, any other animate Thing. If there are several map cell candidates,
681 one is selected randomly.
683 def test_cell(t, y, x):
684 if "." == chr(world_db["MAP"][(y * world_db["MAP_LENGTH"]) + x]):
685 for id in [id for id in world_db["Things"]
686 if y == world_db["Things"][id]["T_POSY"]
687 if x == world_db["Things"][id]["T_POSX"]
688 if (t["T_TYPE"] == world_db["Things"][id]["T_TYPE"])
689 or (t["T_LIFEPOINTS"] and
690 world_db["Things"][id]["T_LIFEPOINTS"])]:
694 prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
695 if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)):
697 for dir in [directions_db[key] for key in directions_db]:
698 mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
699 if mv_result[0] and test_cell(t, mv_result[1], mv_result[2]):
700 candidates.append((mv_result[1], mv_result[2]))
702 i = rand.next() % len(candidates)
703 id = id_setter(-1, "Things")
704 newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
705 world_db["Things"][id] = newT
709 """Run game world and its inhabitants until new player input expected."""
712 while world_db["Things"][0]["T_LIFEPOINTS"]:
713 for id in [id for id in world_db["Things"]]:
714 Thing = world_db["Things"][id]
715 if Thing["T_LIFEPOINTS"]:
716 if not Thing["T_COMMAND"]:
717 update_map_memory(Thing)
722 Thing["T_COMMAND"] = 1
724 Thing["T_PROGRESS"] += 1
725 taid = [a for a in world_db["ThingActions"]
726 if a == Thing["T_COMMAND"]][0]
727 ThingAction = world_db["ThingActions"][taid]
728 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
729 eval("actor_" + ThingAction["TA_NAME"])(Thing)
730 Thing["T_COMMAND"] = 0
731 Thing["T_PROGRESS"] = 0
733 thingproliferation(Thing)
736 world_db["TURN"] += 1
739 def new_Thing(type, pos=(0,0)):
740 """Return Thing of type T_TYPE, with fovmap if alive and world active."""
742 "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
754 "T_MEMDEPTHMAP": False,
757 if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
762 def id_setter(id, category, id_store=False, start_at_1=False):
763 """Set ID of object of category to manipulate ID unused? Create new one.
765 The ID is stored as id_store.id (if id_store is set). If the integer of the
766 input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
767 32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
768 >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
769 new object is created, otherwise the new object's ID.
771 min = 0 if start_at_1 else -32768
772 max = 255 if start_at_1 else 32767
774 id = integer_test(id, min, max)
776 if id in world_db[category]:
781 if (start_at_1 and 0 == id) \
782 or ((not start_at_1) and (id < 0 or id > 255)):
786 if id not in world_db[category]:
790 "No unused ID available to add to ID list.")
798 """Send PONG line to server output file."""
799 strong_write(io_db["file_out"], "PONG\n")
803 """Abort server process."""
804 raise SystemExit("received QUIT command")
807 def command_thingshere(str_y, str_x):
808 """Write to out file list of Things known to player at coordinate y, x."""
809 def write_thing_if_here():
810 if y == world_db["Things"][id]["T_POSY"] \
811 and x == world_db["Things"][id]["T_POSX"] \
812 and not world_db["Things"][id]["carried"]:
813 type = world_db["Things"][id]["T_TYPE"]
814 name = world_db["ThingTypes"][type]["TT_NAME"]
815 strong_write(io_db["file_out"], name + "\n")
816 if world_db["WORLD_ACTIVE"]:
817 y = integer_test(str_y, 0, 255)
818 x = integer_test(str_x, 0, 255)
819 length = world_db["MAP_LENGTH"]
820 if None != y and None != x and y < length and x < length:
821 pos = (y * world_db["MAP_LENGTH"]) + x
822 strong_write(io_db["file_out"], "THINGS_HERE START\n")
823 if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
824 for id in world_db["Things"]:
825 write_thing_if_here()
827 for id in world_db["Things"][id]["T_MEMTHING"]:
828 write_thing_if_here()
829 strong_write(io_db["file_out"], "THINGS_HERE END\n")
831 print("Ignoring: Invalid map coordinates.")
833 print("Ignoring: Command only works on existing worlds.")
836 def play_commander(action, args=False):
837 """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
839 T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
843 id = [x for x in world_db["ThingActions"]
844 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
845 world_db["Things"][0]["T_COMMAND"] = id
848 def set_command_and_argument_int(str_arg):
849 val = integer_test(str_arg, 0, 255)
851 world_db["Things"][0]["T_ARGUMENT"] = val
854 def set_command_and_argument_movestring(str_arg):
855 if str_arg in directions_db:
856 world_db["Things"][0]["T_ARGUMENT"] = directions_db[str_arg]
859 print("Ignoring: Argument must be valid direction string.")
862 return set_command_and_argument_movestring
864 return set_command_and_argument_int
869 def command_seedrandomness(seed_string):
870 """Set rand seed to int(seed_string)."""
871 val = integer_test(seed_string, 0, 4294967295)
876 def command_seedmap(seed_string):
877 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
878 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
882 def command_makeworld(seed_string):
883 """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
885 Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
886 "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
887 TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
888 and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
889 according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
890 of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
891 other. Init player's memory map. Write "NEW_WORLD" line to out file.
897 err = "Space to put thing on too hard to find. Map too small?"
899 y = rand.next() % world_db["MAP_LENGTH"]
900 x = rand.next() % world_db["MAP_LENGTH"]
901 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
905 raise SystemExit(err)
906 # Replica of C code, wrongly ignores animatedness of new Thing.
907 pos_clear = (0 == len([id for id in world_db["Things"]
908 if world_db["Things"][id]["T_LIFEPOINTS"]
909 if world_db["Things"][id]["T_POSY"] == y
910 if world_db["Things"][id]["T_POSX"] == x]))
915 val = integer_test(seed_string, 0, 4294967295)
919 world_db["SEED_MAP"] = val
920 player_will_be_generated = False
921 playertype = world_db["PLAYER_TYPE"]
922 for ThingType in world_db["ThingTypes"]:
923 if playertype == ThingType:
924 if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
925 player_will_be_generated = True
927 if not player_will_be_generated:
928 print("Ignoring beyond SEED_MAP: " +
929 "No player type with start number >0 defined.")
932 for ThingAction in world_db["ThingActions"]:
933 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
936 print("Ignoring beyond SEED_MAP: " +
937 "No thing action with name 'wait' defined.")
939 world_db["Things"] = {}
941 world_db["WORLD_ACTIVE"] = 1
943 for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
944 id = id_setter(-1, "Things")
945 world_db["Things"][id] = new_Thing(playertype, free_pos())
946 update_map_memory(world_db["Things"][0])
947 for type in world_db["ThingTypes"]:
948 for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
949 if type != playertype:
950 id = id_setter(-1, "Things")
951 world_db["Things"][id] = new_Thing(type, free_pos())
952 strong_write(io_db["file_out"], "NEW_WORLD\n")
955 def command_maplength(maplength_string):
956 """Redefine map length. Invalidate map, therefore lose all things on it."""
957 val = integer_test(maplength_string, 1, 256)
959 world_db["MAP_LENGTH"] = val
961 world_db["Things"] = {}
962 libpr.set_maplength(val)
965 def command_worldactive(worldactive_string):
966 """Toggle world_db["WORLD_ACTIVE"] if possible.
968 An active world can always be set inactive. An inactive world can only be
969 set active with a "wait" ThingAction, and a player Thing (of ID 0). On
970 activation, rebuild all Things' FOVs, and the player's map memory.
972 # In original version, map existence was also tested (unnecessarily?).
973 val = integer_test(worldactive_string, 0, 1)
975 if 0 != world_db["WORLD_ACTIVE"]:
979 print("World already active.")
980 elif 0 == world_db["WORLD_ACTIVE"]:
982 for ThingAction in world_db["ThingActions"]:
983 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
986 player_exists = False
987 for Thing in world_db["Things"]:
991 if wait_exists and player_exists:
992 for id in world_db["Things"]:
993 if world_db["Things"][id]["T_LIFEPOINTS"]:
994 build_fov_map(world_db["Things"][id])
996 update_map_memory(world_db["Things"][id])
997 world_db["WORLD_ACTIVE"] = 1
1000 def test_for_id_maker(object, category):
1001 """Return decorator testing for object having "id" attribute."""
1004 if hasattr(object, "id"):
1007 print("Ignoring: No " + category +
1008 " defined to manipulate yet.")
1013 def command_tid(id_string):
1014 """Set ID of Thing to manipulate. ID unused? Create new one.
1016 Default new Thing's type to the first available ThingType, others: zero.
1018 id = id_setter(id_string, "Things", command_tid)
1020 if world_db["ThingTypes"] == {}:
1021 print("Ignoring: No ThingType to settle new Thing in.")
1023 type = list(world_db["ThingTypes"].keys())[0]
1024 world_db["Things"][id] = new_Thing(type)
1027 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1031 def command_tcommand(str_int):
1032 """Set T_COMMAND of selected Thing."""
1033 val = integer_test(str_int, 0, 255)
1035 if 0 == val or val in world_db["ThingActions"]:
1036 world_db["Things"][command_tid.id]["T_COMMAND"] = val
1038 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1042 def command_ttype(str_int):
1043 """Set T_TYPE of selected Thing."""
1044 val = integer_test(str_int, 0, 255)
1046 if val in world_db["ThingTypes"]:
1047 world_db["Things"][command_tid.id]["T_TYPE"] = val
1049 print("Ignoring: ThingType ID belongs to no known ThingType.")
1053 def command_tcarries(str_int):
1054 """Append int(str_int) to T_CARRIES of selected Thing.
1056 The ID int(str_int) must not be of the selected Thing, and must belong to a
1057 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1059 val = integer_test(str_int, 0, 255)
1061 if val == command_tid.id:
1062 print("Ignoring: Thing cannot carry itself.")
1063 elif val in world_db["Things"] \
1064 and not world_db["Things"][val]["carried"]:
1065 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1066 world_db["Things"][val]["carried"] = True
1068 print("Ignoring: Thing not available for carrying.")
1069 # Note that the whole carrying structure is different from the C version:
1070 # Carried-ness is marked by a "carried" flag, not by Things containing
1071 # Things internally.
1075 def command_tmemthing(str_t, str_y, str_x):
1076 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1078 The type must fit to an existing ThingType, and the position into the map.
1080 type = integer_test(str_t, 0, 255)
1081 posy = integer_test(str_y, 0, 255)
1082 posx = integer_test(str_x, 0, 255)
1083 if None != type and None != posy and None != posx:
1084 if type not in world_db["ThingTypes"] \
1085 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1086 print("Ignoring: Illegal value for thing type or position.")
1088 memthing = (type, posy, posx)
1089 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1092 def setter_map(maptype):
1093 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
1095 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
1098 def helper(str_int, mapline):
1099 val = integer_test(str_int, 0, 255)
1101 if val >= world_db["MAP_LENGTH"]:
1102 print("Illegal value for map line number.")
1103 elif len(mapline) != world_db["MAP_LENGTH"]:
1104 print("Map line length is unequal map width.")
1106 length = world_db["MAP_LENGTH"]
1108 if not world_db["Things"][command_tid.id][maptype]:
1109 map = bytearray(b' ' * (length ** 2))
1111 map = world_db["Things"][command_tid.id][maptype]
1112 map[val * length:(val * length) + length] = mapline.encode()
1113 world_db["Things"][command_tid.id][maptype] = map
1117 def setter_tpos(axis):
1118 """Generate setter for T_POSX or T_POSY of selected Thing.
1120 If world is active, rebuilds animate things' fovmap, player's memory map.
1123 def helper(str_int):
1124 val = integer_test(str_int, 0, 255)
1126 if val < world_db["MAP_LENGTH"]:
1127 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1128 if world_db["WORLD_ACTIVE"] \
1129 and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1130 build_fov_map(world_db["Things"][command_tid.id])
1131 if 0 == command_tid.id:
1132 update_map_memory(world_db["Things"][command_tid.id])
1134 print("Ignoring: Position is outside of map.")
1138 def command_ttid(id_string):
1139 """Set ID of ThingType to manipulate. ID unused? Create new one.
1141 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1143 id = id_setter(id_string, "ThingTypes", command_ttid)
1145 world_db["ThingTypes"][id] = {
1146 "TT_NAME": "(none)",
1149 "TT_PROLIFERATE": 0,
1150 "TT_START_NUMBER": 0,
1156 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1160 def command_ttname(name):
1161 """Set TT_NAME of selected ThingType."""
1162 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1166 def command_ttsymbol(char):
1167 """Set TT_SYMBOL of selected ThingType. """
1169 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1171 print("Ignoring: Argument must be single character.")
1175 def command_ttcorpseid(str_int):
1176 """Set TT_CORPSE_ID of selected ThingType."""
1177 val = integer_test(str_int, 0, 255)
1179 if val in world_db["ThingTypes"]:
1180 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1182 print("Ignoring: Corpse ID belongs to no known ThignType.")
1185 def command_taid(id_string):
1186 """Set ID of ThingAction to manipulate. ID unused? Create new one.
1188 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1190 id = id_setter(id_string, "ThingActions", command_taid, True)
1192 world_db["ThingActions"][id] = {
1198 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1201 @test_ThingAction_id
1202 def command_taname(name):
1203 """Set TA_NAME of selected ThingAction.
1205 The name must match a valid thing action function. If after the name
1206 setting no ThingAction with name "wait" remains, call set_world_inactive().
1208 if name == "wait" or name == "move" or name == "use" or name == "drop" \
1209 or name == "pick_up":
1210 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1211 if 1 == world_db["WORLD_ACTIVE"]:
1212 wait_defined = False
1213 for id in world_db["ThingActions"]:
1214 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1217 if not wait_defined:
1218 set_world_inactive()
1220 print("Ignoring: Invalid action name.")
1221 # In contrast to the original,naming won't map a function to a ThingAction.
1224 """Commands database.
1226 Map command start tokens to ([0]) number of expected command arguments, ([1])
1227 the command's meta-ness (i.e. is it to be written to the record file, is it to
1228 be ignored in replay mode if read from server input file), and ([2]) a function
1232 "QUIT": (0, True, command_quit),
1233 "PING": (0, True, command_ping),
1234 "THINGS_HERE": (2, True, command_thingshere),
1235 "MAKE_WORLD": (1, False, command_makeworld),
1236 "SEED_MAP": (1, False, command_seedmap),
1237 "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1238 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1239 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1240 "MAP_LENGTH": (1, False, command_maplength),
1241 "WORLD_ACTIVE": (1, False, command_worldactive),
1242 "TA_ID": (1, False, command_taid),
1243 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1244 "TA_NAME": (1, False, command_taname),
1245 "TT_ID": (1, False, command_ttid),
1246 "TT_NAME": (1, False, command_ttname),
1247 "TT_SYMBOL": (1, False, command_ttsymbol),
1248 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1249 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1251 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1253 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1255 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1256 "T_ID": (1, False, command_tid),
1257 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1258 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1259 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1260 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1261 "T_COMMAND": (1, False, command_tcommand),
1262 "T_TYPE": (1, False, command_ttype),
1263 "T_CARRIES": (1, False, command_tcarries),
1264 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1265 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1266 "T_MEMTHING": (3, False, command_tmemthing),
1267 "T_POSY": (1, False, setter_tpos("Y")),
1268 "T_POSX": (1, False, setter_tpos("X")),
1269 "wait": (0, False, play_commander("wait")),
1270 "move": (1, False, play_commander("move")),
1271 "pick_up": (0, False, play_commander("pick_up")),
1272 "drop": (1, False, play_commander("drop", True)),
1273 "use": (1, False, play_commander("use", True)),
1277 """World state database. With sane default values. (Randomness is in rand.)"""
1289 """Mapping of direction names to internal direction chars."""
1290 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1291 "west": "s", "north-west": "w", "north-east": "e"}
1293 """File IO database."""
1295 "path_save": "save",
1296 "path_record": "record",
1297 "path_worldconf": "confserver/world",
1298 "path_server": "server/",
1299 "path_in": "server/in",
1300 "path_out": "server/out",
1301 "path_worldstate": "server/worldstate",
1302 "tmp_suffix": "_tmp",
1303 "kicked_by_rival": False,
1304 "worldstate_updateable": False
1309 libpr = prep_library()
1310 rand = RandomnessIO()
1311 opts = parse_command_line_arguments()
1313 if None != opts.replay:
1317 except SystemExit as exit:
1318 print("ABORTING: " + exit.args[0])
1320 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")