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