home · contact · privacy
Server/py: Fix another MEMTHING management bug.
[plomrogue] / plomrogue-server.py
1 import argparse
2 import errno
3 import os
4 import shlex
5 import shutil
6 import time
7 import ctypes
8
9
10 class RandomnessIO:
11     """"Interface to libplomrogue's pseudo-randomness generator."""
12
13     def set_seed(self, seed):
14         libpr.seed_rrand(1, seed)
15
16     def get_seed(self):
17         return libpr.seed_rrand(0, 0)
18
19     def next(self):
20         return libpr.rrand()
21
22     seed = property(get_seed, set_seed)
23
24
25 def prep_library():
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,
37                                               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
45     return libpr
46
47
48 def strong_write(file, string):
49     """Apply write(string), flush(), and os.fsync() to file."""
50     file.write(string)
51     file.flush()
52     os.fsync(file)
53
54
55 def setup_server_io():
56     """Fill IO files DB with proper file( path)s. Write process IO test string.
57
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"].
63     """
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):
70             raise SystemExit(msg)
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"])
82
83
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):
87         if file_key in io_db:
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()
97
98
99 def obey(command, prefix, replay=False, do_record=False):
100     """Call function from commands_db mapped to command's first token.
101
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.
109     """
110     server_test()
111     print("input " + prefix + ": " + command)
112     try:
113         tokens = shlex.split(command, comments=True)
114     except ValueError as err:
115         print("Can't tokenize command string: " + str(err) + ".")
116         return
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:])
121         elif replay:
122             print("Due to replay mode, reading command as 'go on in record'.")
123             line = io_db["file_record"].readline()
124             if len(line) > 0:
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
128             else:
129                 print("Reached end of record file.")
130         else:
131             commands_db[tokens[0]][2](*tokens[1:])
132             if do_record:
133                 record(command)
134                 save_world()
135             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
136     elif 0 != len(tokens):
137         print("Invalid command/argument, or bad number of tokens.")
138
139
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"]
143     mode = "w"
144     if do_append:
145         mode = "a"
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)
150     file.close()
151     if os.access(path, os.F_OK):
152         os.remove(path)
153     os.rename(path_tmp, path)
154
155
156 def record(command):
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)
162
163
164 def save_world():
165     """Save all commands needed to reconstruct current world state."""
166     # TODO: Misses same optimizations as record() from the original record().
167
168     def quote(string):
169         string = string.replace("\u005C", '\u005C\u005C')
170         return '"' + string.replace('"', '\u005C"') + '"'
171
172     def mapsetter(key):
173         def helper(id):
174             string = ""
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) \
181                              + "\n"
182             return string
183         return helper
184
185     def memthing(id):
186         string = ""
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"
190         return string
191
192     def helper(category, id_string, special_keys={}):
193         string = ""
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)
203         return string
204
205     string = ""
206     for key in world_db:
207         if dict != type(world_db[key]) and key != "MAP" and \
208            key != "WORLD_ACTIVE" and key != "SEED_MAP":
209             string = string + key + " " + str(world_db[key]) + "\n"
210     string = string + "SEED_MAP " + str(world_db["SEED_MAP"]) + "\n"
211     string = string + helper("ThingActions", "TA_ID")
212     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
213     for id in world_db["ThingTypes"]:
214         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
215                  str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
216     string = string + helper("Things", "T_ID",
217                              {"T_CARRIES": False, "carried": False,
218                               "T_MEMMAP": mapsetter("T_MEMMAP"),
219                               "T_MEMTHING": memthing, "fovmap": False,
220                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
221     for id in world_db["Things"]:
222         if [] != world_db["Things"][id]["T_CARRIES"]:
223             string = string + "T_ID " + str(id) + "\n"
224             for carried_id in world_db["Things"][id]["T_CARRIES"]:
225                 string = string + "T_CARRIES " + str(carried_id) + "\n"
226     string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
227              "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
228     atomic_write(io_db["path_save"], string)
229
230
231 def obey_lines_in_file(path, name, do_record=False):
232     """Call obey() on each line of path's file, use name in input prefix."""
233     file = open(path, "r")
234     line_n = 1
235     for line in file.readlines():
236         obey(line.rstrip(), name + "file line " + str(line_n),
237              do_record=do_record)
238         line_n = line_n + 1
239     file.close()
240
241
242 def parse_command_line_arguments():
243     """Return settings values read from command line arguments."""
244     parser = argparse.ArgumentParser()
245     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
246                         action='store')
247     opts, unknown = parser.parse_known_args()
248     return opts
249
250
251 def server_test():
252     """Ensure valid server out file belonging to current process.
253
254     This is done by comparing io_db["teststring"] to what's found at the start
255     of the current file at io_db["path_out"]. On failure, set
256     io_db["kicked_by_rival"] and raise SystemExit.
257     """
258     if not os.access(io_db["path_out"], os.F_OK):
259         raise SystemExit("Server output file has disappeared.")
260     file = open(io_db["path_out"], "r")
261     test = file.readline().rstrip("\n")
262     file.close()
263     if test != io_db["teststring"]:
264         io_db["kicked_by_rival"] = True
265         msg = "Server test string in server output file does not match. This" \
266               " indicates that the current server process has been " \
267               "superseded by another one."
268         raise SystemExit(msg)
269
270
271 def read_command():
272     """Return next newline-delimited command from server in file.
273
274     Keep building return string until a newline is encountered. Pause between
275     unsuccessful reads, and after too much waiting, run server_test().
276     """
277     wait_on_fail = 0.03333
278     max_wait = 5
279     now = time.time()
280     command = ""
281     while True:
282         add = io_db["file_in"].readline()
283         if len(add) > 0:
284             command = command + add
285             if len(command) > 0 and "\n" == command[-1]:
286                 command = command[:-1]
287                 break
288         else:
289             time.sleep(wait_on_fail)
290             if now + max_wait < time.time():
291                 server_test()
292                 now = time.time()
293     return command
294
295
296 def try_worldstate_update():
297     """Write worldstate file if io_db["worldstate_updateable"] is set."""
298     if io_db["worldstate_updateable"]:
299
300         def draw_visible_Things(map, run):
301             for id in world_db["Things"]:
302                 type = world_db["Things"][id]["T_TYPE"]
303                 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
304                 alive = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]
305                 if (0 == run and not consumable and not alive) \
306                    or (1 == run and consumable and not alive) \
307                    or (2 == run and alive):
308                     y = world_db["Things"][id]["T_POSY"]
309                     x = world_db["Things"][id]["T_POSX"]
310                     fovflag = world_db["Things"][0]["fovmap"][(y * length) + x]
311                     if 'v' == chr(fovflag):
312                         c = world_db["ThingTypes"][type]["TT_SYMBOL"]
313                         map[(y * length) + x] = ord(c)
314
315         def write_map(string, map):
316             for i in range(length):
317                 line = map[i * length:(i * length) + length].decode()
318                 string = string + line + "\n"
319             return string
320
321         inventory = ""
322         if [] == world_db["Things"][0]["T_CARRIES"]:
323             inventory = "(none)\n"
324         else:
325             for id in world_db["Things"][0]["T_CARRIES"]:
326                 type_id = world_db["Things"][id]["T_TYPE"]
327                 name = world_db["ThingTypes"][type_id]["TT_NAME"]
328                 inventory = inventory + name + "\n"
329         string = str(world_db["TURN"]) + "\n" + \
330                  str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
331                  str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
332                  inventory + "%\n" + \
333                  str(world_db["Things"][0]["T_POSY"]) + "\n" + \
334                  str(world_db["Things"][0]["T_POSX"]) + "\n" + \
335                  str(world_db["MAP_LENGTH"]) + "\n"
336         length = world_db["MAP_LENGTH"]
337         fov = bytearray(b' ' * (length ** 2))
338         for pos in range(length ** 2):
339             if 'v' == chr(world_db["Things"][0]["fovmap"][pos]):
340                 fov[pos] = world_db["MAP"][pos]
341         for i in range(3):
342             draw_visible_Things(fov, i)
343         string = write_map(string, fov)
344         mem = world_db["Things"][0]["T_MEMMAP"][:]
345         for i in range(2):
346             for mt in world_db["Things"][0]["T_MEMTHING"]:
347                 consumable = world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]
348                 if (i == 0 and not consumable) or (i == 1 and consumable):
349                     c = world_db["ThingTypes"][mt[0]]["TT_SYMBOL"]
350                     mem[(mt[1] * length) + mt[2]] = ord(c)
351         string = write_map(string, mem)
352         atomic_write(io_db["path_worldstate"], string)
353         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
354         io_db["worldstate_updateable"] = False
355
356
357 def replay_game():
358     """Replay game from record file.
359
360     Use opts.replay as breakpoint turn to which to replay automatically before
361     switching to manual input by non-meta commands in server input file
362     triggering further reads of record file. Ensure opts.replay is at least 1.
363     Run try_worldstate_update() before each interactive obey()/read_command().
364     """
365     if opts.replay < 1:
366         opts.replay = 1
367     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
368           " (if so late a turn is to be found).")
369     if not os.access(io_db["path_record"], os.F_OK):
370         raise SystemExit("No record file found to replay.")
371     io_db["file_record"] = open(io_db["path_record"], "r")
372     io_db["file_record"].prefix = "record file line "
373     io_db["file_record"].line_n = 1
374     while world_db["TURN"] < opts.replay:
375         line = io_db["file_record"].readline()
376         if "" == line:
377             break
378         obey(line.rstrip(), io_db["file_record"].prefix
379              + str(io_db["file_record"].line_n))
380         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
381     while True:
382         try_worldstate_update()
383         obey(read_command(), "in file", replay=True)
384
385
386 def play_game():
387     """Play game by server input file commands. Before, load save file found.
388
389     If no save file is found, a new world is generated from the commands in the
390     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
391     command and all that follow via the server input file. Run
392     try_worldstate_update() before each interactive obey()/read_command().
393     """
394     if os.access(io_db["path_save"], os.F_OK):
395         obey_lines_in_file(io_db["path_save"], "save")
396     else:
397         if not os.access(io_db["path_worldconf"], os.F_OK):
398             msg = "No world config file from which to start a new world."
399             raise SystemExit(msg)
400         obey_lines_in_file(io_db["path_worldconf"], "world config ",
401                            do_record=True)
402         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
403     while True:
404         try_worldstate_update()
405         obey(read_command(), "in file", do_record=True)
406
407
408 def remake_map():
409     """(Re-)make island map.
410
411     Let "~" represent water, "." land, "X" trees: Build island shape randomly,
412     start with one land cell in the middle, then go into cycle of repeatedly
413     selecting a random sea cell and transforming it into land if it is neighbor
414     to land. The cycle ends when a land cell is due to be created at the map's
415     border. Then put some trees on the map (TODO: more precise algorithm desc).
416     """
417     def is_neighbor(coordinates, type):
418         y = coordinates[0]
419         x = coordinates[1]
420         length = world_db["MAP_LENGTH"]
421         ind = y % 2
422         diag_west = x + (ind > 0)
423         diag_east = x + (ind < (length - 1))
424         pos = (y * length) + x
425         if (y > 0 and diag_east
426             and type == chr(world_db["MAP"][pos - length + ind])) \
427            or (x < (length - 1)
428                and type == chr(world_db["MAP"][pos + 1])) \
429            or (y < (length - 1) and diag_east
430                and type == chr(world_db["MAP"][pos + length + ind])) \
431            or (y > 0 and diag_west
432                and type == chr(world_db["MAP"][pos - length - (not ind)])) \
433            or (x > 0
434                and type == chr(world_db["MAP"][pos - 1])) \
435            or (y < (length - 1) and diag_west
436                and type == chr(world_db["MAP"][pos + length - (not ind)])):
437             return True
438         return False
439     store_seed = rand.seed
440     rand.seed = world_db["SEED_MAP"]
441     world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
442     length = world_db["MAP_LENGTH"]
443     add_half_width = (not (length % 2)) * int(length / 2)
444     world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
445     while (1):
446         y = rand.next() % length
447         x = rand.next() % length
448         pos = (y * length) + x
449         if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
450             if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
451                 break
452             world_db["MAP"][pos] = ord(".")
453     n_trees = int((length ** 2) / 16)
454     i_trees = 0
455     while (i_trees <= n_trees):
456         single_allowed = rand.next() % 32
457         y = rand.next() % length
458         x = rand.next() % length
459         pos = (y * length) + x
460         if "." == chr(world_db["MAP"][pos]) \
461           and ((not single_allowed) or is_neighbor((y, x), "X")):
462             world_db["MAP"][pos] = ord("X")
463             i_trees += 1
464     rand.seed = store_seed
465     # This all-too-precise replica of the original C code misses iter_limit().
466
467
468 def update_map_memory(t, age_map=True):
469     """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
470     if not t["T_MEMMAP"]:
471         t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
472     if not t["T_MEMDEPTHMAP"]:
473         t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
474     for pos in range(world_db["MAP_LENGTH"] ** 2):
475         if "v" == chr(t["fovmap"][pos]):
476             t["T_MEMDEPTHMAP"][pos] = ord("0")
477             if " " == chr(t["T_MEMMAP"][pos]):
478                 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
479             continue
480         if age_map and ord('0') <= t["T_MEMDEPTHMAP"][pos] \
481            and ord('9') > t["T_MEMDEPTHMAP"][pos] \
482            and not rand.next() % (2 ** (t["T_MEMDEPTHMAP"][pos] - 48)):
483             t["T_MEMDEPTHMAP"][pos] += 1
484     for mt in [mt for mt in t["T_MEMTHING"]
485                if "v" == chr(t["fovmap"][(mt[1] * world_db["MAP_LENGTH"])
486                                          + mt[2]])]:
487             t["T_MEMTHING"].remove(mt)
488     for id in [id for id in world_db["Things"]
489                if not world_db["Things"][id]["carried"]]:
490         type = world_db["Things"][id]["T_TYPE"]
491         if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
492             y = world_db["Things"][id]["T_POSY"]
493             x = world_db["Things"][id]["T_POSX"]
494             if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
495                 t["T_MEMTHING"].append((type, y, x))
496
497
498 def set_world_inactive():
499     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
500     server_test()
501     if os.access(io_db["path_worldstate"], os.F_OK):
502         os.remove(io_db["path_worldstate"])
503     world_db["WORLD_ACTIVE"] = 0
504
505
506 def integer_test(val_string, min, max):
507     """Return val_string if possible integer >= min and <= max, else None."""
508     try:
509         val = int(val_string)
510         if val < min or val > max:
511             raise ValueError
512         return val
513     except ValueError:
514         print("Ignoring: Please use integer >= " + str(min) + " and <= " +
515               str(max) + ".")
516         return None
517
518
519 def setter(category, key, min, max):
520     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
521     if category is None:
522         def f(val_string):
523             val = integer_test(val_string, min, max)
524             if None != val:
525                 world_db[key] = val
526     else:
527         if category == "Thing":
528             id_store = command_tid
529             decorator = test_Thing_id
530         elif category == "ThingType":
531             id_store = command_ttid
532             decorator = test_ThingType_id
533         elif category == "ThingAction":
534             id_store = command_taid
535             decorator = test_ThingAction_id
536
537         @decorator
538         def f(val_string):
539             val = integer_test(val_string, min, max)
540             if None != val:
541                 world_db[category + "s"][id_store.id][key] = val
542     return f
543
544
545 def build_fov_map(t):
546     """Build Thing's FOV map."""
547     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
548     maptype = ctypes.c_char * len(world_db["MAP"])
549     test = libpr.build_fov_map(t["T_POSY"], t["T_POSX"],
550                                maptype.from_buffer(t["fovmap"]),
551                                maptype.from_buffer(world_db["MAP"]))
552     if test:
553         raise RuntimeError("Malloc error in build_fov_Map().")
554
555
556 def decrement_lifepoints(t):
557     """Decrement t's lifepoints by 1, and if to zero, corpse it.
558
559     If t is the player avatar, only blank its fovmap, so that the client may
560     still display memory data. On non-player things, erase fovmap and memory.
561     """
562     t["T_LIFEPOINTS"] -= 1
563     if 0 == t["T_LIFEPOINTS"]:
564         t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
565         if world_db["Things"][0] == t:
566             t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
567             strong_write(io_db["file_out"], "LOG You die.\n")
568         else:
569             t["fovmap"] = False
570             t["T_MEMMAP"] = False
571             t["T_MEMDEPTHMAP"] = False
572             t["T_MEMTHING"] = []
573             strong_write(io_db["file_out"], "LOG It dies.\n")
574
575
576 def mv_yx_in_dir_legal(dir, y, x):
577     """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
578     dir_c = dir.encode("ascii")[0]
579     test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
580     if -1 == test:
581         raise RuntimeError("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
582     return (test, libpr.result_y(), libpr.result_x())
583
584
585 def actor_wait(t):
586     """Make t do nothing (but loudly, if player avatar)."""
587     if t == world_db["Things"][0]:
588         strong_write(io_db["file_out"], "LOG You wait.\n")
589
590
591 def actor_move(t):
592     """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
593     passable = False
594     move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
595                                      t["T_POSY"], t["T_POSX"])
596     if 1 == move_result[0]:
597         pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
598         passable = "." == chr(world_db["MAP"][pos])
599         hitted = [id for id in world_db["Things"]
600                   if world_db["Things"][id] != t
601                   if world_db["Things"][id]["T_LIFEPOINTS"]
602                   if world_db["Things"][id]["T_POSY"] == move_result[1]
603                   if world_db["Things"][id]["T_POSX"] == move_result[2]]
604         if len(hitted):
605             hit_id = hitted[0]
606             hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
607             hitter = "You" if t == world_db["Things"][0] else hitter_name
608             hitted_type = world_db["Things"][hit_id]["T_TYPE"]
609             hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
610             hitted = "you" if hit_id == 0 else hitted_name
611             verb = " wound " if hitter == "You" else " wounds "
612             strong_write(io_db["file_out"], "LOG " + hitter + verb + hitted +
613                                             ".\n")
614             decrement_lifepoints(world_db["Things"][hit_id])
615             return
616     dir = [dir for dir in directions_db
617            if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
618     if passable:
619         t["T_POSY"] = move_result[1]
620         t["T_POSX"] = move_result[2]
621         for id in t["T_CARRIES"]:
622             world_db["Things"][id]["T_POSY"] = move_result[1]
623             world_db["Things"][id]["T_POSX"] = move_result[2]
624         build_fov_map(t)
625         strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
626     else:
627         strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
628
629
630 def actor_pick_up(t):
631     """Make t pick up (topmost?) Thing from ground into inventory."""
632     # Topmostness is actually not defined so far.
633     ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
634            if not world_db["Things"][id]["carried"]
635            if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
636            if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
637     if len(ids):
638         world_db["Things"][ids[0]]["carried"] = True
639         t["T_CARRIES"].append(ids[0])
640         if t == world_db["Things"][0]:
641             strong_write(io_db["file_out"], "LOG You pick up an object.\n")
642     elif t == world_db["Things"][0]:
643         err = "You try to pick up an object, but there is none."
644         strong_write(io_db["file_out"], "LOG " + err + "\n")
645
646
647 def actor_drop(t):
648     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
649     # TODO: Handle case where T_ARGUMENT matches nothing.
650     if len(t["T_CARRIES"]):
651         id = t["T_CARRIES"][t["T_ARGUMENT"]]
652         t["T_CARRIES"].remove(id)
653         world_db["Things"][id]["carried"] = False
654         if t == world_db["Things"][0]:
655             strong_write(io_db["file_out"], "LOG You drop an object.\n")
656     elif t == world_db["Things"][0]:
657         err = "You try to drop an object, but you own none."
658         strong_write(io_db["file_out"], "LOG " + err + "\n")
659
660
661 def actor_use(t):
662     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
663     # TODO: Handle case where T_ARGUMENT matches nothing.
664     if len(t["T_CARRIES"]):
665         id = t["T_CARRIES"][t["T_ARGUMENT"]]
666         type = world_db["Things"][id]["T_TYPE"]
667         if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
668             t["T_CARRIES"].remove(id)
669             del world_db["Things"][id]
670             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
671             t["T_LIFEPOINTS"] += 1
672             # Wrongly increment HPs is a replica of the original code.
673             strong_write(io_db["file_out"], "LOG You consume this object.\n")
674         else:
675             strong_write(io_db["file_out"], "LOG You try to use this object," +
676                                             "but fail.\n")
677     else:
678         strong_write(io_db["file_out"], "LOG You try to use an object, but " +
679                                         "you own none.\n")
680
681
682 def thingproliferation(t):
683     """To chance of 1/TT_PROLIFERATE, create t offspring in neighbor cell.
684
685     Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be
686     passable and not be inhabited by a Thing of the same type, or, if Thing is
687     animate, any other animate Thing. If there are several map cell candidates,
688     one is selected randomly.
689     """
690     def test_cell(t, y, x):
691         if "." == chr(world_db["MAP"][(y * world_db["MAP_LENGTH"]) + x]):
692             for id in [id for id in world_db["Things"]
693                        if y == world_db["Things"][id]["T_POSY"]
694                        if x == world_db["Things"][id]["T_POSX"]
695                        if (t["T_TYPE"] == world_db["Things"][id]["T_TYPE"])
696                        or (t["T_LIFEPOINTS"] and
697                            world_db["Things"][id]["T_LIFEPOINTS"])]:
698                 return False
699             return True
700         return False
701     prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
702     if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)):
703         candidates = []
704         for dir in [directions_db[key] for key in directions_db]:
705             mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
706             if mv_result[0] and test_cell(t, mv_result[1], mv_result[2]):
707                 candidates.append((mv_result[1], mv_result[2]))
708         if len(candidates):
709             i = rand.next() % len(candidates)
710             id = id_setter(-1, "Things")
711             newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
712             world_db["Things"][id] = newT
713
714
715 def try_healing(t):
716     """Grow t's HP to a 1/32 chance if < HP max, satiation > 0, and waiting.
717
718     On success, decrease satiation score by 32.
719     """
720     if t["T_SATIATION"] > 0 \
721        and t["T_LIFEPOINTS"] < \
722            world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"] \
723        and 0 == (rand.next() % 31) \
724        and t["T_COMMAND"] == [id for id in world_db["ThingActions"]
725                               if world_db["ThingActions"][id]["TA_NAME"] ==
726                                  "wait"][0]:
727         t["T_LIFEPOINTS"] += 1
728         t["T_SATIATION"] -= 32
729         if t == world_db["Things"][0]:
730             strong_write(io_db["file_out"], "LOG You heal.\n")
731         else:
732             name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
733             strong_write(io_db["file_out"], "LOG " + name + "heals.\n")
734
735
736 def hunger(t):
737     """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
738     if t["T_SATIATION"] > -32768:
739         t["T_SATIATION"] -= 1
740     testbase = t["T_SATIATION"] if t["T_SATIATION"] >= 0 else -t["T_SATIATION"]
741     if not world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]:
742         raise RuntimeError("A thing that should not hunger is hungering.")
743     stomach = int(32767 / world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"])
744     if int(int(testbase / stomach) / ((rand.next() % stomach) + 1)):
745         if t == world_db["Things"][0]:
746             strong_write(io_db["file_out"], "LOG You suffer from hunger.\n")
747         else:
748             name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
749             strong_write(io_db["file_out"], "LOG " + name +
750                                             " suffers from hunger.\n")
751         decrement_lifepoints(t)
752
753
754 def get_dir_to_nearest_target(t, c):
755     # Dummy
756     return False
757
758
759 def standing_on_consumable(t):
760     """Return True/False whether t is standing on a consumable."""
761     for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
762                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
763                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
764                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
765                           ["TT_CONSUMABLE"]]:
766         return True
767     return False
768
769
770 def get_inventory_slot_to_consume(t):
771     """Return slot Id of strongest consumable in t's inventory, else -1."""
772     cmp_consumability = 0
773     selection = -1
774     i = 0
775     for id in t["T_CARRIES"]:
776         type = world_db["Things"][id]["T_TYPE"]
777         if world_db["ThingTypes"][type]["TT_CONSUMABLE"] > cmp_consumability:
778             cmp_consumability = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
779             selection = i
780         i += 1
781     return selection
782
783
784 def ai(t):
785     """Determine next command/argment for actor t via AI algorithms.
786
787     AI will look for, and move towards, enemies (animate Things not of their
788     own ThingType); if they see none, they will consume consumables in their
789     inventory; if there are none, they will pick up what they stand on if they
790     stand on consumables; if they stand on none, they will move towards the
791     next consumable they see or remember on the map; if they see or remember
792     none, they will explore parts of the map unseen since ever or for at least
793     one turn; if there is nothing to explore, they will simply wait.
794     """
795     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
796                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
797     if not get_dir_to_nearest_target(t, "f"):
798         sel = get_inventory_slot_to_consume(t)
799         if -1 != sel:
800             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
801                               if world_db["ThingActions"][id]["TA_NAME"]
802                                  == "use"][0]
803             t["T_ARGUMENT"] = sel
804         elif standing_on_consumable(t):
805             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
806                               if world_db["ThingActions"][id]["TA_NAME"]
807                                  == "pick_up"][0]
808         elif (not get_dir_to_nearest_target(t, "c")) and \
809              (not get_dir_to_nearest_target(t, "a")):
810             get_dir_to_nearest_target(t, "s")
811
812
813 def turn_over():
814     """Run game world and its inhabitants until new player input expected."""
815     id = 0
816     whilebreaker = False
817     while world_db["Things"][0]["T_LIFEPOINTS"]:
818         for id in [id for id in world_db["Things"]]:
819             if not id in world_db["Things"]: # Thing may have been consumed
820                 continue                     # during turn …
821             Thing = world_db["Things"][id]
822             if Thing["T_LIFEPOINTS"]:
823                 if not Thing["T_COMMAND"]:
824                     update_map_memory(Thing)
825                     if 0 == id:
826                         whilebreaker = True
827                         break
828                     ai(Thing)
829                     Thing["T_COMMAND"] = 1
830                 try_healing(Thing)
831                 Thing["T_PROGRESS"] += 1
832                 taid = [a for a in world_db["ThingActions"]
833                           if a == Thing["T_COMMAND"]][0]
834                 ThingAction = world_db["ThingActions"][taid]
835                 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
836                     eval("actor_" + ThingAction["TA_NAME"])(Thing)
837                     Thing["T_COMMAND"] = 0
838                     Thing["T_PROGRESS"] = 0
839                 hunger(Thing)
840             thingproliferation(Thing)
841         if whilebreaker:
842             break
843         world_db["TURN"] += 1
844
845
846 def new_Thing(type, pos=(0, 0)):
847     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
848     thing = {
849         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
850         "T_ARGUMENT": 0,
851         "T_PROGRESS": 0,
852         "T_SATIATION": 0,
853         "T_COMMAND": 0,
854         "T_TYPE": type,
855         "T_POSY": pos[0],
856         "T_POSX": pos[1],
857         "T_CARRIES": [],
858         "carried": False,
859         "T_MEMTHING": [],
860         "T_MEMMAP": False,
861         "T_MEMDEPTHMAP": False,
862         "fovmap": False
863     }
864     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
865         build_fov_map(thing)
866     return thing
867
868
869 def id_setter(id, category, id_store=False, start_at_1=False):
870     """Set ID of object of category to manipulate ID unused? Create new one.
871
872     The ID is stored as id_store.id (if id_store is set). If the integer of the
873     input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
874     32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
875     >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
876     new object is created, otherwise the new object's ID.
877     """
878     min = 0 if start_at_1 else -32768
879     max = 255 if start_at_1 else 32767
880     if str == type(id):
881         id = integer_test(id, min, max)
882     if None != id:
883         if id in world_db[category]:
884             if id_store:
885                 id_store.id = id
886             return None
887         else:
888             if (start_at_1 and 0 == id) \
889                or ((not start_at_1) and (id < 0 or id > 255)):
890                 id = -1
891                 while 1:
892                     id = id + 1
893                     if id not in world_db[category]:
894                         break
895                 if id > 255:
896                     print("Ignoring: "
897                           "No unused ID available to add to ID list.")
898                     return None
899             if id_store:
900                 id_store.id = id
901     return id
902
903
904 def command_ping():
905     """Send PONG line to server output file."""
906     strong_write(io_db["file_out"], "PONG\n")
907
908
909 def command_quit():
910     """Abort server process."""
911     raise SystemExit("received QUIT command")
912
913
914 def command_thingshere(str_y, str_x):
915     """Write to out file list of Things known to player at coordinate y, x."""
916     if world_db["WORLD_ACTIVE"]:
917         y = integer_test(str_y, 0, 255)
918         x = integer_test(str_x, 0, 255)
919         length = world_db["MAP_LENGTH"]
920         if None != y and None != x and y < length and x < length:
921             pos = (y * world_db["MAP_LENGTH"]) + x
922             strong_write(io_db["file_out"], "THINGS_HERE START\n")
923             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
924                 for id in world_db["Things"]:
925                     # write_thing_if_here()
926                     if y == world_db["Things"][id]["T_POSY"] \
927                        and x == world_db["Things"][id]["T_POSX"] \
928                        and not world_db["Things"][id]["carried"]:
929                         type = world_db["Things"][id]["T_TYPE"]
930                         name = world_db["ThingTypes"][type]["TT_NAME"]
931                         strong_write(io_db["file_out"], name + "\n")
932             else:
933                 for mt in world_db["Things"][0]["T_MEMTHING"]:
934                     if y == mt[1] and x == mt[2]:
935                         name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
936                         strong_write(io_db["file_out"], name + "\n")
937             strong_write(io_db["file_out"], "THINGS_HERE END\n")
938         else:
939             print("Ignoring: Invalid map coordinates.")
940     else:
941         print("Ignoring: Command only works on existing worlds.")
942
943
944 def play_commander(action, args=False):
945     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
946
947     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
948     """
949
950     def set_command():
951         id = [x for x in world_db["ThingActions"]
952                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
953         world_db["Things"][0]["T_COMMAND"] = id
954         turn_over()
955
956     def set_command_and_argument_int(str_arg):
957         val = integer_test(str_arg, 0, 255)
958         if None != val:
959             world_db["Things"][0]["T_ARGUMENT"] = val
960             set_command()
961
962     def set_command_and_argument_movestring(str_arg):
963         if str_arg in directions_db:
964             world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
965             set_command()
966         else:
967             print("Ignoring: Argument must be valid direction string.")
968
969     if action == "move":
970         return set_command_and_argument_movestring
971     elif args:
972         return set_command_and_argument_int
973     else:
974         return set_command
975
976
977 def command_seedrandomness(seed_string):
978     """Set rand seed to int(seed_string)."""
979     val = integer_test(seed_string, 0, 4294967295)
980     if None != val:
981         rand.seed = val
982
983
984 def command_seedmap(seed_string):
985     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
986     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
987     remake_map()
988
989
990 def command_makeworld(seed_string):
991     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
992
993     Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
994     "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
995     TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
996     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
997     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
998     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
999     other. Init player's memory map. Write "NEW_WORLD" line to out file.
1000     """
1001
1002     def free_pos():
1003         i = 0
1004         while 1:
1005             err = "Space to put thing on too hard to find. Map too small?"
1006             while 1:
1007                 y = rand.next() % world_db["MAP_LENGTH"]
1008                 x = rand.next() % world_db["MAP_LENGTH"]
1009                 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
1010                     break
1011                 i += 1
1012                 if i == 65535:
1013                     raise SystemExit(err)
1014             # Replica of C code, wrongly ignores animatedness of new Thing.
1015             pos_clear = (0 == len([id for id in world_db["Things"]
1016                                    if world_db["Things"][id]["T_LIFEPOINTS"]
1017                                    if world_db["Things"][id]["T_POSY"] == y
1018                                    if world_db["Things"][id]["T_POSX"] == x]))
1019             if pos_clear:
1020                 break
1021         return (y, x)
1022
1023     val = integer_test(seed_string, 0, 4294967295)
1024     if None == val:
1025         return
1026     rand.seed = val
1027     world_db["SEED_MAP"] = val
1028     player_will_be_generated = False
1029     playertype = world_db["PLAYER_TYPE"]
1030     for ThingType in world_db["ThingTypes"]:
1031         if playertype == ThingType:
1032             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1033                 player_will_be_generated = True
1034             break
1035     if not player_will_be_generated:
1036         print("Ignoring beyond SEED_MAP: " +
1037               "No player type with start number >0 defined.")
1038         return
1039     wait_action = False
1040     for ThingAction in world_db["ThingActions"]:
1041         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1042             wait_action = True
1043     if not wait_action:
1044         print("Ignoring beyond SEED_MAP: " +
1045               "No thing action with name 'wait' defined.")
1046         return
1047     world_db["Things"] = {}
1048     remake_map()
1049     world_db["WORLD_ACTIVE"] = 1
1050     world_db["TURN"] = 1
1051     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1052         id = id_setter(-1, "Things")
1053         world_db["Things"][id] = new_Thing(playertype, free_pos())
1054     update_map_memory(world_db["Things"][0])
1055     for type in world_db["ThingTypes"]:
1056         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1057             if type != playertype:
1058                 id = id_setter(-1, "Things")
1059                 world_db["Things"][id] = new_Thing(type, free_pos())
1060     strong_write(io_db["file_out"], "NEW_WORLD\n")
1061
1062
1063 def command_maplength(maplength_string):
1064     """Redefine map length. Invalidate map, therefore lose all things on it."""
1065     val = integer_test(maplength_string, 1, 256)
1066     if None != val:
1067         world_db["MAP_LENGTH"] = val
1068         set_world_inactive()
1069         world_db["Things"] = {}
1070         libpr.set_maplength(val)
1071
1072
1073 def command_worldactive(worldactive_string):
1074     """Toggle world_db["WORLD_ACTIVE"] if possible.
1075
1076     An active world can always be set inactive. An inactive world can only be
1077     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
1078     activation, rebuild all Things' FOVs, and the player's map memory.
1079     """
1080     # In original version, map existence was also tested (unnecessarily?).
1081     val = integer_test(worldactive_string, 0, 1)
1082     if val:
1083         if 0 != world_db["WORLD_ACTIVE"]:
1084             if 0 == val:
1085                 set_world_inactive()
1086             else:
1087                 print("World already active.")
1088         elif 0 == world_db["WORLD_ACTIVE"]:
1089             wait_exists = False
1090             for ThingAction in world_db["ThingActions"]:
1091                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1092                     wait_exists = True
1093                     break
1094             player_exists = False
1095             for Thing in world_db["Things"]:
1096                 if 0 == Thing:
1097                     player_exists = True
1098                     break
1099             if wait_exists and player_exists:
1100                 for id in world_db["Things"]:
1101                     if world_db["Things"][id]["T_LIFEPOINTS"]:
1102                         build_fov_map(world_db["Things"][id])
1103                         if 0 == id:
1104                             update_map_memory(world_db["Things"][id])
1105                 world_db["WORLD_ACTIVE"] = 1
1106
1107
1108 def test_for_id_maker(object, category):
1109     """Return decorator testing for object having "id" attribute."""
1110     def decorator(f):
1111         def helper(*args):
1112             if hasattr(object, "id"):
1113                 f(*args)
1114             else:
1115                 print("Ignoring: No " + category +
1116                       " defined to manipulate yet.")
1117         return helper
1118     return decorator
1119
1120
1121 def command_tid(id_string):
1122     """Set ID of Thing to manipulate. ID unused? Create new one.
1123
1124     Default new Thing's type to the first available ThingType, others: zero.
1125     """
1126     id = id_setter(id_string, "Things", command_tid)
1127     if None != id:
1128         if world_db["ThingTypes"] == {}:
1129             print("Ignoring: No ThingType to settle new Thing in.")
1130             return
1131         type = list(world_db["ThingTypes"].keys())[0]
1132         world_db["Things"][id] = new_Thing(type)
1133
1134
1135 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1136
1137
1138 @test_Thing_id
1139 def command_tcommand(str_int):
1140     """Set T_COMMAND of selected Thing."""
1141     val = integer_test(str_int, 0, 255)
1142     if None != val:
1143         if 0 == val or val in world_db["ThingActions"]:
1144             world_db["Things"][command_tid.id]["T_COMMAND"] = val
1145         else:
1146             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1147
1148
1149 @test_Thing_id
1150 def command_ttype(str_int):
1151     """Set T_TYPE of selected Thing."""
1152     val = integer_test(str_int, 0, 255)
1153     if None != val:
1154         if val in world_db["ThingTypes"]:
1155             world_db["Things"][command_tid.id]["T_TYPE"] = val
1156         else:
1157             print("Ignoring: ThingType ID belongs to no known ThingType.")
1158
1159
1160 @test_Thing_id
1161 def command_tcarries(str_int):
1162     """Append int(str_int) to T_CARRIES of selected Thing.
1163
1164     The ID int(str_int) must not be of the selected Thing, and must belong to a
1165     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1166     """
1167     val = integer_test(str_int, 0, 255)
1168     if None != val:
1169         if val == command_tid.id:
1170             print("Ignoring: Thing cannot carry itself.")
1171         elif val in world_db["Things"] \
1172              and not world_db["Things"][val]["carried"]:
1173             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1174             world_db["Things"][val]["carried"] = True
1175         else:
1176             print("Ignoring: Thing not available for carrying.")
1177     # Note that the whole carrying structure is different from the C version:
1178     # Carried-ness is marked by a "carried" flag, not by Things containing
1179     # Things internally.
1180
1181
1182 @test_Thing_id
1183 def command_tmemthing(str_t, str_y, str_x):
1184     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1185
1186     The type must fit to an existing ThingType, and the position into the map.
1187     """
1188     type = integer_test(str_t, 0, 255)
1189     posy = integer_test(str_y, 0, 255)
1190     posx = integer_test(str_x, 0, 255)
1191     if None != type and None != posy and None != posx:
1192         if type not in world_db["ThingTypes"] \
1193            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1194             print("Ignoring: Illegal value for thing type or position.")
1195         else:
1196             memthing = (type, posy, posx)
1197             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1198
1199
1200 def setter_map(maptype):
1201     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
1202
1203     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
1204     """
1205     @test_Thing_id
1206     def helper(str_int, mapline):
1207         val = integer_test(str_int, 0, 255)
1208         if None != val:
1209             if val >= world_db["MAP_LENGTH"]:
1210                 print("Illegal value for map line number.")
1211             elif len(mapline) != world_db["MAP_LENGTH"]:
1212                 print("Map line length is unequal map width.")
1213             else:
1214                 length = world_db["MAP_LENGTH"]
1215                 map = None
1216                 if not world_db["Things"][command_tid.id][maptype]:
1217                     map = bytearray(b' ' * (length ** 2))
1218                 else:
1219                     map = world_db["Things"][command_tid.id][maptype]
1220                 map[val * length:(val * length) + length] = mapline.encode()
1221                 world_db["Things"][command_tid.id][maptype] = map
1222     return helper
1223
1224
1225 def setter_tpos(axis):
1226     """Generate setter for T_POSX or  T_POSY of selected Thing.
1227
1228     If world is active, rebuilds animate things' fovmap, player's memory map.
1229     """
1230     @test_Thing_id
1231     def helper(str_int):
1232         val = integer_test(str_int, 0, 255)
1233         if None != val:
1234             if val < world_db["MAP_LENGTH"]:
1235                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1236                 if world_db["WORLD_ACTIVE"] \
1237                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1238                     build_fov_map(world_db["Things"][command_tid.id])
1239                     if 0 == command_tid.id:
1240                         update_map_memory(world_db["Things"][command_tid.id])
1241             else:
1242                 print("Ignoring: Position is outside of map.")
1243     return helper
1244
1245
1246 def command_ttid(id_string):
1247     """Set ID of ThingType to manipulate. ID unused? Create new one.
1248
1249     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1250     """
1251     id = id_setter(id_string, "ThingTypes", command_ttid)
1252     if None != id:
1253         world_db["ThingTypes"][id] = {
1254             "TT_NAME": "(none)",
1255             "TT_CONSUMABLE": 0,
1256             "TT_LIFEPOINTS": 0,
1257             "TT_PROLIFERATE": 0,
1258             "TT_START_NUMBER": 0,
1259             "TT_SYMBOL": "?",
1260             "TT_CORPSE_ID": id
1261         }
1262
1263
1264 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1265
1266
1267 @test_ThingType_id
1268 def command_ttname(name):
1269     """Set TT_NAME of selected ThingType."""
1270     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1271
1272
1273 @test_ThingType_id
1274 def command_ttsymbol(char):
1275     """Set TT_SYMBOL of selected ThingType. """
1276     if 1 == len(char):
1277         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1278     else:
1279         print("Ignoring: Argument must be single character.")
1280
1281
1282 @test_ThingType_id
1283 def command_ttcorpseid(str_int):
1284     """Set TT_CORPSE_ID of selected ThingType."""
1285     val = integer_test(str_int, 0, 255)
1286     if None != val:
1287         if val in world_db["ThingTypes"]:
1288             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1289         else:
1290             print("Ignoring: Corpse ID belongs to no known ThignType.")
1291
1292
1293 def command_taid(id_string):
1294     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1295
1296     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1297     """
1298     id = id_setter(id_string, "ThingActions", command_taid, True)
1299     if None != id:
1300         world_db["ThingActions"][id] = {
1301             "TA_EFFORT": 1,
1302             "TA_NAME": "wait"
1303         }
1304
1305
1306 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1307
1308
1309 @test_ThingAction_id
1310 def command_taname(name):
1311     """Set TA_NAME of selected ThingAction.
1312
1313     The name must match a valid thing action function. If after the name
1314     setting no ThingAction with name "wait" remains, call set_world_inactive().
1315     """
1316     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1317        or name == "pick_up":
1318         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1319         if 1 == world_db["WORLD_ACTIVE"]:
1320             wait_defined = False
1321             for id in world_db["ThingActions"]:
1322                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1323                     wait_defined = True
1324                     break
1325             if not wait_defined:
1326                 set_world_inactive()
1327     else:
1328         print("Ignoring: Invalid action name.")
1329     # In contrast to the original,naming won't map a function to a ThingAction.
1330
1331
1332 def command_ai():
1333     """Call ai() on player Thing, then turn_over()."""
1334     ai(world_db["Things"][0])
1335     turn_over()
1336
1337
1338 """Commands database.
1339
1340 Map command start tokens to ([0]) number of expected command arguments, ([1])
1341 the command's meta-ness (i.e. is it to be written to the record file, is it to
1342 be ignored in replay mode if read from server input file), and ([2]) a function
1343 to be called on it.
1344 """
1345 commands_db = {
1346     "QUIT": (0, True, command_quit),
1347     "PING": (0, True, command_ping),
1348     "THINGS_HERE": (2, True, command_thingshere),
1349     "MAKE_WORLD": (1, False, command_makeworld),
1350     "SEED_MAP": (1, False, command_seedmap),
1351     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1352     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1353     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1354     "MAP_LENGTH": (1, False, command_maplength),
1355     "WORLD_ACTIVE": (1, False, command_worldactive),
1356     "TA_ID": (1, False, command_taid),
1357     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1358     "TA_NAME": (1, False, command_taname),
1359     "TT_ID": (1, False, command_ttid),
1360     "TT_NAME": (1, False, command_ttname),
1361     "TT_SYMBOL": (1, False, command_ttsymbol),
1362     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1363     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1364                                        0, 65535)),
1365     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1366                                          0, 255)),
1367     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1368                                         0, 255)),
1369     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1370     "T_ID": (1, False, command_tid),
1371     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1372     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1373     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1374     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1375     "T_COMMAND": (1, False, command_tcommand),
1376     "T_TYPE": (1, False, command_ttype),
1377     "T_CARRIES": (1, False, command_tcarries),
1378     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1379     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1380     "T_MEMTHING": (3, False, command_tmemthing),
1381     "T_POSY": (1, False, setter_tpos("Y")),
1382     "T_POSX": (1, False, setter_tpos("X")),
1383     "wait": (0, False, play_commander("wait")),
1384     "move": (1, False, play_commander("move")),
1385     "pick_up": (0, False, play_commander("pick_up")),
1386     "drop": (1, False, play_commander("drop", True)),
1387     "use": (1, False, play_commander("use", True)),
1388     "ai": (0, False, command_ai)
1389 }
1390
1391
1392 """World state database. With sane default values. (Randomness is in rand.)"""
1393 world_db = {
1394     "TURN": 0,
1395     "MAP_LENGTH": 64,
1396     "SEED_MAP": 0,
1397     "PLAYER_TYPE": 0,
1398     "WORLD_ACTIVE": 0,
1399     "ThingActions": {},
1400     "ThingTypes": {},
1401     "Things": {}
1402 }
1403
1404 """Mapping of direction names to internal direction chars."""
1405 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1406                  "west": "s", "north-west": "w", "north-east": "e"}
1407
1408 """File IO database."""
1409 io_db = {
1410     "path_save": "save",
1411     "path_record": "record",
1412     "path_worldconf": "confserver/world",
1413     "path_server": "server/",
1414     "path_in": "server/in",
1415     "path_out": "server/out",
1416     "path_worldstate": "server/worldstate",
1417     "tmp_suffix": "_tmp",
1418     "kicked_by_rival": False,
1419     "worldstate_updateable": False
1420 }
1421
1422
1423 try:
1424     libpr = prep_library()
1425     rand = RandomnessIO()
1426     opts = parse_command_line_arguments()
1427     setup_server_io()
1428     if None != opts.replay:
1429         replay_game()
1430     else:
1431         play_game()
1432 except SystemExit as exit:
1433     print("ABORTING: " + exit.args[0])
1434 except:
1435     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1436     raise
1437 finally:
1438     cleanup_server_io()