home · contact · privacy
Server/py: Fix memdepthmapping 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):
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 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 world_db["Things"]:
489         type = world_db["Things"][id]["T_TYPE"]
490         if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
491             y = world_db["Things"][id]["T_POSY"]
492             x = world_db["Things"][id]["T_POSX"]
493             if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
494                 t["T_MEMTHING"].append((type, y, x))
495
496
497 def set_world_inactive():
498     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
499     server_test()
500     if os.access(io_db["path_worldstate"], os.F_OK):
501         os.remove(io_db["path_worldstate"])
502     world_db["WORLD_ACTIVE"] = 0
503
504
505 def integer_test(val_string, min, max):
506     """Return val_string if possible integer >= min and <= max, else None."""
507     try:
508         val = int(val_string)
509         if val < min or val > max:
510             raise ValueError
511         return val
512     except ValueError:
513         print("Ignoring: Please use integer >= " + str(min) + " and <= " +
514               str(max) + ".")
515         return None
516
517
518 def setter(category, key, min, max):
519     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
520     if category is None:
521         def f(val_string):
522             val = integer_test(val_string, min, max)
523             if None != val:
524                 world_db[key] = val
525     else:
526         if category == "Thing":
527             id_store = command_tid
528             decorator = test_Thing_id
529         elif category == "ThingType":
530             id_store = command_ttid
531             decorator = test_ThingType_id
532         elif category == "ThingAction":
533             id_store = command_taid
534             decorator = test_ThingAction_id
535
536         @decorator
537         def f(val_string):
538             val = integer_test(val_string, min, max)
539             if None != val:
540                 world_db[category + "s"][id_store.id][key] = val
541     return f
542
543
544 def build_fov_map(t):
545     """Build Thing's FOV map."""
546     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
547     maptype = ctypes.c_char * len(world_db["MAP"])
548     test = libpr.build_fov_map(t["T_POSY"], t["T_POSX"],
549                                maptype.from_buffer(t["fovmap"]),
550                                maptype.from_buffer(world_db["MAP"]))
551     if test:
552         raise RuntimeError("Malloc error in build_fov_Map().")
553
554
555 def decrement_lifepoints(t):
556     """Decrement t's lifepoints by 1, and if to zero, corpse it.
557
558     If t is the player avatar, only blank its fovmap, so that the client may
559     still display memory data. On non-player things, erase fovmap and memory.
560     """
561     t["T_LIFEPOINTS"] -= 1
562     if 0 == t["T_LIFEPOINTS"]:
563         t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
564         if world_db["Things"][0] == t:
565             t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
566             strong_write(io_db["file_out"], "LOG You die.\n")
567         else:
568             t["fovmap"] = False
569             t["T_MEMMAP"] = False
570             t["T_MEMDEPTHMAP"] = False
571             t["T_MEMTHING"] = []
572             strong_write(io_db["file_out"], "LOG It dies.\n")
573
574
575 def mv_yx_in_dir_legal(dir, y, x):
576     """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
577     dir_c = dir.encode("ascii")[0]
578     test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
579     if -1 == test:
580         raise RuntimeError("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
581     return (test, libpr.result_y(), libpr.result_x())
582
583
584 def actor_wait(t):
585     """Make t do nothing (but loudly, if player avatar)."""
586     if t == world_db["Things"][0]:
587         strong_write(io_db["file_out"], "LOG You wait.\n")
588
589
590 def actor_move(t):
591     """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
592     passable = False
593     move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
594                                      t["T_POSY"], t["T_POSX"])
595     if 1 == move_result[0]:
596         pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
597         passable = "." == chr(world_db["MAP"][pos])
598         hitted = [id for id in world_db["Things"]
599                   if world_db["Things"][id] != t
600                   if world_db["Things"][id]["T_LIFEPOINTS"]
601                   if world_db["Things"][id]["T_POSY"] == move_result[1]
602                   if world_db["Things"][id]["T_POSX"] == move_result[2]]
603         if len(hitted):
604             hit_id = hitted[0]
605             hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
606             hitter = "You" if t == world_db["Things"][0] else hitter_name
607             hitted_type = world_db["Things"][hit_id]["T_TYPE"]
608             hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
609             hitted = "you" if hit_id == 0 else hitted_name
610             verb = " wound " if hitter == "You" else " wounds "
611             strong_write(io_db["file_out"], "LOG " + hitter + verb + hitted +
612                                             ".\n")
613             decrement_lifepoints(world_db["Things"][hit_id])
614             return
615     dir = [dir for dir in directions_db
616            if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
617     if passable:
618         t["T_POSY"] = move_result[1]
619         t["T_POSX"] = move_result[2]
620         for id in t["T_CARRIES"]:
621             world_db["Things"][id]["T_POSY"] = move_result[1]
622             world_db["Things"][id]["T_POSX"] = move_result[2]
623         build_fov_map(t)
624         strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
625     else:
626         strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
627
628
629 def actor_pick_up(t):
630     """Make t pick up (topmost?) Thing from ground into inventory."""
631     # Topmostness is actually not defined so far.
632     ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
633            if not world_db["Things"][id]["carried"]
634            if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
635            if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
636     if len(ids):
637         world_db["Things"][ids[0]]["carried"] = True
638         t["T_CARRIES"].append(ids[0])
639         if t == world_db["Things"][0]:
640             strong_write(io_db["file_out"], "LOG You pick up an object.\n")
641     elif t == world_db["Things"][0]:
642         err = "You try to pick up an object, but there is none."
643         strong_write(io_db["file_out"], "LOG " + err + "\n")
644
645
646 def actor_drop(t):
647     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
648     # TODO: Handle case where T_ARGUMENT matches nothing.
649     if len(t["T_CARRIES"]):
650         id = t["T_CARRIES"][t["T_ARGUMENT"]]
651         t["T_CARRIES"].remove(id)
652         world_db["Things"][id]["carried"] = False
653         if t == world_db["Things"][0]:
654             strong_write(io_db["file_out"], "LOG You drop an object.\n")
655     elif t == world_db["Things"][0]:
656         err = "You try to drop an object, but you own none."
657         strong_write(io_db["file_out"], "LOG " + err + "\n")
658
659
660 def actor_use(t):
661     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
662     # TODO: Handle case where T_ARGUMENT matches nothing.
663     if len(t["T_CARRIES"]):
664         id = t["T_CARRIES"][t["T_ARGUMENT"]]
665         type = world_db["Things"][id]["T_TYPE"]
666         if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
667             t["T_CARRIES"].remove(id)
668             del world_db["Things"][id]
669             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
670             t["T_LIFEPOINTS"] += 1
671             # Wrongly increment HPs is a replica of the original code.
672             strong_write(io_db["file_out"], "LOG You consume this object.\n")
673         else:
674             strong_write(io_db["file_out"], "LOG You try to use this object," +
675                                             "but fail.\n")
676     else:
677         strong_write(io_db["file_out"], "LOG You try to use an object, but " +
678                                         "you own none.\n")
679
680
681 def thingproliferation(t):
682     """To chance of 1/TT_PROLIFERATE, create t offspring in neighbor cell.
683
684     Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be
685     passable and not be inhabited by a Thing of the same type, or, if Thing is
686     animate, any other animate Thing. If there are several map cell candidates,
687     one is selected randomly.
688     """
689     def test_cell(t, y, x):
690         if "." == chr(world_db["MAP"][(y * world_db["MAP_LENGTH"]) + x]):
691             for id in [id for id in world_db["Things"]
692                        if y == world_db["Things"][id]["T_POSY"]
693                        if x == world_db["Things"][id]["T_POSX"]
694                        if (t["T_TYPE"] == world_db["Things"][id]["T_TYPE"])
695                        or (t["T_LIFEPOINTS"] and
696                            world_db["Things"][id]["T_LIFEPOINTS"])]:
697                 return False
698             return True
699         return False
700     prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
701     if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)):
702         candidates = []
703         for dir in [directions_db[key] for key in directions_db]:
704             mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
705             if mv_result[0] and test_cell(t, mv_result[1], mv_result[2]):
706                 candidates.append((mv_result[1], mv_result[2]))
707         if len(candidates):
708             i = rand.next() % len(candidates)
709             id = id_setter(-1, "Things")
710             newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
711             world_db["Things"][id] = newT
712
713
714 def try_healing(t):
715     """Grow t's HP to a 1/32 chance if < HP max, satiation > 0, and waiting.
716
717     On success, decrease satiation score by 32.
718     """
719     if t["T_SATIATION"] > 0 \
720        and t["T_LIFEPOINTS"] < \
721            world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"] \
722        and 0 == (rand.next() % 31) \
723        and t["T_COMMAND"] == [id for id in world_db["ThingActions"]
724                               if world_db["ThingActions"][id]["TA_NAME"] ==
725                                  "wait"][0]:
726         t["T_LIFEPOINTS"] += 1
727         t["T_SATIATION"] -= 32
728         if t == world_db["Things"][0]:
729             strong_write(io_db["file_out"], "LOG You heal.\n")
730         else:
731             name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
732             strong_write(io_db["file_out"], "LOG " + name + "heals.\n")
733
734
735 def hunger(t):
736     """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
737     if t["T_SATIATION"] > -32768:
738         t["T_SATIATION"] -= 1
739     testbase = t["T_SATIATION"] if t["T_SATIATION"] >= 0 else -t["T_SATIATION"]
740     if not world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]:
741         raise RuntimeError("A thing that should not hunger is hungering.")
742     stomach = int(32767 / world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"])
743     if int(int(testbase / stomach) / ((rand.next() % stomach) + 1)):
744         if t == world_db["Things"][0]:
745             strong_write(io_db["file_out"], "LOG You suffer from hunger.\n")
746         else:
747             name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
748             strong_write(io_db["file_out"], "LOG " + name +
749                                             " suffers from hunger.\n")
750         decrement_lifepoints(t)
751
752
753 def get_dir_to_nearest_target(t, c):
754     # Dummy
755     return False
756
757
758 def standing_on_consumable(t):
759     """Return True/False whether t is standing on a consumable."""
760     for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
761                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
762                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
763                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
764                           ["TT_CONSUMABLE"]]:
765         return True
766     return False
767
768
769 def get_inventory_slot_to_consume(t):
770     """Return slot Id of strongest consumable in t's inventory, else -1."""
771     cmp_consumability = 0
772     selection = -1
773     i = 0
774     for id in t["T_CARRIES"]:
775         type = world_db["Things"][id]["T_TYPE"]
776         if world_db["ThingTypes"][type]["TT_CONSUMABLE"] > cmp_consumability:
777             cmp_consumability = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
778             selection = i
779         i += 1
780     return selection
781
782
783 def ai(t):
784     """Determine next command/argment for actor t via AI algorithms.
785
786     AI will look for, and move towards, enemies (animate Things not of their
787     own ThingType); if they see none, they will consume consumables in their
788     inventory; if there are none, they will pick up what they stand on if they
789     stand on consumables; if they stand on none, they will move towards the
790     next consumable they see or remember on the map; if they see or remember
791     none, they will explore parts of the map unseen since ever or for at least
792     one turn; if there is nothing to explore, they will simply wait.
793     """
794     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
795                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
796     if not get_dir_to_nearest_target(t, "f"):
797         sel = get_inventory_slot_to_consume(t)
798         if -1 != sel:
799             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
800                               if world_db["ThingActions"][id]["TA_NAME"]
801                                  == "use"][0]
802             t["T_ARGUMENT"] = sel
803         elif standing_on_consumable(t):
804             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
805                               if world_db["ThingActions"][id]["TA_NAME"]
806                                  == "pick_up"][0]
807         elif (not get_dir_to_nearest_target(t, "c")) and \
808              (not get_dir_to_nearest_target(t, "a")):
809             get_dir_to_nearest_target(t, "s")
810
811
812 def turn_over():
813     """Run game world and its inhabitants until new player input expected."""
814     id = 0
815     whilebreaker = False
816     while world_db["Things"][0]["T_LIFEPOINTS"]:
817         for id in [id for id in world_db["Things"]]:
818             if not id in world_db["Things"]: # Thing may have been consumed
819                 continue                     # during turn …
820             Thing = world_db["Things"][id]
821             if Thing["T_LIFEPOINTS"]:
822                 if not Thing["T_COMMAND"]:
823                     update_map_memory(Thing)
824                     if 0 == id:
825                         whilebreaker = True
826                         break
827                     ai(Thing)
828                     Thing["T_COMMAND"] = 1
829                 try_healing(Thing)
830                 Thing["T_PROGRESS"] += 1
831                 taid = [a for a in world_db["ThingActions"]
832                           if a == Thing["T_COMMAND"]][0]
833                 ThingAction = world_db["ThingActions"][taid]
834                 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
835                     eval("actor_" + ThingAction["TA_NAME"])(Thing)
836                     Thing["T_COMMAND"] = 0
837                     Thing["T_PROGRESS"] = 0
838                 hunger(Thing)
839             thingproliferation(Thing)
840         if whilebreaker:
841             break
842         world_db["TURN"] += 1
843
844
845 def new_Thing(type, pos=(0, 0)):
846     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
847     thing = {
848         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
849         "T_ARGUMENT": 0,
850         "T_PROGRESS": 0,
851         "T_SATIATION": 0,
852         "T_COMMAND": 0,
853         "T_TYPE": type,
854         "T_POSY": pos[0],
855         "T_POSX": pos[1],
856         "T_CARRIES": [],
857         "carried": False,
858         "T_MEMTHING": [],
859         "T_MEMMAP": False,
860         "T_MEMDEPTHMAP": False,
861         "fovmap": False
862     }
863     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
864         build_fov_map(thing)
865     return thing
866
867
868 def id_setter(id, category, id_store=False, start_at_1=False):
869     """Set ID of object of category to manipulate ID unused? Create new one.
870
871     The ID is stored as id_store.id (if id_store is set). If the integer of the
872     input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
873     32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
874     >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
875     new object is created, otherwise the new object's ID.
876     """
877     min = 0 if start_at_1 else -32768
878     max = 255 if start_at_1 else 32767
879     if str == type(id):
880         id = integer_test(id, min, max)
881     if None != id:
882         if id in world_db[category]:
883             if id_store:
884                 id_store.id = id
885             return None
886         else:
887             if (start_at_1 and 0 == id) \
888                or ((not start_at_1) and (id < 0 or id > 255)):
889                 id = -1
890                 while 1:
891                     id = id + 1
892                     if id not in world_db[category]:
893                         break
894                 if id > 255:
895                     print("Ignoring: "
896                           "No unused ID available to add to ID list.")
897                     return None
898             if id_store:
899                 id_store.id = id
900     return id
901
902
903 def command_ping():
904     """Send PONG line to server output file."""
905     strong_write(io_db["file_out"], "PONG\n")
906
907
908 def command_quit():
909     """Abort server process."""
910     raise SystemExit("received QUIT command")
911
912
913 def command_thingshere(str_y, str_x):
914     """Write to out file list of Things known to player at coordinate y, x."""
915     if world_db["WORLD_ACTIVE"]:
916         y = integer_test(str_y, 0, 255)
917         x = integer_test(str_x, 0, 255)
918         length = world_db["MAP_LENGTH"]
919         if None != y and None != x and y < length and x < length:
920             pos = (y * world_db["MAP_LENGTH"]) + x
921             strong_write(io_db["file_out"], "THINGS_HERE START\n")
922             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
923                 for id in world_db["Things"]:
924                     # write_thing_if_here()
925                     if y == world_db["Things"][id]["T_POSY"] \
926                        and x == world_db["Things"][id]["T_POSX"] \
927                        and not world_db["Things"][id]["carried"]:
928                         type = world_db["Things"][id]["T_TYPE"]
929                         name = world_db["ThingTypes"][type]["TT_NAME"]
930                         strong_write(io_db["file_out"], name + "\n")
931             else:
932                 for mt in world_db["Things"][0]["T_MEMTHING"]:
933                     if y == mt[1] and x == mt[2]:
934                         name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
935                         strong_write(io_db["file_out"], name + "\n")
936             strong_write(io_db["file_out"], "THINGS_HERE END\n")
937         else:
938             print("Ignoring: Invalid map coordinates.")
939     else:
940         print("Ignoring: Command only works on existing worlds.")
941
942
943 def play_commander(action, args=False):
944     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
945
946     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
947     """
948
949     def set_command():
950         id = [x for x in world_db["ThingActions"]
951                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
952         world_db["Things"][0]["T_COMMAND"] = id
953         turn_over()
954
955     def set_command_and_argument_int(str_arg):
956         val = integer_test(str_arg, 0, 255)
957         if None != val:
958             world_db["Things"][0]["T_ARGUMENT"] = val
959             set_command()
960
961     def set_command_and_argument_movestring(str_arg):
962         if str_arg in directions_db:
963             world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
964             set_command()
965         else:
966             print("Ignoring: Argument must be valid direction string.")
967
968     if action == "move":
969         return set_command_and_argument_movestring
970     elif args:
971         return set_command_and_argument_int
972     else:
973         return set_command
974
975
976 def command_seedrandomness(seed_string):
977     """Set rand seed to int(seed_string)."""
978     val = integer_test(seed_string, 0, 4294967295)
979     if None != val:
980         rand.seed = val
981
982
983 def command_seedmap(seed_string):
984     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
985     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
986     remake_map()
987
988
989 def command_makeworld(seed_string):
990     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
991
992     Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
993     "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
994     TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
995     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
996     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
997     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
998     other. Init player's memory map. Write "NEW_WORLD" line to out file.
999     """
1000
1001     def free_pos():
1002         i = 0
1003         while 1:
1004             err = "Space to put thing on too hard to find. Map too small?"
1005             while 1:
1006                 y = rand.next() % world_db["MAP_LENGTH"]
1007                 x = rand.next() % world_db["MAP_LENGTH"]
1008                 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
1009                     break
1010                 i += 1
1011                 if i == 65535:
1012                     raise SystemExit(err)
1013             # Replica of C code, wrongly ignores animatedness of new Thing.
1014             pos_clear = (0 == len([id for id in world_db["Things"]
1015                                    if world_db["Things"][id]["T_LIFEPOINTS"]
1016                                    if world_db["Things"][id]["T_POSY"] == y
1017                                    if world_db["Things"][id]["T_POSX"] == x]))
1018             if pos_clear:
1019                 break
1020         return (y, x)
1021
1022     val = integer_test(seed_string, 0, 4294967295)
1023     if None == val:
1024         return
1025     rand.seed = val
1026     world_db["SEED_MAP"] = val
1027     player_will_be_generated = False
1028     playertype = world_db["PLAYER_TYPE"]
1029     for ThingType in world_db["ThingTypes"]:
1030         if playertype == ThingType:
1031             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1032                 player_will_be_generated = True
1033             break
1034     if not player_will_be_generated:
1035         print("Ignoring beyond SEED_MAP: " +
1036               "No player type with start number >0 defined.")
1037         return
1038     wait_action = False
1039     for ThingAction in world_db["ThingActions"]:
1040         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1041             wait_action = True
1042     if not wait_action:
1043         print("Ignoring beyond SEED_MAP: " +
1044               "No thing action with name 'wait' defined.")
1045         return
1046     world_db["Things"] = {}
1047     remake_map()
1048     world_db["WORLD_ACTIVE"] = 1
1049     world_db["TURN"] = 1
1050     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1051         id = id_setter(-1, "Things")
1052         world_db["Things"][id] = new_Thing(playertype, free_pos())
1053     update_map_memory(world_db["Things"][0])
1054     for type in world_db["ThingTypes"]:
1055         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1056             if type != playertype:
1057                 id = id_setter(-1, "Things")
1058                 world_db["Things"][id] = new_Thing(type, free_pos())
1059     strong_write(io_db["file_out"], "NEW_WORLD\n")
1060
1061
1062 def command_maplength(maplength_string):
1063     """Redefine map length. Invalidate map, therefore lose all things on it."""
1064     val = integer_test(maplength_string, 1, 256)
1065     if None != val:
1066         world_db["MAP_LENGTH"] = val
1067         set_world_inactive()
1068         world_db["Things"] = {}
1069         libpr.set_maplength(val)
1070
1071
1072 def command_worldactive(worldactive_string):
1073     """Toggle world_db["WORLD_ACTIVE"] if possible.
1074
1075     An active world can always be set inactive. An inactive world can only be
1076     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
1077     activation, rebuild all Things' FOVs, and the player's map memory.
1078     """
1079     # In original version, map existence was also tested (unnecessarily?).
1080     val = integer_test(worldactive_string, 0, 1)
1081     if val:
1082         if 0 != world_db["WORLD_ACTIVE"]:
1083             if 0 == val:
1084                 set_world_inactive()
1085             else:
1086                 print("World already active.")
1087         elif 0 == world_db["WORLD_ACTIVE"]:
1088             wait_exists = False
1089             for ThingAction in world_db["ThingActions"]:
1090                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1091                     wait_exists = True
1092                     break
1093             player_exists = False
1094             for Thing in world_db["Things"]:
1095                 if 0 == Thing:
1096                     player_exists = True
1097                     break
1098             if wait_exists and player_exists:
1099                 for id in world_db["Things"]:
1100                     if world_db["Things"][id]["T_LIFEPOINTS"]:
1101                         build_fov_map(world_db["Things"][id])
1102                         if 0 == id:
1103                             update_map_memory(world_db["Things"][id])
1104                 world_db["WORLD_ACTIVE"] = 1
1105
1106
1107 def test_for_id_maker(object, category):
1108     """Return decorator testing for object having "id" attribute."""
1109     def decorator(f):
1110         def helper(*args):
1111             if hasattr(object, "id"):
1112                 f(*args)
1113             else:
1114                 print("Ignoring: No " + category +
1115                       " defined to manipulate yet.")
1116         return helper
1117     return decorator
1118
1119
1120 def command_tid(id_string):
1121     """Set ID of Thing to manipulate. ID unused? Create new one.
1122
1123     Default new Thing's type to the first available ThingType, others: zero.
1124     """
1125     id = id_setter(id_string, "Things", command_tid)
1126     if None != id:
1127         if world_db["ThingTypes"] == {}:
1128             print("Ignoring: No ThingType to settle new Thing in.")
1129             return
1130         type = list(world_db["ThingTypes"].keys())[0]
1131         world_db["Things"][id] = new_Thing(type)
1132
1133
1134 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1135
1136
1137 @test_Thing_id
1138 def command_tcommand(str_int):
1139     """Set T_COMMAND of selected Thing."""
1140     val = integer_test(str_int, 0, 255)
1141     if None != val:
1142         if 0 == val or val in world_db["ThingActions"]:
1143             world_db["Things"][command_tid.id]["T_COMMAND"] = val
1144         else:
1145             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1146
1147
1148 @test_Thing_id
1149 def command_ttype(str_int):
1150     """Set T_TYPE of selected Thing."""
1151     val = integer_test(str_int, 0, 255)
1152     if None != val:
1153         if val in world_db["ThingTypes"]:
1154             world_db["Things"][command_tid.id]["T_TYPE"] = val
1155         else:
1156             print("Ignoring: ThingType ID belongs to no known ThingType.")
1157
1158
1159 @test_Thing_id
1160 def command_tcarries(str_int):
1161     """Append int(str_int) to T_CARRIES of selected Thing.
1162
1163     The ID int(str_int) must not be of the selected Thing, and must belong to a
1164     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1165     """
1166     val = integer_test(str_int, 0, 255)
1167     if None != val:
1168         if val == command_tid.id:
1169             print("Ignoring: Thing cannot carry itself.")
1170         elif val in world_db["Things"] \
1171              and not world_db["Things"][val]["carried"]:
1172             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1173             world_db["Things"][val]["carried"] = True
1174         else:
1175             print("Ignoring: Thing not available for carrying.")
1176     # Note that the whole carrying structure is different from the C version:
1177     # Carried-ness is marked by a "carried" flag, not by Things containing
1178     # Things internally.
1179
1180
1181 @test_Thing_id
1182 def command_tmemthing(str_t, str_y, str_x):
1183     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1184
1185     The type must fit to an existing ThingType, and the position into the map.
1186     """
1187     type = integer_test(str_t, 0, 255)
1188     posy = integer_test(str_y, 0, 255)
1189     posx = integer_test(str_x, 0, 255)
1190     if None != type and None != posy and None != posx:
1191         if type not in world_db["ThingTypes"] \
1192            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1193             print("Ignoring: Illegal value for thing type or position.")
1194         else:
1195             memthing = (type, posy, posx)
1196             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1197
1198
1199 def setter_map(maptype):
1200     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
1201
1202     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
1203     """
1204     @test_Thing_id
1205     def helper(str_int, mapline):
1206         val = integer_test(str_int, 0, 255)
1207         if None != val:
1208             if val >= world_db["MAP_LENGTH"]:
1209                 print("Illegal value for map line number.")
1210             elif len(mapline) != world_db["MAP_LENGTH"]:
1211                 print("Map line length is unequal map width.")
1212             else:
1213                 length = world_db["MAP_LENGTH"]
1214                 map = None
1215                 if not world_db["Things"][command_tid.id][maptype]:
1216                     map = bytearray(b' ' * (length ** 2))
1217                 else:
1218                     map = world_db["Things"][command_tid.id][maptype]
1219                 map[val * length:(val * length) + length] = mapline.encode()
1220                 world_db["Things"][command_tid.id][maptype] = map
1221     return helper
1222
1223
1224 def setter_tpos(axis):
1225     """Generate setter for T_POSX or  T_POSY of selected Thing.
1226
1227     If world is active, rebuilds animate things' fovmap, player's memory map.
1228     """
1229     @test_Thing_id
1230     def helper(str_int):
1231         val = integer_test(str_int, 0, 255)
1232         if None != val:
1233             if val < world_db["MAP_LENGTH"]:
1234                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1235                 if world_db["WORLD_ACTIVE"] \
1236                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1237                     build_fov_map(world_db["Things"][command_tid.id])
1238                     if 0 == command_tid.id:
1239                         update_map_memory(world_db["Things"][command_tid.id])
1240             else:
1241                 print("Ignoring: Position is outside of map.")
1242     return helper
1243
1244
1245 def command_ttid(id_string):
1246     """Set ID of ThingType to manipulate. ID unused? Create new one.
1247
1248     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1249     """
1250     id = id_setter(id_string, "ThingTypes", command_ttid)
1251     if None != id:
1252         world_db["ThingTypes"][id] = {
1253             "TT_NAME": "(none)",
1254             "TT_CONSUMABLE": 0,
1255             "TT_LIFEPOINTS": 0,
1256             "TT_PROLIFERATE": 0,
1257             "TT_START_NUMBER": 0,
1258             "TT_SYMBOL": "?",
1259             "TT_CORPSE_ID": id
1260         }
1261
1262
1263 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1264
1265
1266 @test_ThingType_id
1267 def command_ttname(name):
1268     """Set TT_NAME of selected ThingType."""
1269     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1270
1271
1272 @test_ThingType_id
1273 def command_ttsymbol(char):
1274     """Set TT_SYMBOL of selected ThingType. """
1275     if 1 == len(char):
1276         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1277     else:
1278         print("Ignoring: Argument must be single character.")
1279
1280
1281 @test_ThingType_id
1282 def command_ttcorpseid(str_int):
1283     """Set TT_CORPSE_ID of selected ThingType."""
1284     val = integer_test(str_int, 0, 255)
1285     if None != val:
1286         if val in world_db["ThingTypes"]:
1287             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1288         else:
1289             print("Ignoring: Corpse ID belongs to no known ThignType.")
1290
1291
1292 def command_taid(id_string):
1293     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1294
1295     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1296     """
1297     id = id_setter(id_string, "ThingActions", command_taid, True)
1298     if None != id:
1299         world_db["ThingActions"][id] = {
1300             "TA_EFFORT": 1,
1301             "TA_NAME": "wait"
1302         }
1303
1304
1305 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1306
1307
1308 @test_ThingAction_id
1309 def command_taname(name):
1310     """Set TA_NAME of selected ThingAction.
1311
1312     The name must match a valid thing action function. If after the name
1313     setting no ThingAction with name "wait" remains, call set_world_inactive().
1314     """
1315     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1316        or name == "pick_up":
1317         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1318         if 1 == world_db["WORLD_ACTIVE"]:
1319             wait_defined = False
1320             for id in world_db["ThingActions"]:
1321                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1322                     wait_defined = True
1323                     break
1324             if not wait_defined:
1325                 set_world_inactive()
1326     else:
1327         print("Ignoring: Invalid action name.")
1328     # In contrast to the original,naming won't map a function to a ThingAction.
1329
1330
1331 def command_ai():
1332     """Call ai() on player Thing, then turn_over()."""
1333     ai(world_db["Things"][0])
1334     turn_over()
1335
1336
1337 """Commands database.
1338
1339 Map command start tokens to ([0]) number of expected command arguments, ([1])
1340 the command's meta-ness (i.e. is it to be written to the record file, is it to
1341 be ignored in replay mode if read from server input file), and ([2]) a function
1342 to be called on it.
1343 """
1344 commands_db = {
1345     "QUIT": (0, True, command_quit),
1346     "PING": (0, True, command_ping),
1347     "THINGS_HERE": (2, True, command_thingshere),
1348     "MAKE_WORLD": (1, False, command_makeworld),
1349     "SEED_MAP": (1, False, command_seedmap),
1350     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1351     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1352     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1353     "MAP_LENGTH": (1, False, command_maplength),
1354     "WORLD_ACTIVE": (1, False, command_worldactive),
1355     "TA_ID": (1, False, command_taid),
1356     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1357     "TA_NAME": (1, False, command_taname),
1358     "TT_ID": (1, False, command_ttid),
1359     "TT_NAME": (1, False, command_ttname),
1360     "TT_SYMBOL": (1, False, command_ttsymbol),
1361     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1362     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1363                                        0, 65535)),
1364     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1365                                          0, 255)),
1366     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1367                                         0, 255)),
1368     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1369     "T_ID": (1, False, command_tid),
1370     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1371     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1372     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1373     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1374     "T_COMMAND": (1, False, command_tcommand),
1375     "T_TYPE": (1, False, command_ttype),
1376     "T_CARRIES": (1, False, command_tcarries),
1377     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1378     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1379     "T_MEMTHING": (3, False, command_tmemthing),
1380     "T_POSY": (1, False, setter_tpos("Y")),
1381     "T_POSX": (1, False, setter_tpos("X")),
1382     "wait": (0, False, play_commander("wait")),
1383     "move": (1, False, play_commander("move")),
1384     "pick_up": (0, False, play_commander("pick_up")),
1385     "drop": (1, False, play_commander("drop", True)),
1386     "use": (1, False, play_commander("use", True)),
1387     "ai": (0, False, command_ai)
1388 }
1389
1390
1391 """World state database. With sane default values. (Randomness is in rand.)"""
1392 world_db = {
1393     "TURN": 0,
1394     "MAP_LENGTH": 64,
1395     "SEED_MAP": 0,
1396     "PLAYER_TYPE": 0,
1397     "WORLD_ACTIVE": 0,
1398     "ThingActions": {},
1399     "ThingTypes": {},
1400     "Things": {}
1401 }
1402
1403 """Mapping of direction names to internal direction chars."""
1404 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1405                  "west": "s", "north-west": "w", "north-east": "e"}
1406
1407 """File IO database."""
1408 io_db = {
1409     "path_save": "save",
1410     "path_record": "record",
1411     "path_worldconf": "confserver/world",
1412     "path_server": "server/",
1413     "path_in": "server/in",
1414     "path_out": "server/out",
1415     "path_worldstate": "server/worldstate",
1416     "tmp_suffix": "_tmp",
1417     "kicked_by_rival": False,
1418     "worldstate_updateable": False
1419 }
1420
1421
1422 try:
1423     libpr = prep_library()
1424     rand = RandomnessIO()
1425     opts = parse_command_line_arguments()
1426     setup_server_io()
1427     if None != opts.replay:
1428         replay_game()
1429     else:
1430         play_game()
1431 except SystemExit as exit:
1432     print("ABORTING: " + exit.args[0])
1433 except:
1434     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1435     raise
1436 finally:
1437     cleanup_server_io()