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