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
38 def strong_write(file, string):
39 """Apply write(string), flush(), and os.fsync() to file."""
45 def setup_server_io():
46 """Fill IO files DB with proper file( path)s. Write process IO test string.
48 Ensure IO files directory at server/. Remove any old input file if found.
49 Set up new input file for reading, and new output file for writing. Start
50 output file with process hash line of format PID + " " + floated UNIX time
51 (io_db["teststring"]). Raise SystemExit if file is found at path of either
52 record or save file plus io_db["tmp_suffix"].
54 def detect_atomic_leftover(path, tmp_suffix):
55 path_tmp = path + tmp_suffix
56 msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
57 "aborted previous attempt to write '" + path + "'. Aborting " \
58 "until matter is resolved by removing it from its current path."
59 if os.access(path_tmp, os.F_OK):
61 io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
62 os.makedirs(io_db["path_server"], exist_ok=True)
63 io_db["file_out"] = open(io_db["path_out"], "w")
64 strong_write(io_db["file_out"], io_db["teststring"] + "\n")
65 if os.access(io_db["path_in"], os.F_OK):
66 os.remove(io_db["path_in"])
67 io_db["file_in"] = open(io_db["path_in"], "w")
68 io_db["file_in"].close()
69 io_db["file_in"] = open(io_db["path_in"], "r")
70 detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
71 detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
74 def cleanup_server_io():
75 """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
76 def helper(file_key, path_key):
78 io_db[file_key].close()
79 if not io_db["kicked_by_rival"] \
80 and os.access(io_db[path_key], os.F_OK):
81 os.remove(io_db[path_key])
82 helper("file_out", "path_out")
83 helper("file_in", "path_in")
84 helper("file_worldstate", "path_worldstate")
85 if "file_record" in io_db:
86 io_db["file_record"].close()
89 def obey(command, prefix, replay=False, do_record=False):
90 """Call function from commands_db mapped to command's first token.
92 Tokenize command string with shlex.split(comments=True). If replay is set,
93 a non-meta command from the commands_db merely triggers obey() on the next
94 command from the records file. If not, non-meta commands set
95 io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
96 do_record is set, are recorded via record(), and save_world() is called.
97 The prefix string is inserted into the server's input message between its
98 beginning 'input ' & ':'. All activity is preceded by a server_test() call.
101 print("input " + prefix + ": " + command)
103 tokens = shlex.split(command, comments=True)
104 except ValueError as err:
105 print("Can't tokenize command string: " + str(err) + ".")
107 if len(tokens) > 0 and tokens[0] in commands_db \
108 and len(tokens) == commands_db[tokens[0]][0] + 1:
109 if commands_db[tokens[0]][1]:
110 commands_db[tokens[0]][2](*tokens[1:])
112 print("Due to replay mode, reading command as 'go on in record'.")
113 line = io_db["file_record"].readline()
115 obey(line.rstrip(), io_db["file_record"].prefix
116 + str(io_db["file_record"].line_n))
117 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
119 print("Reached end of record file.")
121 commands_db[tokens[0]][2](*tokens[1:])
125 io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
126 elif 0 != len(tokens):
127 print("Invalid command/argument, or bad number of tokens.")
130 def atomic_write(path, text, do_append=False):
131 """Atomic write of text to file at path, appended if do_append is set."""
132 path_tmp = path + io_db["tmp_suffix"]
136 if os.access(path, os.F_OK):
137 shutil.copyfile(path, path_tmp)
138 file = open(path_tmp, mode)
139 strong_write(file, text)
141 if os.access(path, os.F_OK):
143 os.rename(path_tmp, path)
147 """Append command string plus newline to record file. (Atomic.)"""
148 # This misses some optimizations from the original record(), namely only
149 # finishing the atomic write with expensive flush() and fsync() every 15
150 # seconds unless explicitely forced. Implement as needed.
151 atomic_write(io_db["path_record"], command + "\n", do_append=True)
155 """Save all commands needed to reconstruct current world state."""
156 # TODO: Misses same optimizations as record() from the original record().
159 string = string.replace("\u005C", '\u005C\u005C')
160 return '"' + string.replace('"', '\u005C"') + '"'
165 if world_db["Things"][id][key]:
166 map = world_db["Things"][id][key]
167 length = world_db["MAP_LENGTH"]
168 for i in range(length):
169 line = map[i * length:(i * length) + length].decode()
170 string = string + key + " " + str(i) + " " + quote(line) \
177 for memthing in world_db["Things"][id]["T_MEMTHING"]:
178 string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
179 str(memthing[1]) + " " + str(memthing[2]) + "\n"
182 def helper(category, id_string, special_keys={}):
184 for id in world_db[category]:
185 string = string + id_string + " " + str(id) + "\n"
186 for key in world_db[category][id]:
187 if not key in special_keys:
188 x = world_db[category][id][key]
189 argument = quote(x) if str == type(x) else str(x)
190 string = string + key + " " + argument + "\n"
191 elif special_keys[key]:
192 string = string + special_keys[key](id)
197 if dict != type(world_db[key]) and key != "MAP":
198 string = string + key + " " + str(world_db[key]) + "\n"
199 string = string + helper("ThingActions", "TA_ID")
200 string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
201 for id in world_db["ThingTypes"]:
202 string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
203 str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
204 string = string + helper("Things", "T_ID",
205 {"T_CARRIES": False, "carried": False,
206 "T_MEMMAP": mapsetter("T_MEMMAP"),
207 "T_MEMTHING": memthing, "fovmap": False,
208 "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
209 for id in world_db["Things"]:
210 if [] != world_db["Things"][id]["T_CARRIES"]:
211 string = string + "T_ID " + str(id) + "\n"
212 for carried_id in world_db["Things"][id]["T_CARRIES"]:
213 string = string + "T_CARRIES " + str(carried_id) + "\n"
214 string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
215 "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
216 atomic_write(io_db["path_save"], string)
219 def obey_lines_in_file(path, name, do_record=False):
220 """Call obey() on each line of path's file, use name in input prefix."""
221 file = open(path, "r")
223 for line in file.readlines():
224 obey(line.rstrip(), name + "file line " + str(line_n),
230 def parse_command_line_arguments():
231 """Return settings values read from command line arguments."""
232 parser = argparse.ArgumentParser()
233 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
235 opts, unknown = parser.parse_known_args()
240 """Ensure valid server out file belonging to current process.
242 This is done by comparing io_db["teststring"] to what's found at the start
243 of the current file at io_db["path_out"]. On failure, set
244 io_db["kicked_by_rival"] and raise SystemExit.
246 if not os.access(io_db["path_out"], os.F_OK):
247 raise SystemExit("Server output file has disappeared.")
248 file = open(io_db["path_out"], "r")
249 test = file.readline().rstrip("\n")
251 if test != io_db["teststring"]:
252 io_db["kicked_by_rival"] = True
253 msg = "Server test string in server output file does not match. This" \
254 " indicates that the current server process has been " \
255 "superseded by another one."
256 raise SystemExit(msg)
260 """Return next newline-delimited command from server in file.
262 Keep building return string until a newline is encountered. Pause between
263 unsuccessful reads, and after too much waiting, run server_test().
270 add = io_db["file_in"].readline()
272 command = command + add
273 if len(command) > 0 and "\n" == command[-1]:
274 command = command[:-1]
277 time.sleep(wait_on_fail)
278 if now + max_wait < time.time():
284 def try_worldstate_update():
285 """Write worldstate file if io_db["worldstate_updateable"] is set."""
286 if io_db["worldstate_updateable"]:
288 def draw_visible_Things(map, run):
289 for id in world_db["Things"]:
290 type = world_db["Things"][id]["T_TYPE"]
291 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
292 alive = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]
293 if (0 == run and not consumable and not alive) \
294 or (1 == run and consumable and not alive) \
295 or (2 == run and alive):
296 y = world_db["Things"][id]["T_POSY"]
297 x = world_db["Things"][id]["T_POSX"]
298 fovflag = world_db["Things"][0]["fovmap"][(y * length) + x]
299 if 'v' == chr(fovflag):
300 c = world_db["ThingTypes"][type]["TT_SYMBOL"]
301 map[(y * length) + x] = ord(c)
303 def write_map(string, map):
304 for i in range(length):
305 line = map[i * length:(i * length) + length].decode()
306 string = string + line + "\n"
310 if [] == world_db["Things"][0]["T_CARRIES"]:
311 inventory = "(none)\n"
313 for id in world_db["Things"][0]["T_CARRIES"]:
314 type_id = world_db["Things"][id]["T_TYPE"]
315 name = world_db["ThingTypes"][type_id]["TT_NAME"]
316 inventory = inventory + name + "\n"
317 string = str(world_db["TURN"]) + "\n" + \
318 str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
319 str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
320 inventory + "%\n" + \
321 str(world_db["Things"][0]["T_POSY"]) + "\n" + \
322 str(world_db["Things"][0]["T_POSX"]) + "\n" + \
323 str(world_db["MAP_LENGTH"]) + "\n"
324 length = world_db["MAP_LENGTH"]
325 fov = bytearray(b' ' * (length ** 2))
326 for pos in range(length ** 2):
327 if 'v' == chr(world_db["Things"][0]["fovmap"][pos]):
328 fov[pos] = world_db["MAP"][pos]
330 draw_visible_Things(fov, i)
331 string = write_map(string, fov)
332 mem = world_db["Things"][0]["T_MEMMAP"][:]
334 for memthing in world_db["Things"][0]["T_MEMTHING"]:
335 type = world_db["Things"][memthing[0]]["T_TYPE"]
336 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
337 if (i == 0 and not consumable) or (i == 1 and consumable):
338 c = world_db["ThingTypes"][type]["TT_SYMBOL"]
339 mem[(memthing[1] * length) + memthing[2]] = ord(c)
340 string = write_map(string, mem)
341 atomic_write(io_db["path_worldstate"], string)
342 strong_write(io_db["file_out"], "WORLD_UPDATED\n")
343 io_db["worldstate_updateable"] = False
347 """Replay game from record file.
349 Use opts.replay as breakpoint turn to which to replay automatically before
350 switching to manual input by non-meta commands in server input file
351 triggering further reads of record file. Ensure opts.replay is at least 1.
352 Run try_worldstate_update() before each interactive obey()/read_command().
356 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
357 " (if so late a turn is to be found).")
358 if not os.access(io_db["path_record"], os.F_OK):
359 raise SystemExit("No record file found to replay.")
360 io_db["file_record"] = open(io_db["path_record"], "r")
361 io_db["file_record"].prefix = "record file line "
362 io_db["file_record"].line_n = 1
363 while world_db["TURN"] < opts.replay:
364 line = io_db["file_record"].readline()
367 obey(line.rstrip(), io_db["file_record"].prefix
368 + str(io_db["file_record"].line_n))
369 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
371 try_worldstate_update()
372 obey(read_command(), "in file", replay=True)
376 """Play game by server input file commands. Before, load save file found.
378 If no save file is found, a new world is generated from the commands in the
379 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
380 command and all that follow via the server input file. Run
381 try_worldstate_update() before each interactive obey()/read_command().
383 if os.access(io_db["path_save"], os.F_OK):
384 obey_lines_in_file(io_db["path_save"], "save")
386 if not os.access(io_db["path_worldconf"], os.F_OK):
387 msg = "No world config file from which to start a new world."
388 raise SystemExit(msg)
389 obey_lines_in_file(io_db["path_worldconf"], "world config ",
391 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
393 try_worldstate_update()
394 obey(read_command(), "in file", do_record=True)
398 """(Re-)make island map.
400 Let "~" represent water, "." land, "X" trees: Build island shape randomly,
401 start with one land cell in the middle, then go into cycle of repeatedly
402 selecting a random sea cell and transforming it into land if it is neighbor
403 to land. The cycle ends when a land cell is due to be created at the map's
404 border. Then put some trees on the map (TODO: more precise algorithm desc).
406 def is_neighbor(coordinates, type):
409 length = world_db["MAP_LENGTH"]
411 diag_west = x + (ind > 0)
412 diag_east = x + (ind < (length - 1))
413 pos = (y * length) + x
414 if (y > 0 and diag_east
415 and type == chr(world_db["MAP"][pos - length + ind])) \
417 and type == chr(world_db["MAP"][pos + 1])) \
418 or (y < (length - 1) and diag_east
419 and type == chr(world_db["MAP"][pos + length + ind])) \
420 or (y > 0 and diag_west
421 and type == chr(world_db["MAP"][pos - length - (not ind)])) \
423 and type == chr(world_db["MAP"][pos - 1])) \
424 or (y < (length - 1) and diag_west
425 and type == chr(world_db["MAP"][pos + length - (not ind)])):
428 store_seed = rand.seed
429 world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
430 length = world_db["MAP_LENGTH"]
431 add_half_width = (not (length % 2)) * int(length / 2)
432 world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
434 y = rand.next() % length
435 x = rand.next() % length
436 pos = (y * length) + x
437 if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
438 if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
440 world_db["MAP"][pos] = ord(".")
441 n_trees = int((length ** 2) / 16)
443 while (i_trees <= n_trees):
444 single_allowed = rand.next() % 32
445 y = rand.next() % length
446 x = rand.next() % length
447 pos = (y * length) + x
448 if "." == chr(world_db["MAP"][pos]) \
449 and ((not single_allowed) or is_neighbor((y, x), "X")):
450 world_db["MAP"][pos] = ord("X")
452 rand.seed = store_seed
453 # This all-too-precise replica of the original C code misses iter_limit().
456 def update_map_memory(t):
457 """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
458 if not t["T_MEMMAP"]:
459 t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
460 if not t["T_MEMDEPTHMAP"]:
461 t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
462 for pos in range(world_db["MAP_LENGTH"] ** 2):
463 if "v" == chr(t["fovmap"][pos]):
464 t["T_MEMDEPTHMAP"][pos] = ord("0")
465 if " " == chr(t["T_MEMMAP"][pos]):
466 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
468 # TODO: Aging of MEMDEPTHMAP.
469 for memthing in t["T_MEMTHING"]:
470 y = world_db["Things"][memthing[0]]["T_POSY"]
471 x = world_db["Things"][memthing[1]]["T_POSY"]
472 if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
473 t["T_MEMTHING"].remove(memthing)
474 for id in world_db["Things"]:
475 type = world_db["Things"][id]["T_TYPE"]
476 if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
477 y = world_db["Things"][id]["T_POSY"]
478 x = world_db["Things"][id]["T_POSY"]
479 if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
480 t["T_MEMTHING"].append((type, y, x))
483 def set_world_inactive():
484 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
486 if os.access(io_db["path_worldstate"], os.F_OK):
487 os.remove(io_db["path_worldstate"])
488 world_db["WORLD_ACTIVE"] = 0
491 def integer_test(val_string, min, max):
492 """Return val_string if possible integer >= min and <= max, else None."""
494 val = int(val_string)
495 if val < min or val > max:
499 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
504 def setter(category, key, min, max):
505 """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
508 val = integer_test(val_string, min, max)
512 if category == "Thing":
513 id_store = command_tid
514 decorator = test_Thing_id
515 elif category == "ThingType":
516 id_store = command_ttid
517 decorator = test_ThingType_id
518 elif category == "ThingAction":
519 id_store = command_taid
520 decorator = test_ThingAction_id
524 val = integer_test(val_string, min, max)
526 world_db[category + "s"][id_store.id][key] = val
530 def build_fov_map(t):
531 """Build Thing's FOV map."""
532 t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
533 # DUMMY so far. Just builds an all-visible map.
537 """Make t do nothing (but loudly, if player avatar)."""
538 if t == world_db["Things"][0]:
539 strong_write(io_db["file_out"], "LOG You wait.\n")
546 def actor_pick_up(t):
547 """Make t pick up (topmost?) Thing from ground into inventory."""
548 # Topmostness is actually not defined so far.
549 ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
550 if not world_db["Things"][id]["carried"]
551 if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
552 if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
554 world_db["Things"][ids[0]]["carried"] = True
555 t["T_CARRIES"].append(ids[0])
556 if t == world_db["Things"][0]:
557 strong_write(io_db["file_out"], "LOG You pick up an object.\n")
558 elif t == world_db["Things"][0]:
559 err = "You try to pick up an object, but there is none."
560 strong_write(io_db["file_out"], "LOG " + err + "\n")
564 """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
565 # TODO: Handle case where T_ARGUMENT matches nothing.
566 if len(t["T_CARRIES"]):
567 id = t["T_CARRIES"][t["T_ARGUMENT"]]
568 t["T_CARRIES"].remove(id)
569 world_db["Things"][id]["carried"] = False
570 if t == world_db["Things"][0]:
571 strong_write(io_db["file_out"], "LOG You drop an object.\n")
572 elif t == world_db["Things"][0]:
573 err = "You try to drop an object, but you own none."
574 strong_write(io_db["file_out"], "LOG " + err + "\n")
578 """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
579 # Original wrongly featured lifepoints increase through consumable!
580 # TODO: Handle case where T_ARGUMENT matches nothing.
581 if len(t["T_CARRIES"]):
582 id = t["T_CARRIES"][t["T_ARGUMENT"]]
583 type = world_db["Things"][id]["T_TYPE"]
584 if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
585 t["T_CARRIES"].remove(id)
586 del world_db["Things"][id]
587 t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
588 strong_write(io_db["file_out"], "LOG You consume this object.\n")
590 strong_write(io_db["file_out"], "LOG You try to use this object," +
593 strong_write(io_db["file_out"], "LOG You try to use an object, but " +
598 """Run game world and its inhabitants until new player input expected."""
601 while world_db["Things"][0]["T_LIFEPOINTS"]:
602 for id in [id for id in world_db["Things"]
603 if world_db["Things"][id]["T_LIFEPOINTS"]]:
604 Thing = world_db["Things"][id]
605 if Thing["T_LIFEPOINTS"]:
606 if not Thing["T_COMMAND"]:
607 update_map_memory(Thing)
612 Thing["T_COMMAND"] = 1
614 Thing["T_PROGRESS"] += 1
615 taid = [a for a in world_db["ThingActions"]
616 if a == Thing["T_COMMAND"]][0]
617 ThingAction = world_db["ThingActions"][taid]
618 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
619 eval("actor_" + ThingAction["TA_NAME"])(Thing)
620 Thing["T_COMMAND"] = 0
621 Thing["T_PROGRESS"] = 0
623 # DUMMY: thingproliferation
626 world_db["TURN"] += 1
629 def new_Thing(type, pos=(0,0)):
630 """Return Thing of type T_TYPE, with fovmap if alive and world active."""
632 "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
644 "T_MEMDEPTHMAP": False,
647 if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
652 def id_setter(id, category, id_store=False, start_at_1=False):
653 """Set ID of object of category to manipulate ID unused? Create new one.
655 The ID is stored as id_store.id (if id_store is set). If the integer of the
656 input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
657 32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
658 >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
659 new object is created, otherwise the new object's ID.
661 min = 0 if start_at_1 else -32768
662 max = 255 if start_at_1 else 32767
664 id = integer_test(id, min, max)
666 if id in world_db[category]:
671 if (start_at_1 and 0 == id) \
672 or ((not start_at_1) and (id < 0 or id > 255)):
676 if id not in world_db[category]:
680 "No unused ID available to add to ID list.")
688 """Send PONG line to server output file."""
689 strong_write(io_db["file_out"], "PONG\n")
693 """Abort server process."""
694 raise SystemExit("received QUIT command")
697 def command_thingshere(str_y, str_x):
698 """Write to out file list of Things known to player at coordinate y, x."""
699 def write_thing_if_here():
700 if y == world_db["Things"][id]["T_POSY"] \
701 and x == world_db["Things"][id]["T_POSX"] \
702 and not world_db["Things"][id]["carried"]:
703 type = world_db["Things"][id]["T_TYPE"]
704 name = world_db["ThingTypes"][type]["TT_NAME"]
705 strong_write(io_db["file_out"], name + "\n")
706 if world_db["WORLD_ACTIVE"]:
707 y = integer_test(str_y, 0, 255)
708 x = integer_test(str_x, 0, 255)
709 length = world_db["MAP_LENGTH"]
710 if None != y and None != x and y < length and x < length:
711 pos = (y * world_db["MAP_LENGTH"]) + x
712 strong_write(io_db["file_out"], "THINGS_HERE START\n")
713 if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
714 for id in world_db["Things"]:
715 write_thing_if_here()
717 for id in world_db["Things"]["T_MEMTHING"]:
718 write_thing_if_here()
719 strong_write(io_db["file_out"], "THINGS_HERE END\n")
721 print("Ignoring: Invalid map coordinates.")
723 print("Ignoring: Command only works on existing worlds.")
726 def play_commander(action, args=False):
727 """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
729 T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
733 id = [x for x in world_db["ThingActions"]
734 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
735 world_db["Things"][0]["T_COMMAND"] = id
738 def set_command_and_argument_int(str_arg):
739 val = integer_test(str_arg, 0, 255)
741 world_db["Things"][0]["T_ARGUMENT"] = val
744 print("Ignoring: Argument must be integer >= 0 <=255.")
746 def set_command_and_argument_movestring(str_arg):
747 dirs = {"east": "d", "south-east": "c", "south-west": "x",
748 "west": "s", "north-west": "w", "north-east": "e"}
750 world_db["Things"][0]["T_ARGUMENT"] = dirs[str_arg]
753 print("Ignoring: Argument must be valid direction string.")
756 return set_command_and_argument_movestring
758 return set_command_and_argument_int
763 def command_seedrandomness(seed_string):
764 """Set rand seed to int(seed_string)."""
765 val = integer_test(seed_string, 0, 4294967295)
769 print("Ignoring: Value must be integer >= 0, <= 4294967295.")
772 def command_seedmap(seed_string):
773 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
774 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
778 def command_makeworld(seed_string):
779 """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
781 Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
782 "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
783 TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
784 and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
785 according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
786 of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
787 other. Init player's memory map. Write "NEW_WORLD" line to out file.
793 err = "Space to put thing on too hard to find. Map too small?"
795 y = rand.next() % world_db["MAP_LENGTH"]
796 x = rand.next() % world_db["MAP_LENGTH"]
797 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
801 raise SystemExit(err)
802 # Replica of C code, wrongly ignores animatedness of new Thing.
803 pos_clear = (0 == len([id for id in world_db["Things"]
804 if world_db["Things"][id]["T_LIFEPOINTS"]
805 if world_db["Things"][id]["T_POSY"] == y
806 if world_db["Things"][id]["T_POSX"] == x]))
811 val = integer_test(seed_string, 0, 4294967295)
813 print("Ignoring: Value must be integer >= 0, <= 4294967295.")
816 world_db["SEED_MAP"] = val
817 player_will_be_generated = False
818 playertype = world_db["PLAYER_TYPE"]
819 for ThingType in world_db["ThingTypes"]:
820 if playertype == ThingType:
821 if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
822 player_will_be_generated = True
824 if not player_will_be_generated:
825 print("Ignoring beyond SEED_MAP: " +
826 "No player type with start number >0 defined.")
829 for ThingAction in world_db["ThingActions"]:
830 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
833 print("Ignoring beyond SEED_MAP: " +
834 "No thing action with name 'wait' defined.")
836 world_db["Things"] = {}
838 world_db["WORLD_ACTIVE"] = 1
840 for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
841 id = id_setter(-1, "Things")
842 world_db["Things"][id] = new_Thing(playertype, free_pos())
843 update_map_memory(world_db["Things"][0])
844 for type in world_db["ThingTypes"]:
845 for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
846 if type != playertype:
847 id = id_setter(-1, "Things")
848 world_db["Things"][id] = new_Thing(type, free_pos())
849 strong_write(io_db["file_out"], "NEW_WORLD\n")
852 def command_maplength(maplength_string):
853 """Redefine map length. Invalidate map, therefore lose all things on it."""
855 world_db["Things"] = {}
856 setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
859 def command_worldactive(worldactive_string):
860 """Toggle world_db["WORLD_ACTIVE"] if possible.
862 An active world can always be set inactive. An inactive world can only be
863 set active with a "wait" ThingAction, and a player Thing (of ID 0). On
864 activation, rebuild all Things' FOVs, and the player's map memory.
866 # In original version, map existence was also tested (unnecessarily?).
867 val = integer_test(worldactive_string, 0, 1)
869 if 0 != world_db["WORLD_ACTIVE"]:
873 print("World already active.")
874 elif 0 == world_db["WORLD_ACTIVE"]:
876 for ThingAction in world_db["ThingActions"]:
877 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
880 player_exists = False
881 for Thing in world_db["Things"]:
885 if wait_exists and player_exists:
886 for id in world_db["Things"]:
887 if world_db["Things"][id]["T_LIFEPOINTS"]:
888 build_fov_map(world_db["Things"][id])
890 update_map_memory(world_db["Things"][id])
891 world_db["WORLD_ACTIVE"] = 1
894 def test_for_id_maker(object, category):
895 """Return decorator testing for object having "id" attribute."""
898 if hasattr(object, "id"):
901 print("Ignoring: No " + category +
902 " defined to manipulate yet.")
907 def command_tid(id_string):
908 """Set ID of Thing to manipulate. ID unused? Create new one.
910 Default new Thing's type to the first available ThingType, others: zero.
912 id = id_setter(id_string, "Things", command_tid)
914 if world_db["ThingTypes"] == {}:
915 print("Ignoring: No ThingType to settle new Thing in.")
917 type = list(world_db["ThingTypes"].keys())[0]
918 world_db["Things"][id] = new_Thing(type)
921 test_Thing_id = test_for_id_maker(command_tid, "Thing")
925 def command_tcommand(str_int):
926 """Set T_COMMAND of selected Thing."""
927 val = integer_test(str_int, 0, 255)
929 if 0 == val or val in world_db["ThingActions"]:
930 world_db["Things"][command_tid.id]["T_COMMAND"] = val
932 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
936 def command_ttype(str_int):
937 """Set T_TYPE of selected Thing."""
938 val = integer_test(str_int, 0, 255)
940 if val in world_db["ThingTypes"]:
941 world_db["Things"][command_tid.id]["T_TYPE"] = val
943 print("Ignoring: ThingType ID belongs to no known ThingType.")
947 def command_tcarries(str_int):
948 """Append int(str_int) to T_CARRIES of selected Thing.
950 The ID int(str_int) must not be of the selected Thing, and must belong to a
951 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
953 val = integer_test(str_int, 0, 255)
955 if val == command_tid.id:
956 print("Ignoring: Thing cannot carry itself.")
957 elif val in world_db["Things"] \
958 and not world_db["Things"][val]["carried"]:
959 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
960 world_db["Things"][val]["carried"] = True
962 print("Ignoring: Thing not available for carrying.")
963 # Note that the whole carrying structure is different from the C version:
964 # Carried-ness is marked by a "carried" flag, not by Things containing
969 def command_tmemthing(str_t, str_y, str_x):
970 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
972 The type must fit to an existing ThingType, and the position into the map.
974 type = integer_test(str_t, 0, 255)
975 posy = integer_test(str_y, 0, 255)
976 posx = integer_test(str_x, 0, 255)
977 if None != type and None != posy and None != posx:
978 if type not in world_db["ThingTypes"] \
979 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
980 print("Ignoring: Illegal value for thing type or position.")
982 memthing = (type, posy, posx)
983 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
986 def setter_map(maptype):
987 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
989 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
992 def helper(str_int, mapline):
993 val = integer_test(str_int, 0, 255)
995 if val >= world_db["MAP_LENGTH"]:
996 print("Illegal value for map line number.")
997 elif len(mapline) != world_db["MAP_LENGTH"]:
998 print("Map line length is unequal map width.")
1000 length = world_db["MAP_LENGTH"]
1002 if not world_db["Things"][command_tid.id][maptype]:
1003 map = bytearray(b' ' * (length ** 2))
1005 map = world_db["Things"][command_tid.id][maptype]
1006 map[val * length:(val * length) + length] = mapline.encode()
1007 world_db["Things"][command_tid.id][maptype] = map
1011 def setter_tpos(axis):
1012 """Generate setter for T_POSX or T_POSY of selected Thing.
1014 If world is active, rebuilds animate things' fovmap, player's memory map.
1017 def helper(str_int):
1018 val = integer_test(str_int, 0, 255)
1020 if val < world_db["MAP_LENGTH"]:
1021 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1022 if world_db["WORLD_ACTIVE"] \
1023 and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1024 build_fov_map(world_db["Things"][command_tid.id])
1025 if 0 == command_tid.id:
1026 update_map_memory(world_db["Things"][command_tid.id])
1028 print("Ignoring: Position is outside of map.")
1032 def command_ttid(id_string):
1033 """Set ID of ThingType to manipulate. ID unused? Create new one.
1035 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1037 id = id_setter(id_string, "ThingTypes", command_ttid)
1039 world_db["ThingTypes"][id] = {
1040 "TT_NAME": "(none)",
1043 "TT_PROLIFERATE": 0,
1044 "TT_START_NUMBER": 0,
1050 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1054 def command_ttname(name):
1055 """Set TT_NAME of selected ThingType."""
1056 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1060 def command_ttsymbol(char):
1061 """Set TT_SYMBOL of selected ThingType. """
1063 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1065 print("Ignoring: Argument must be single character.")
1069 def command_ttcorpseid(str_int):
1070 """Set TT_CORPSE_ID of selected ThingType."""
1071 val = integer_test(str_int, 0, 255)
1073 if val in world_db["ThingTypes"]:
1074 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1076 print("Ignoring: Corpse ID belongs to no known ThignType.")
1079 def command_taid(id_string):
1080 """Set ID of ThingAction to manipulate. ID unused? Create new one.
1082 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1084 id = id_setter(id_string, "ThingActions", command_taid, True)
1086 world_db["ThingActions"][id] = {
1092 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1095 @test_ThingAction_id
1096 def command_taname(name):
1097 """Set TA_NAME of selected ThingAction.
1099 The name must match a valid thing action function. If after the name
1100 setting no ThingAction with name "wait" remains, call set_world_inactive().
1102 if name == "wait" or name == "move" or name == "use" or name == "drop" \
1103 or name == "pick_up":
1104 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1105 if 1 == world_db["WORLD_ACTIVE"]:
1106 wait_defined = False
1107 for id in world_db["ThingActions"]:
1108 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1111 if not wait_defined:
1112 set_world_inactive()
1114 print("Ignoring: Invalid action name.")
1115 # In contrast to the original,naming won't map a function to a ThingAction.
1118 """Commands database.
1120 Map command start tokens to ([0]) number of expected command arguments, ([1])
1121 the command's meta-ness (i.e. is it to be written to the record file, is it to
1122 be ignored in replay mode if read from server input file), and ([2]) a function
1126 "QUIT": (0, True, command_quit),
1127 "PING": (0, True, command_ping),
1128 "THINGS_HERE": (2, True, command_thingshere),
1129 "MAKE_WORLD": (1, False, command_makeworld),
1130 "SEED_MAP": (1, False, command_seedmap),
1131 "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1132 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1133 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1134 "MAP_LENGTH": (1, False, command_maplength),
1135 "WORLD_ACTIVE": (1, False, command_worldactive),
1136 "TA_ID": (1, False, command_taid),
1137 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1138 "TA_NAME": (1, False, command_taname),
1139 "TT_ID": (1, False, command_ttid),
1140 "TT_NAME": (1, False, command_ttname),
1141 "TT_SYMBOL": (1, False, command_ttsymbol),
1142 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1143 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1145 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1147 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1149 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1150 "T_ID": (1, False, command_tid),
1151 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1152 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1153 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1154 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1155 "T_COMMAND": (1, False, command_tcommand),
1156 "T_TYPE": (1, False, command_ttype),
1157 "T_CARRIES": (1, False, command_tcarries),
1158 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1159 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1160 "T_MEMTHING": (3, False, command_tmemthing),
1161 "T_POSY": (1, False, setter_tpos("Y")),
1162 "T_POSX": (1, False, setter_tpos("X")),
1163 "wait": (0, False, play_commander("wait")),
1164 "move": (1, False, play_commander("move")),
1165 "pick_up": (0, False, play_commander("pick_up")),
1166 "drop": (1, False, play_commander("drop", True)),
1167 "use": (1, False, play_commander("use", True)),
1171 """World state database. With sane default values. (Randomness is in rand.)"""
1184 """File IO database."""
1186 "path_save": "save",
1187 "path_record": "record",
1188 "path_worldconf": "confserver/world",
1189 "path_server": "server/",
1190 "path_in": "server/in",
1191 "path_out": "server/out",
1192 "path_worldstate": "server/worldstate",
1193 "tmp_suffix": "_tmp",
1194 "kicked_by_rival": False,
1195 "worldstate_updateable": False
1200 libpr = prep_library()
1201 rand = RandomnessIO()
1202 opts = parse_command_line_arguments()
1204 if None != opts.replay:
1208 except SystemExit as exit:
1209 print("ABORTING: " + exit.args[0])
1211 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")