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_POSY"]
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")
572 """Make t do nothing (but loudly, if player avatar)."""
573 if t == world_db["Things"][0]:
574 strong_write(io_db["file_out"], "LOG You wait.\n")
578 """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
579 dir_c = t["T_ARGUMENT"].encode("ascii")[0]
580 legal_move = libpr.mv_yx_in_dir_legal_wrap(dir_c, t["T_POSY"], t["T_POSX"])
583 raise SystemExit("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
584 elif 1 == legal_move:
585 pos = (libpr.result_y() * world_db["MAP_LENGTH"]) + libpr.result_x()
586 passable = "." == chr(world_db["MAP"][pos])
587 hitted = [id for id in world_db["Things"]
588 if world_db["Things"][id] != t
589 if world_db["Things"][id]["T_LIFEPOINTS"]
590 if world_db["Things"][id]["T_POSY"] == libpr.result_y()
591 if world_db["Things"][id]["T_POSX"] == libpr.result_x()]
594 hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
595 hitter = "You" if t == world_db["Things"][0] else hitter_name
596 hitted_type = world_db["Things"][hit_id]["T_TYPE"]
597 hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
598 hitted = "you" if hit_id == 0 else hitted_name
599 verb = " wound " if hitter == "You" else " wounds "
600 strong_write(io_db["file_out"], "LOG " + hitter + verb + hitted + \
602 decrement_lifepoints(world_db["Things"][hit_id])
604 dir = [dir for dir in directions_db
605 if directions_db[dir] == t["T_ARGUMENT"]][0]
607 t["T_POSY"] = libpr.result_y()
608 t["T_POSX"] = libpr.result_x()
609 for id in t["T_CARRIES"]:
610 world_db["Things"][id]["T_POSY"] = libpr.result_y()
611 world_db["Things"][id]["T_POSX"] = libpr.result_x()
613 strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
615 strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
618 def actor_pick_up(t):
619 """Make t pick up (topmost?) Thing from ground into inventory."""
620 # Topmostness is actually not defined so far.
621 ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
622 if not world_db["Things"][id]["carried"]
623 if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
624 if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
626 world_db["Things"][ids[0]]["carried"] = True
627 t["T_CARRIES"].append(ids[0])
628 if t == world_db["Things"][0]:
629 strong_write(io_db["file_out"], "LOG You pick up an object.\n")
630 elif t == world_db["Things"][0]:
631 err = "You try to pick up an object, but there is none."
632 strong_write(io_db["file_out"], "LOG " + err + "\n")
636 """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
637 # TODO: Handle case where T_ARGUMENT matches nothing.
638 if len(t["T_CARRIES"]):
639 id = t["T_CARRIES"][t["T_ARGUMENT"]]
640 t["T_CARRIES"].remove(id)
641 world_db["Things"][id]["carried"] = False
642 if t == world_db["Things"][0]:
643 strong_write(io_db["file_out"], "LOG You drop an object.\n")
644 elif t == world_db["Things"][0]:
645 err = "You try to drop an object, but you own none."
646 strong_write(io_db["file_out"], "LOG " + err + "\n")
650 """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
651 # Original wrongly featured lifepoints increase through consumable!
652 # TODO: Handle case where T_ARGUMENT matches nothing.
653 if len(t["T_CARRIES"]):
654 id = t["T_CARRIES"][t["T_ARGUMENT"]]
655 type = world_db["Things"][id]["T_TYPE"]
656 if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
657 t["T_CARRIES"].remove(id)
658 del world_db["Things"][id]
659 t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
660 strong_write(io_db["file_out"], "LOG You consume this object.\n")
662 strong_write(io_db["file_out"], "LOG You try to use this object," +
665 strong_write(io_db["file_out"], "LOG You try to use an object, but " +
670 """Run game world and its inhabitants until new player input expected."""
673 while world_db["Things"][0]["T_LIFEPOINTS"]:
674 for id in [id for id in world_db["Things"]
675 if world_db["Things"][id]["T_LIFEPOINTS"]]:
676 Thing = world_db["Things"][id]
677 if Thing["T_LIFEPOINTS"]:
678 if not Thing["T_COMMAND"]:
679 update_map_memory(Thing)
684 Thing["T_COMMAND"] = 1
686 Thing["T_PROGRESS"] += 1
687 taid = [a for a in world_db["ThingActions"]
688 if a == Thing["T_COMMAND"]][0]
689 ThingAction = world_db["ThingActions"][taid]
690 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
691 eval("actor_" + ThingAction["TA_NAME"])(Thing)
692 Thing["T_COMMAND"] = 0
693 Thing["T_PROGRESS"] = 0
695 # DUMMY: thingproliferation
698 world_db["TURN"] += 1
701 def new_Thing(type, pos=(0,0)):
702 """Return Thing of type T_TYPE, with fovmap if alive and world active."""
704 "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
716 "T_MEMDEPTHMAP": False,
719 if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
724 def id_setter(id, category, id_store=False, start_at_1=False):
725 """Set ID of object of category to manipulate ID unused? Create new one.
727 The ID is stored as id_store.id (if id_store is set). If the integer of the
728 input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
729 32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
730 >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
731 new object is created, otherwise the new object's ID.
733 min = 0 if start_at_1 else -32768
734 max = 255 if start_at_1 else 32767
736 id = integer_test(id, min, max)
738 if id in world_db[category]:
743 if (start_at_1 and 0 == id) \
744 or ((not start_at_1) and (id < 0 or id > 255)):
748 if id not in world_db[category]:
752 "No unused ID available to add to ID list.")
760 """Send PONG line to server output file."""
761 strong_write(io_db["file_out"], "PONG\n")
765 """Abort server process."""
766 raise SystemExit("received QUIT command")
769 def command_thingshere(str_y, str_x):
770 """Write to out file list of Things known to player at coordinate y, x."""
771 def write_thing_if_here():
772 if y == world_db["Things"][id]["T_POSY"] \
773 and x == world_db["Things"][id]["T_POSX"] \
774 and not world_db["Things"][id]["carried"]:
775 type = world_db["Things"][id]["T_TYPE"]
776 name = world_db["ThingTypes"][type]["TT_NAME"]
777 strong_write(io_db["file_out"], name + "\n")
778 if world_db["WORLD_ACTIVE"]:
779 y = integer_test(str_y, 0, 255)
780 x = integer_test(str_x, 0, 255)
781 length = world_db["MAP_LENGTH"]
782 if None != y and None != x and y < length and x < length:
783 pos = (y * world_db["MAP_LENGTH"]) + x
784 strong_write(io_db["file_out"], "THINGS_HERE START\n")
785 if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
786 for id in world_db["Things"]:
787 write_thing_if_here()
789 for id in world_db["Things"]["T_MEMTHING"]:
790 write_thing_if_here()
791 strong_write(io_db["file_out"], "THINGS_HERE END\n")
793 print("Ignoring: Invalid map coordinates.")
795 print("Ignoring: Command only works on existing worlds.")
798 def play_commander(action, args=False):
799 """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
801 T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
805 id = [x for x in world_db["ThingActions"]
806 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
807 world_db["Things"][0]["T_COMMAND"] = id
810 def set_command_and_argument_int(str_arg):
811 val = integer_test(str_arg, 0, 255)
813 world_db["Things"][0]["T_ARGUMENT"] = val
816 def set_command_and_argument_movestring(str_arg):
817 if str_arg in directions_db:
818 world_db["Things"][0]["T_ARGUMENT"] = directions_db[str_arg]
821 print("Ignoring: Argument must be valid direction string.")
824 return set_command_and_argument_movestring
826 return set_command_and_argument_int
831 def command_seedrandomness(seed_string):
832 """Set rand seed to int(seed_string)."""
833 val = integer_test(seed_string, 0, 4294967295)
838 def command_seedmap(seed_string):
839 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
840 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
844 def command_makeworld(seed_string):
845 """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
847 Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
848 "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
849 TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
850 and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
851 according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
852 of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
853 other. Init player's memory map. Write "NEW_WORLD" line to out file.
859 err = "Space to put thing on too hard to find. Map too small?"
861 y = rand.next() % world_db["MAP_LENGTH"]
862 x = rand.next() % world_db["MAP_LENGTH"]
863 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
867 raise SystemExit(err)
868 # Replica of C code, wrongly ignores animatedness of new Thing.
869 pos_clear = (0 == len([id for id in world_db["Things"]
870 if world_db["Things"][id]["T_LIFEPOINTS"]
871 if world_db["Things"][id]["T_POSY"] == y
872 if world_db["Things"][id]["T_POSX"] == x]))
877 val = integer_test(seed_string, 0, 4294967295)
881 world_db["SEED_MAP"] = val
882 player_will_be_generated = False
883 playertype = world_db["PLAYER_TYPE"]
884 for ThingType in world_db["ThingTypes"]:
885 if playertype == ThingType:
886 if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
887 player_will_be_generated = True
889 if not player_will_be_generated:
890 print("Ignoring beyond SEED_MAP: " +
891 "No player type with start number >0 defined.")
894 for ThingAction in world_db["ThingActions"]:
895 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
898 print("Ignoring beyond SEED_MAP: " +
899 "No thing action with name 'wait' defined.")
901 world_db["Things"] = {}
903 world_db["WORLD_ACTIVE"] = 1
905 for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
906 id = id_setter(-1, "Things")
907 world_db["Things"][id] = new_Thing(playertype, free_pos())
908 update_map_memory(world_db["Things"][0])
909 for type in world_db["ThingTypes"]:
910 for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
911 if type != playertype:
912 id = id_setter(-1, "Things")
913 world_db["Things"][id] = new_Thing(type, free_pos())
914 strong_write(io_db["file_out"], "NEW_WORLD\n")
917 def command_maplength(maplength_string):
918 """Redefine map length. Invalidate map, therefore lose all things on it."""
919 val = integer_test(maplength_string, 1, 256)
921 world_db["MAP_LENGTH"] = val
923 world_db["Things"] = {}
924 libpr.set_maplength(val)
927 def command_worldactive(worldactive_string):
928 """Toggle world_db["WORLD_ACTIVE"] if possible.
930 An active world can always be set inactive. An inactive world can only be
931 set active with a "wait" ThingAction, and a player Thing (of ID 0). On
932 activation, rebuild all Things' FOVs, and the player's map memory.
934 # In original version, map existence was also tested (unnecessarily?).
935 val = integer_test(worldactive_string, 0, 1)
937 if 0 != world_db["WORLD_ACTIVE"]:
941 print("World already active.")
942 elif 0 == world_db["WORLD_ACTIVE"]:
944 for ThingAction in world_db["ThingActions"]:
945 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
948 player_exists = False
949 for Thing in world_db["Things"]:
953 if wait_exists and player_exists:
954 for id in world_db["Things"]:
955 if world_db["Things"][id]["T_LIFEPOINTS"]:
956 build_fov_map(world_db["Things"][id])
958 update_map_memory(world_db["Things"][id])
959 world_db["WORLD_ACTIVE"] = 1
962 def test_for_id_maker(object, category):
963 """Return decorator testing for object having "id" attribute."""
966 if hasattr(object, "id"):
969 print("Ignoring: No " + category +
970 " defined to manipulate yet.")
975 def command_tid(id_string):
976 """Set ID of Thing to manipulate. ID unused? Create new one.
978 Default new Thing's type to the first available ThingType, others: zero.
980 id = id_setter(id_string, "Things", command_tid)
982 if world_db["ThingTypes"] == {}:
983 print("Ignoring: No ThingType to settle new Thing in.")
985 type = list(world_db["ThingTypes"].keys())[0]
986 world_db["Things"][id] = new_Thing(type)
989 test_Thing_id = test_for_id_maker(command_tid, "Thing")
993 def command_tcommand(str_int):
994 """Set T_COMMAND of selected Thing."""
995 val = integer_test(str_int, 0, 255)
997 if 0 == val or val in world_db["ThingActions"]:
998 world_db["Things"][command_tid.id]["T_COMMAND"] = val
1000 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1004 def command_ttype(str_int):
1005 """Set T_TYPE of selected Thing."""
1006 val = integer_test(str_int, 0, 255)
1008 if val in world_db["ThingTypes"]:
1009 world_db["Things"][command_tid.id]["T_TYPE"] = val
1011 print("Ignoring: ThingType ID belongs to no known ThingType.")
1015 def command_tcarries(str_int):
1016 """Append int(str_int) to T_CARRIES of selected Thing.
1018 The ID int(str_int) must not be of the selected Thing, and must belong to a
1019 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1021 val = integer_test(str_int, 0, 255)
1023 if val == command_tid.id:
1024 print("Ignoring: Thing cannot carry itself.")
1025 elif val in world_db["Things"] \
1026 and not world_db["Things"][val]["carried"]:
1027 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1028 world_db["Things"][val]["carried"] = True
1030 print("Ignoring: Thing not available for carrying.")
1031 # Note that the whole carrying structure is different from the C version:
1032 # Carried-ness is marked by a "carried" flag, not by Things containing
1033 # Things internally.
1037 def command_tmemthing(str_t, str_y, str_x):
1038 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1040 The type must fit to an existing ThingType, and the position into the map.
1042 type = integer_test(str_t, 0, 255)
1043 posy = integer_test(str_y, 0, 255)
1044 posx = integer_test(str_x, 0, 255)
1045 if None != type and None != posy and None != posx:
1046 if type not in world_db["ThingTypes"] \
1047 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1048 print("Ignoring: Illegal value for thing type or position.")
1050 memthing = (type, posy, posx)
1051 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1054 def setter_map(maptype):
1055 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
1057 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
1060 def helper(str_int, mapline):
1061 val = integer_test(str_int, 0, 255)
1063 if val >= world_db["MAP_LENGTH"]:
1064 print("Illegal value for map line number.")
1065 elif len(mapline) != world_db["MAP_LENGTH"]:
1066 print("Map line length is unequal map width.")
1068 length = world_db["MAP_LENGTH"]
1070 if not world_db["Things"][command_tid.id][maptype]:
1071 map = bytearray(b' ' * (length ** 2))
1073 map = world_db["Things"][command_tid.id][maptype]
1074 map[val * length:(val * length) + length] = mapline.encode()
1075 world_db["Things"][command_tid.id][maptype] = map
1079 def setter_tpos(axis):
1080 """Generate setter for T_POSX or T_POSY of selected Thing.
1082 If world is active, rebuilds animate things' fovmap, player's memory map.
1085 def helper(str_int):
1086 val = integer_test(str_int, 0, 255)
1088 if val < world_db["MAP_LENGTH"]:
1089 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1090 if world_db["WORLD_ACTIVE"] \
1091 and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1092 build_fov_map(world_db["Things"][command_tid.id])
1093 if 0 == command_tid.id:
1094 update_map_memory(world_db["Things"][command_tid.id])
1096 print("Ignoring: Position is outside of map.")
1100 def command_ttid(id_string):
1101 """Set ID of ThingType to manipulate. ID unused? Create new one.
1103 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1105 id = id_setter(id_string, "ThingTypes", command_ttid)
1107 world_db["ThingTypes"][id] = {
1108 "TT_NAME": "(none)",
1111 "TT_PROLIFERATE": 0,
1112 "TT_START_NUMBER": 0,
1118 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1122 def command_ttname(name):
1123 """Set TT_NAME of selected ThingType."""
1124 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1128 def command_ttsymbol(char):
1129 """Set TT_SYMBOL of selected ThingType. """
1131 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1133 print("Ignoring: Argument must be single character.")
1137 def command_ttcorpseid(str_int):
1138 """Set TT_CORPSE_ID of selected ThingType."""
1139 val = integer_test(str_int, 0, 255)
1141 if val in world_db["ThingTypes"]:
1142 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1144 print("Ignoring: Corpse ID belongs to no known ThignType.")
1147 def command_taid(id_string):
1148 """Set ID of ThingAction to manipulate. ID unused? Create new one.
1150 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1152 id = id_setter(id_string, "ThingActions", command_taid, True)
1154 world_db["ThingActions"][id] = {
1160 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1163 @test_ThingAction_id
1164 def command_taname(name):
1165 """Set TA_NAME of selected ThingAction.
1167 The name must match a valid thing action function. If after the name
1168 setting no ThingAction with name "wait" remains, call set_world_inactive().
1170 if name == "wait" or name == "move" or name == "use" or name == "drop" \
1171 or name == "pick_up":
1172 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1173 if 1 == world_db["WORLD_ACTIVE"]:
1174 wait_defined = False
1175 for id in world_db["ThingActions"]:
1176 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1179 if not wait_defined:
1180 set_world_inactive()
1182 print("Ignoring: Invalid action name.")
1183 # In contrast to the original,naming won't map a function to a ThingAction.
1186 """Commands database.
1188 Map command start tokens to ([0]) number of expected command arguments, ([1])
1189 the command's meta-ness (i.e. is it to be written to the record file, is it to
1190 be ignored in replay mode if read from server input file), and ([2]) a function
1194 "QUIT": (0, True, command_quit),
1195 "PING": (0, True, command_ping),
1196 "THINGS_HERE": (2, True, command_thingshere),
1197 "MAKE_WORLD": (1, False, command_makeworld),
1198 "SEED_MAP": (1, False, command_seedmap),
1199 "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1200 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1201 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1202 "MAP_LENGTH": (1, False, command_maplength),
1203 "WORLD_ACTIVE": (1, False, command_worldactive),
1204 "TA_ID": (1, False, command_taid),
1205 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1206 "TA_NAME": (1, False, command_taname),
1207 "TT_ID": (1, False, command_ttid),
1208 "TT_NAME": (1, False, command_ttname),
1209 "TT_SYMBOL": (1, False, command_ttsymbol),
1210 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1211 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1213 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1215 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1217 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1218 "T_ID": (1, False, command_tid),
1219 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1220 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1221 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1222 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1223 "T_COMMAND": (1, False, command_tcommand),
1224 "T_TYPE": (1, False, command_ttype),
1225 "T_CARRIES": (1, False, command_tcarries),
1226 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1227 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1228 "T_MEMTHING": (3, False, command_tmemthing),
1229 "T_POSY": (1, False, setter_tpos("Y")),
1230 "T_POSX": (1, False, setter_tpos("X")),
1231 "wait": (0, False, play_commander("wait")),
1232 "move": (1, False, play_commander("move")),
1233 "pick_up": (0, False, play_commander("pick_up")),
1234 "drop": (1, False, play_commander("drop", True)),
1235 "use": (1, False, play_commander("use", True)),
1239 """World state database. With sane default values. (Randomness is in rand.)"""
1251 """Mapping of direction names to internal direction chars."""
1252 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1253 "west": "s", "north-west": "w", "north-east": "e"}
1255 """File IO database."""
1257 "path_save": "save",
1258 "path_record": "record",
1259 "path_worldconf": "confserver/world",
1260 "path_server": "server/",
1261 "path_in": "server/in",
1262 "path_out": "server/out",
1263 "path_worldstate": "server/worldstate",
1264 "tmp_suffix": "_tmp",
1265 "kicked_by_rival": False,
1266 "worldstate_updateable": False
1271 libpr = prep_library()
1272 rand = RandomnessIO()
1273 opts = parse_command_line_arguments()
1275 if None != opts.replay:
1279 except SystemExit as exit:
1280 print("ABORTING: " + exit.args[0])
1282 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")