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")
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."""
588 # dir_c = t["T_ARGUMENT"].encode("ascii")[0]
589 # legal_move = libpr.mv_yx_in_dir_legal_wrap(dir_c, t["T_POSY"], t["T_POSX"])
591 # if -1 == legal_move:
592 # raise SystemExit("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
593 move_result = mv_yx_in_dir_legal(t["T_ARGUMENT"], t["T_POSY"], t["T_POSX"])
594 if 1 == move_result[0]:
595 pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
596 passable = "." == chr(world_db["MAP"][pos])
597 hitted = [id for id in world_db["Things"]
598 if world_db["Things"][id] != t
599 if world_db["Things"][id]["T_LIFEPOINTS"]
600 if world_db["Things"][id]["T_POSY"] == move_result[1]
601 if world_db["Things"][id]["T_POSX"] == move_result[2]]
604 hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
605 hitter = "You" if t == world_db["Things"][0] else hitter_name
606 hitted_type = world_db["Things"][hit_id]["T_TYPE"]
607 hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
608 hitted = "you" if hit_id == 0 else hitted_name
609 verb = " wound " if hitter == "You" else " wounds "
610 strong_write(io_db["file_out"], "LOG " + hitter + verb + hitted + \
612 decrement_lifepoints(world_db["Things"][hit_id])
614 dir = [dir for dir in directions_db
615 if directions_db[dir] == t["T_ARGUMENT"]][0]
617 t["T_POSY"] = move_result[1]
618 t["T_POSX"] = move_result[2]
619 for id in t["T_CARRIES"]:
620 world_db["Things"][id]["T_POSY"] = move_result[1]
621 world_db["Things"][id]["T_POSX"] = move_result[2]
623 strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
625 strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
628 def actor_pick_up(t):
629 """Make t pick up (topmost?) Thing from ground into inventory."""
630 # Topmostness is actually not defined so far.
631 ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
632 if not world_db["Things"][id]["carried"]
633 if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
634 if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
636 world_db["Things"][ids[0]]["carried"] = True
637 t["T_CARRIES"].append(ids[0])
638 if t == world_db["Things"][0]:
639 strong_write(io_db["file_out"], "LOG You pick up an object.\n")
640 elif t == world_db["Things"][0]:
641 err = "You try to pick up an object, but there is none."
642 strong_write(io_db["file_out"], "LOG " + err + "\n")
646 """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
647 # TODO: Handle case where T_ARGUMENT matches nothing.
648 if len(t["T_CARRIES"]):
649 id = t["T_CARRIES"][t["T_ARGUMENT"]]
650 t["T_CARRIES"].remove(id)
651 world_db["Things"][id]["carried"] = False
652 if t == world_db["Things"][0]:
653 strong_write(io_db["file_out"], "LOG You drop an object.\n")
654 elif t == world_db["Things"][0]:
655 err = "You try to drop an object, but you own none."
656 strong_write(io_db["file_out"], "LOG " + err + "\n")
660 """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
661 # Original wrongly featured lifepoints increase through consumable!
662 # TODO: Handle case where T_ARGUMENT matches nothing.
663 if len(t["T_CARRIES"]):
664 id = t["T_CARRIES"][t["T_ARGUMENT"]]
665 type = world_db["Things"][id]["T_TYPE"]
666 if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
667 t["T_CARRIES"].remove(id)
668 del world_db["Things"][id]
669 t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
670 strong_write(io_db["file_out"], "LOG You consume this object.\n")
672 strong_write(io_db["file_out"], "LOG You try to use this object," +
675 strong_write(io_db["file_out"], "LOG You try to use an object, but " +
680 """Run game world and its inhabitants until new player input expected."""
683 while world_db["Things"][0]["T_LIFEPOINTS"]:
684 for id in [id for id in world_db["Things"]
685 if world_db["Things"][id]["T_LIFEPOINTS"]]:
686 Thing = world_db["Things"][id]
687 if Thing["T_LIFEPOINTS"]:
688 if not Thing["T_COMMAND"]:
689 update_map_memory(Thing)
694 Thing["T_COMMAND"] = 1
696 Thing["T_PROGRESS"] += 1
697 taid = [a for a in world_db["ThingActions"]
698 if a == Thing["T_COMMAND"]][0]
699 ThingAction = world_db["ThingActions"][taid]
700 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
701 eval("actor_" + ThingAction["TA_NAME"])(Thing)
702 Thing["T_COMMAND"] = 0
703 Thing["T_PROGRESS"] = 0
705 # DUMMY: thingproliferation
708 world_db["TURN"] += 1
711 def new_Thing(type, pos=(0,0)):
712 """Return Thing of type T_TYPE, with fovmap if alive and world active."""
714 "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
726 "T_MEMDEPTHMAP": False,
729 if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
734 def id_setter(id, category, id_store=False, start_at_1=False):
735 """Set ID of object of category to manipulate ID unused? Create new one.
737 The ID is stored as id_store.id (if id_store is set). If the integer of the
738 input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
739 32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
740 >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
741 new object is created, otherwise the new object's ID.
743 min = 0 if start_at_1 else -32768
744 max = 255 if start_at_1 else 32767
746 id = integer_test(id, min, max)
748 if id in world_db[category]:
753 if (start_at_1 and 0 == id) \
754 or ((not start_at_1) and (id < 0 or id > 255)):
758 if id not in world_db[category]:
762 "No unused ID available to add to ID list.")
770 """Send PONG line to server output file."""
771 strong_write(io_db["file_out"], "PONG\n")
775 """Abort server process."""
776 raise SystemExit("received QUIT command")
779 def command_thingshere(str_y, str_x):
780 """Write to out file list of Things known to player at coordinate y, x."""
781 def write_thing_if_here():
782 if y == world_db["Things"][id]["T_POSY"] \
783 and x == world_db["Things"][id]["T_POSX"] \
784 and not world_db["Things"][id]["carried"]:
785 type = world_db["Things"][id]["T_TYPE"]
786 name = world_db["ThingTypes"][type]["TT_NAME"]
787 strong_write(io_db["file_out"], name + "\n")
788 if world_db["WORLD_ACTIVE"]:
789 y = integer_test(str_y, 0, 255)
790 x = integer_test(str_x, 0, 255)
791 length = world_db["MAP_LENGTH"]
792 if None != y and None != x and y < length and x < length:
793 pos = (y * world_db["MAP_LENGTH"]) + x
794 strong_write(io_db["file_out"], "THINGS_HERE START\n")
795 if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
796 for id in world_db["Things"]:
797 write_thing_if_here()
799 for id in world_db["Things"]["T_MEMTHING"]:
800 write_thing_if_here()
801 strong_write(io_db["file_out"], "THINGS_HERE END\n")
803 print("Ignoring: Invalid map coordinates.")
805 print("Ignoring: Command only works on existing worlds.")
808 def play_commander(action, args=False):
809 """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
811 T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
815 id = [x for x in world_db["ThingActions"]
816 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
817 world_db["Things"][0]["T_COMMAND"] = id
820 def set_command_and_argument_int(str_arg):
821 val = integer_test(str_arg, 0, 255)
823 world_db["Things"][0]["T_ARGUMENT"] = val
826 def set_command_and_argument_movestring(str_arg):
827 if str_arg in directions_db:
828 world_db["Things"][0]["T_ARGUMENT"] = directions_db[str_arg]
831 print("Ignoring: Argument must be valid direction string.")
834 return set_command_and_argument_movestring
836 return set_command_and_argument_int
841 def command_seedrandomness(seed_string):
842 """Set rand seed to int(seed_string)."""
843 val = integer_test(seed_string, 0, 4294967295)
848 def command_seedmap(seed_string):
849 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
850 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
854 def command_makeworld(seed_string):
855 """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
857 Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
858 "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
859 TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
860 and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
861 according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
862 of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
863 other. Init player's memory map. Write "NEW_WORLD" line to out file.
869 err = "Space to put thing on too hard to find. Map too small?"
871 y = rand.next() % world_db["MAP_LENGTH"]
872 x = rand.next() % world_db["MAP_LENGTH"]
873 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
877 raise SystemExit(err)
878 # Replica of C code, wrongly ignores animatedness of new Thing.
879 pos_clear = (0 == len([id for id in world_db["Things"]
880 if world_db["Things"][id]["T_LIFEPOINTS"]
881 if world_db["Things"][id]["T_POSY"] == y
882 if world_db["Things"][id]["T_POSX"] == x]))
887 val = integer_test(seed_string, 0, 4294967295)
891 world_db["SEED_MAP"] = val
892 player_will_be_generated = False
893 playertype = world_db["PLAYER_TYPE"]
894 for ThingType in world_db["ThingTypes"]:
895 if playertype == ThingType:
896 if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
897 player_will_be_generated = True
899 if not player_will_be_generated:
900 print("Ignoring beyond SEED_MAP: " +
901 "No player type with start number >0 defined.")
904 for ThingAction in world_db["ThingActions"]:
905 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
908 print("Ignoring beyond SEED_MAP: " +
909 "No thing action with name 'wait' defined.")
911 world_db["Things"] = {}
913 world_db["WORLD_ACTIVE"] = 1
915 for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
916 id = id_setter(-1, "Things")
917 world_db["Things"][id] = new_Thing(playertype, free_pos())
918 update_map_memory(world_db["Things"][0])
919 for type in world_db["ThingTypes"]:
920 for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
921 if type != playertype:
922 id = id_setter(-1, "Things")
923 world_db["Things"][id] = new_Thing(type, free_pos())
924 strong_write(io_db["file_out"], "NEW_WORLD\n")
927 def command_maplength(maplength_string):
928 """Redefine map length. Invalidate map, therefore lose all things on it."""
929 val = integer_test(maplength_string, 1, 256)
931 world_db["MAP_LENGTH"] = val
933 world_db["Things"] = {}
934 libpr.set_maplength(val)
937 def command_worldactive(worldactive_string):
938 """Toggle world_db["WORLD_ACTIVE"] if possible.
940 An active world can always be set inactive. An inactive world can only be
941 set active with a "wait" ThingAction, and a player Thing (of ID 0). On
942 activation, rebuild all Things' FOVs, and the player's map memory.
944 # In original version, map existence was also tested (unnecessarily?).
945 val = integer_test(worldactive_string, 0, 1)
947 if 0 != world_db["WORLD_ACTIVE"]:
951 print("World already active.")
952 elif 0 == world_db["WORLD_ACTIVE"]:
954 for ThingAction in world_db["ThingActions"]:
955 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
958 player_exists = False
959 for Thing in world_db["Things"]:
963 if wait_exists and player_exists:
964 for id in world_db["Things"]:
965 if world_db["Things"][id]["T_LIFEPOINTS"]:
966 build_fov_map(world_db["Things"][id])
968 update_map_memory(world_db["Things"][id])
969 world_db["WORLD_ACTIVE"] = 1
972 def test_for_id_maker(object, category):
973 """Return decorator testing for object having "id" attribute."""
976 if hasattr(object, "id"):
979 print("Ignoring: No " + category +
980 " defined to manipulate yet.")
985 def command_tid(id_string):
986 """Set ID of Thing to manipulate. ID unused? Create new one.
988 Default new Thing's type to the first available ThingType, others: zero.
990 id = id_setter(id_string, "Things", command_tid)
992 if world_db["ThingTypes"] == {}:
993 print("Ignoring: No ThingType to settle new Thing in.")
995 type = list(world_db["ThingTypes"].keys())[0]
996 world_db["Things"][id] = new_Thing(type)
999 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1003 def command_tcommand(str_int):
1004 """Set T_COMMAND of selected Thing."""
1005 val = integer_test(str_int, 0, 255)
1007 if 0 == val or val in world_db["ThingActions"]:
1008 world_db["Things"][command_tid.id]["T_COMMAND"] = val
1010 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1014 def command_ttype(str_int):
1015 """Set T_TYPE of selected Thing."""
1016 val = integer_test(str_int, 0, 255)
1018 if val in world_db["ThingTypes"]:
1019 world_db["Things"][command_tid.id]["T_TYPE"] = val
1021 print("Ignoring: ThingType ID belongs to no known ThingType.")
1025 def command_tcarries(str_int):
1026 """Append int(str_int) to T_CARRIES of selected Thing.
1028 The ID int(str_int) must not be of the selected Thing, and must belong to a
1029 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1031 val = integer_test(str_int, 0, 255)
1033 if val == command_tid.id:
1034 print("Ignoring: Thing cannot carry itself.")
1035 elif val in world_db["Things"] \
1036 and not world_db["Things"][val]["carried"]:
1037 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1038 world_db["Things"][val]["carried"] = True
1040 print("Ignoring: Thing not available for carrying.")
1041 # Note that the whole carrying structure is different from the C version:
1042 # Carried-ness is marked by a "carried" flag, not by Things containing
1043 # Things internally.
1047 def command_tmemthing(str_t, str_y, str_x):
1048 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1050 The type must fit to an existing ThingType, and the position into the map.
1052 type = integer_test(str_t, 0, 255)
1053 posy = integer_test(str_y, 0, 255)
1054 posx = integer_test(str_x, 0, 255)
1055 if None != type and None != posy and None != posx:
1056 if type not in world_db["ThingTypes"] \
1057 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1058 print("Ignoring: Illegal value for thing type or position.")
1060 memthing = (type, posy, posx)
1061 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1064 def setter_map(maptype):
1065 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
1067 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
1070 def helper(str_int, mapline):
1071 val = integer_test(str_int, 0, 255)
1073 if val >= world_db["MAP_LENGTH"]:
1074 print("Illegal value for map line number.")
1075 elif len(mapline) != world_db["MAP_LENGTH"]:
1076 print("Map line length is unequal map width.")
1078 length = world_db["MAP_LENGTH"]
1080 if not world_db["Things"][command_tid.id][maptype]:
1081 map = bytearray(b' ' * (length ** 2))
1083 map = world_db["Things"][command_tid.id][maptype]
1084 map[val * length:(val * length) + length] = mapline.encode()
1085 world_db["Things"][command_tid.id][maptype] = map
1089 def setter_tpos(axis):
1090 """Generate setter for T_POSX or T_POSY of selected Thing.
1092 If world is active, rebuilds animate things' fovmap, player's memory map.
1095 def helper(str_int):
1096 val = integer_test(str_int, 0, 255)
1098 if val < world_db["MAP_LENGTH"]:
1099 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1100 if world_db["WORLD_ACTIVE"] \
1101 and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1102 build_fov_map(world_db["Things"][command_tid.id])
1103 if 0 == command_tid.id:
1104 update_map_memory(world_db["Things"][command_tid.id])
1106 print("Ignoring: Position is outside of map.")
1110 def command_ttid(id_string):
1111 """Set ID of ThingType to manipulate. ID unused? Create new one.
1113 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1115 id = id_setter(id_string, "ThingTypes", command_ttid)
1117 world_db["ThingTypes"][id] = {
1118 "TT_NAME": "(none)",
1121 "TT_PROLIFERATE": 0,
1122 "TT_START_NUMBER": 0,
1128 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1132 def command_ttname(name):
1133 """Set TT_NAME of selected ThingType."""
1134 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1138 def command_ttsymbol(char):
1139 """Set TT_SYMBOL of selected ThingType. """
1141 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1143 print("Ignoring: Argument must be single character.")
1147 def command_ttcorpseid(str_int):
1148 """Set TT_CORPSE_ID of selected ThingType."""
1149 val = integer_test(str_int, 0, 255)
1151 if val in world_db["ThingTypes"]:
1152 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1154 print("Ignoring: Corpse ID belongs to no known ThignType.")
1157 def command_taid(id_string):
1158 """Set ID of ThingAction to manipulate. ID unused? Create new one.
1160 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1162 id = id_setter(id_string, "ThingActions", command_taid, True)
1164 world_db["ThingActions"][id] = {
1170 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1173 @test_ThingAction_id
1174 def command_taname(name):
1175 """Set TA_NAME of selected ThingAction.
1177 The name must match a valid thing action function. If after the name
1178 setting no ThingAction with name "wait" remains, call set_world_inactive().
1180 if name == "wait" or name == "move" or name == "use" or name == "drop" \
1181 or name == "pick_up":
1182 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1183 if 1 == world_db["WORLD_ACTIVE"]:
1184 wait_defined = False
1185 for id in world_db["ThingActions"]:
1186 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1189 if not wait_defined:
1190 set_world_inactive()
1192 print("Ignoring: Invalid action name.")
1193 # In contrast to the original,naming won't map a function to a ThingAction.
1196 """Commands database.
1198 Map command start tokens to ([0]) number of expected command arguments, ([1])
1199 the command's meta-ness (i.e. is it to be written to the record file, is it to
1200 be ignored in replay mode if read from server input file), and ([2]) a function
1204 "QUIT": (0, True, command_quit),
1205 "PING": (0, True, command_ping),
1206 "THINGS_HERE": (2, True, command_thingshere),
1207 "MAKE_WORLD": (1, False, command_makeworld),
1208 "SEED_MAP": (1, False, command_seedmap),
1209 "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1210 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1211 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1212 "MAP_LENGTH": (1, False, command_maplength),
1213 "WORLD_ACTIVE": (1, False, command_worldactive),
1214 "TA_ID": (1, False, command_taid),
1215 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1216 "TA_NAME": (1, False, command_taname),
1217 "TT_ID": (1, False, command_ttid),
1218 "TT_NAME": (1, False, command_ttname),
1219 "TT_SYMBOL": (1, False, command_ttsymbol),
1220 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1221 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1223 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1225 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1227 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1228 "T_ID": (1, False, command_tid),
1229 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1230 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1231 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1232 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1233 "T_COMMAND": (1, False, command_tcommand),
1234 "T_TYPE": (1, False, command_ttype),
1235 "T_CARRIES": (1, False, command_tcarries),
1236 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1237 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1238 "T_MEMTHING": (3, False, command_tmemthing),
1239 "T_POSY": (1, False, setter_tpos("Y")),
1240 "T_POSX": (1, False, setter_tpos("X")),
1241 "wait": (0, False, play_commander("wait")),
1242 "move": (1, False, play_commander("move")),
1243 "pick_up": (0, False, play_commander("pick_up")),
1244 "drop": (1, False, play_commander("drop", True)),
1245 "use": (1, False, play_commander("use", True)),
1249 """World state database. With sane default values. (Randomness is in rand.)"""
1261 """Mapping of direction names to internal direction chars."""
1262 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1263 "west": "s", "north-west": "w", "north-east": "e"}
1265 """File IO database."""
1267 "path_save": "save",
1268 "path_record": "record",
1269 "path_worldconf": "confserver/world",
1270 "path_server": "server/",
1271 "path_in": "server/in",
1272 "path_out": "server/out",
1273 "path_worldstate": "server/worldstate",
1274 "tmp_suffix": "_tmp",
1275 "kicked_by_rival": False,
1276 "worldstate_updateable": False
1281 libpr = prep_library()
1282 rand = RandomnessIO()
1283 opts = parse_command_line_arguments()
1285 if None != opts.replay:
1289 except SystemExit as exit:
1290 print("ABORTING: " + exit.args[0])
1292 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")