home · contact · privacy
Fix bug that created None id'd things in new worlds.
[plomrogue] / roguelike-server
1 #!/usr/bin/python3
2
3 # This file is part of PlomRogue. PlomRogue is licensed under the GPL version 3
4 # or any later version. For details on its copyright, license, and warranties,
5 # see the file NOTICE in the root directory of the PlomRogue source package.
6
7
8 import argparse
9 import errno
10 import os
11 import shlex
12 import shutil
13 import time
14 import ctypes
15
16
17 class RandomnessIO:
18     """"Interface to libplomrogue's pseudo-randomness generator."""
19
20     def set_seed(self, seed):
21         libpr.seed_rrand(1, seed)
22
23     def get_seed(self):
24         return libpr.seed_rrand(0, 0)
25
26     def next(self):
27         return libpr.rrand()
28
29     seed = property(get_seed, set_seed)
30
31
32 def prep_library():
33     """Prepare ctypes library at ./libplomrogue.so"""
34     libpath = ("./libplomrogue.so")
35     if not os.access(libpath, os.F_OK):
36         raise SystemExit("No library " + libpath + ", run ./redo first?")
37     libpr = ctypes.cdll.LoadLibrary(libpath)
38     libpr.seed_rrand.restype = ctypes.c_uint32
39     return libpr
40
41
42 def strong_write(file, string):
43     """Apply write(string), then flush()."""
44     file.write(string)
45     file.flush()
46
47
48 def setup_server_io():
49     """Fill IO files DB with proper file( path)s. Write process IO test string.
50
51     Ensure IO files directory at server/. Remove any old input file if found.
52     Set up new input file for reading, and new output file for writing. Start
53     output file with process hash line of format PID + " " + floated UNIX time
54     (io_db["teststring"]). Raise SystemExit if file is found at path of either
55     record or save file plus io_db["tmp_suffix"].
56     """
57     def detect_atomic_leftover(path, tmp_suffix):
58         path_tmp = path + tmp_suffix
59         msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
60               "aborted previous attempt to write '" + path + "'. Aborting " \
61              "until matter is resolved by removing it from its current path."
62         if os.access(path_tmp, os.F_OK):
63             raise SystemExit(msg)
64     io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
65     io_db["save_wait"] = 0
66     io_db["verbose"] = False
67     io_db["record_chunk"] = ""
68     os.makedirs(io_db["path_server"], exist_ok=True)
69     io_db["file_out"] = open(io_db["path_out"], "w")
70     strong_write(io_db["file_out"], io_db["teststring"] + "\n")
71     if os.access(io_db["path_in"], os.F_OK):
72         os.remove(io_db["path_in"])
73     io_db["file_in"] = open(io_db["path_in"], "w")
74     io_db["file_in"].close()
75     io_db["file_in"] = open(io_db["path_in"], "r")
76     detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
77     detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
78
79
80 def cleanup_server_io():
81     """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
82     def helper(file_key, path_key):
83         if file_key in io_db:
84             io_db[file_key].close()
85         if not io_db["kicked_by_rival"] \
86            and os.access(io_db[path_key], os.F_OK):
87             os.remove(io_db[path_key])
88     helper("file_in", "path_in")
89     helper("file_out", "path_out")
90     helper("file_worldstate", "path_worldstate")
91     if "file_record" in io_db:
92         io_db["file_record"].close()
93
94
95 def obey(command, prefix, replay=False, do_record=False):
96     """Call function from commands_db mapped to command's first token.
97
98     Tokenize command string with shlex.split(comments=True). If replay is set,
99     a non-meta command from the commands_db merely triggers obey() on the next
100     command from the records file. If not, non-meta commands set
101     io_db["worldstate_updateable"] to world_db["WORLD_ACTIVE"], and, if
102     do_record is set, are recorded to io_db["record_chunk"], and save_world()
103     is called (and io_db["record_chunk"] written) if 15 seconds have passed
104     since the last time it was called. The prefix string is inserted into the
105     server's input message between its beginning 'input ' and ':'. All activity
106     is preceded by a server_test() call.
107     """
108     server_test()
109     if io_db["verbose"]:
110         print("input " + prefix + ": " + command)
111     try:
112         tokens = shlex.split(command, comments=True)
113     except ValueError as err:
114         print("Can't tokenize command string: " + str(err) + ".")
115         return
116     if len(tokens) > 0 and tokens[0] in commands_db \
117        and len(tokens) == commands_db[tokens[0]][0] + 1:
118         if commands_db[tokens[0]][1]:
119             commands_db[tokens[0]][2](*tokens[1:])
120         elif replay:
121             print("Due to replay mode, reading command as 'go on in record'.")
122             line = io_db["file_record"].readline()
123             if len(line) > 0:
124                 obey(line.rstrip(), io_db["file_record"].prefix
125                      + str(io_db["file_record"].line_n))
126                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
127             else:
128                 print("Reached end of record file.")
129         else:
130             commands_db[tokens[0]][2](*tokens[1:])
131             if do_record:
132                 io_db["record_chunk"] += command + "\n"
133                 if time.time() > io_db["save_wait"] + 15:
134                     atomic_write(io_db["path_record"], io_db["record_chunk"],
135                                  do_append=True)
136                     save_world()
137                     io_db["record_chunk"] = ""
138                     io_db["save_wait"] = time.time()
139             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
140     elif 0 != len(tokens):
141         print("Invalid command/argument, or bad number of tokens.")
142
143
144 def atomic_write(path, text, do_append=False, delete=True):
145     """Atomic write of text to file at path, appended if do_append is set."""
146     path_tmp = path + io_db["tmp_suffix"]
147     mode = "w"
148     if do_append:
149         mode = "a"
150         if os.access(path, os.F_OK):
151             shutil.copyfile(path, path_tmp)
152     file = open(path_tmp, mode)
153     strong_write(file, text)
154     file.close()
155     if delete and os.access(path, os.F_OK):
156         os.remove(path)
157     os.rename(path_tmp, path)
158
159
160 def save_world():
161     """Save all commands needed to reconstruct current world state."""
162
163     def quote(string):
164         string = string.replace("\u005C", '\u005C\u005C')
165         return '"' + string.replace('"', '\u005C"') + '"'
166
167     def mapsetter(key):
168         def helper(id):
169             string = ""
170             if world_db["Things"][id][key]:
171                 map = world_db["Things"][id][key]
172                 length = world_db["MAP_LENGTH"]
173                 for i in range(length):
174                     line = map[i * length:(i * length) + length].decode()
175                     string = string + key + " " + str(i) + " " + quote(line) \
176                              + "\n"
177             return string
178         return helper
179
180     def memthing(id):
181         string = ""
182         for memthing in world_db["Things"][id]["T_MEMTHING"]:
183             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
184                      str(memthing[1]) + " " + str(memthing[2]) + "\n"
185         return string
186
187     def helper(category, id_string, special_keys={}):
188         string = ""
189         for id in sorted(world_db[category].keys()):
190             string = string + id_string + " " + str(id) + "\n"
191             for key in sorted(world_db[category][id].keys()):
192                 if not key in special_keys:
193                     x = world_db[category][id][key]
194                     argument = quote(x) if str == type(x) else str(x)
195                     string = string + key + " " + argument + "\n"
196                 elif special_keys[key]:
197                     string = string + special_keys[key](id)
198         return string
199
200     string = ""
201     for key in sorted(world_db.keys()):
202         if dict != type(world_db[key]) and key != "MAP" and \
203            key != "WORLD_ACTIVE" and key != "SEED_MAP":
204             string = string + key + " " + str(world_db[key]) + "\n"
205     string = string + "SEED_MAP " + str(world_db["SEED_MAP"]) + "\n"
206     string = string + helper("ThingActions", "TA_ID")
207     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
208     for id in sorted(world_db["ThingTypes"].keys()):
209         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
210                  str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
211     string = string + helper("Things", "T_ID",
212                              {"T_CARRIES": False, "carried": False,
213                               "T_MEMMAP": mapsetter("T_MEMMAP"),
214                               "T_MEMTHING": memthing, "fovmap": False,
215                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
216     for id in sorted(world_db["Things"].keys()):
217         if [] != world_db["Things"][id]["T_CARRIES"]:
218             string = string + "T_ID " + str(id) + "\n"
219             for carried in sorted(world_db["Things"][id]["T_CARRIES"].keys()):
220                 string = string + "T_CARRIES " + str(carried) + "\n"
221     string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
222              "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
223     atomic_write(io_db["path_save"], string)
224
225
226 def obey_lines_in_file(path, name, do_record=False):
227     """Call obey() on each line of path's file, use name in input prefix."""
228     file = open(path, "r")
229     line_n = 1
230     for line in file.readlines():
231         obey(line.rstrip(), name + "file line " + str(line_n),
232              do_record=do_record)
233         line_n = line_n + 1
234     file.close()
235
236
237 def parse_command_line_arguments():
238     """Return settings values read from command line arguments."""
239     parser = argparse.ArgumentParser()
240     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
241                         action='store')
242     parser.add_argument('-l', nargs="?", const="save", dest='savefile',
243                         action="store")
244     parser.add_argument('-v', dest='verbose', action='store_true')
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, delete=False)
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, age_map=True):
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     ord_v = ord("v")
473     ord_0 = ord("0")
474     ord_space = ord(" ")
475     for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
476                 if ord_v == t["fovmap"][pos]]:
477         t["T_MEMDEPTHMAP"][pos] = ord_0
478         if ord_space == t["T_MEMMAP"][pos]:
479             t["T_MEMMAP"][pos] = world_db["MAP"][pos]
480     if age_map:
481         maptype = ctypes.c_char * len(t["T_MEMDEPTHMAP"])
482         memdepthmap = maptype.from_buffer(t["T_MEMDEPTHMAP"])
483         fovmap = maptype.from_buffer(t["fovmap"])
484         libpr.age_some_memdepthmap_on_nonfov_cells(memdepthmap, fovmap)
485     for mt in [mt for mt in t["T_MEMTHING"]
486                if "v" == chr(t["fovmap"][(mt[1] * world_db["MAP_LENGTH"])
487                                          + mt[2]])]:
488          t["T_MEMTHING"].remove(mt)
489     for id in [id for id in world_db["Things"]
490                if not world_db["Things"][id]["carried"]]:
491         type = world_db["Things"][id]["T_TYPE"]
492         if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
493             y = world_db["Things"][id]["T_POSY"]
494             x = world_db["Things"][id]["T_POSX"]
495             if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
496                 t["T_MEMTHING"].append((type, y, x))
497
498
499 def set_world_inactive():
500     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
501     server_test()
502     if os.access(io_db["path_worldstate"], os.F_OK):
503         os.remove(io_db["path_worldstate"])
504     world_db["WORLD_ACTIVE"] = 0
505
506
507 def integer_test(val_string, min, max=None):
508     """Return val_string if integer >= min & (if max set) <= max, else None."""
509     try:
510         val = int(val_string)
511         if val < min or (max != None and val > max):
512             raise ValueError
513         return val
514     except ValueError:
515         msg = "Ignoring: Please use integer >= " + str(min)
516         if max != None:
517             msg += " and <= " + str(max)
518         msg += "."
519         print(msg)
520         return None
521
522
523 def setter(category, key, min, max=None):
524     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
525     if category is None:
526         def f(val_string):
527             val = integer_test(val_string, min, max)
528             if None != val:
529                 world_db[key] = val
530     else:
531         if category == "Thing":
532             id_store = command_tid
533             decorator = test_Thing_id
534         elif category == "ThingType":
535             id_store = command_ttid
536             decorator = test_ThingType_id
537         elif category == "ThingAction":
538             id_store = command_taid
539             decorator = test_ThingAction_id
540
541         @decorator
542         def f(val_string):
543             val = integer_test(val_string, min, max)
544             if None != val:
545                 world_db[category + "s"][id_store.id][key] = val
546     return f
547
548
549 def build_fov_map(t):
550     """Build Thing's FOV map."""
551     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
552     maptype = ctypes.c_char * len(world_db["MAP"])
553     test = libpr.build_fov_map(t["T_POSY"], t["T_POSX"],
554                                maptype.from_buffer(t["fovmap"]),
555                                maptype.from_buffer(world_db["MAP"]))
556     if test:
557         raise RuntimeError("Malloc error in build_fov_Map().")
558
559
560 def decrement_lifepoints(t):
561     """Decrement t's lifepoints by 1, and if to zero, corpse it.
562
563     If t is the player avatar, only blank its fovmap, so that the client may
564     still display memory data. On non-player things, erase fovmap and memory.
565     """
566     t["T_LIFEPOINTS"] -= 1
567     if 0 == t["T_LIFEPOINTS"]:
568         t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
569         if world_db["Things"][0] == t:
570             t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
571             strong_write(io_db["file_out"], "LOG You die.\n")
572         else:
573             t["fovmap"] = False
574             t["T_MEMMAP"] = False
575             t["T_MEMDEPTHMAP"] = False
576             t["T_MEMTHING"] = []
577             strong_write(io_db["file_out"], "LOG It dies.\n")
578
579
580 def mv_yx_in_dir_legal(dir, y, x):
581     """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
582     dir_c = dir.encode("ascii")[0]
583     test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
584     if -1 == test:
585         raise RuntimeError("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
586     return (test, libpr.result_y(), libpr.result_x())
587
588
589 def actor_wait(t):
590     """Make t do nothing (but loudly, if player avatar)."""
591     if t == world_db["Things"][0]:
592         strong_write(io_db["file_out"], "LOG You wait.\n")
593
594
595 def actor_move(t):
596     """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
597     passable = False
598     move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
599                                      t["T_POSY"], t["T_POSX"])
600     if 1 == move_result[0]:
601         pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
602         passable = "." == chr(world_db["MAP"][pos])
603         hitted = [id for id in world_db["Things"]
604                   if world_db["Things"][id] != t
605                   if world_db["Things"][id]["T_LIFEPOINTS"]
606                   if world_db["Things"][id]["T_POSY"] == move_result[1]
607                   if world_db["Things"][id]["T_POSX"] == move_result[2]]
608         if len(hitted):
609             hit_id = hitted[0]
610             hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
611             hitter = "You" if t == world_db["Things"][0] else hitter_name
612             hitted_type = world_db["Things"][hit_id]["T_TYPE"]
613             hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
614             hitted = "you" if hit_id == 0 else hitted_name
615             verb = " wound " if hitter == "You" else " wounds "
616             strong_write(io_db["file_out"], "LOG " + hitter + verb + hitted +
617                                             ".\n")
618             decrement_lifepoints(world_db["Things"][hit_id])
619             return
620     dir = [dir for dir in directions_db
621            if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
622     if passable:
623         t["T_POSY"] = move_result[1]
624         t["T_POSX"] = move_result[2]
625         for id in t["T_CARRIES"]:
626             world_db["Things"][id]["T_POSY"] = move_result[1]
627             world_db["Things"][id]["T_POSX"] = move_result[2]
628         build_fov_map(t)
629         if t == world_db["Things"][0]:
630             strong_write(io_db["file_out"], "LOG You move " + dir + ".\n")
631     elif t == world_db["Things"][0]:
632         strong_write(io_db["file_out"], "LOG You fail to move " + dir + ".\n")
633
634
635 def actor_pick_up(t):
636     """Make t pick up (topmost?) Thing from ground into inventory."""
637     # Topmostness is actually not defined so far. Picks Thing with highest ID.
638     ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
639            if not world_db["Things"][id]["carried"]
640            if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
641            if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
642     if len(ids):
643         highest_id = 0
644         for id in ids:
645             if id > highest_id:
646                 highest_id = id
647         world_db["Things"][highest_id]["carried"] = True
648         t["T_CARRIES"].append(highest_id)
649         if t == world_db["Things"][0]:
650             strong_write(io_db["file_out"], "LOG You pick up an object.\n")
651     elif t == world_db["Things"][0]:
652         err = "You try to pick up an object, but there is none."
653         strong_write(io_db["file_out"], "LOG " + err + "\n")
654
655
656 def actor_drop(t):
657     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
658     # TODO: Handle case where T_ARGUMENT matches nothing.
659     if len(t["T_CARRIES"]):
660         id = t["T_CARRIES"][t["T_ARGUMENT"]]
661         t["T_CARRIES"].remove(id)
662         world_db["Things"][id]["carried"] = False
663         if t == world_db["Things"][0]:
664             strong_write(io_db["file_out"], "LOG You drop an object.\n")
665     elif t == world_db["Things"][0]:
666         err = "You try to drop an object, but you own none."
667         strong_write(io_db["file_out"], "LOG " + err + "\n")
668
669
670 def actor_use(t):
671     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
672     # TODO: Handle case where T_ARGUMENT matches nothing.
673     if len(t["T_CARRIES"]):
674         id = t["T_CARRIES"][t["T_ARGUMENT"]]
675         type = world_db["Things"][id]["T_TYPE"]
676         if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
677             t["T_CARRIES"].remove(id)
678             del world_db["Things"][id]
679             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
680             if t == world_db["Things"][0]:
681                 strong_write(io_db["file_out"],
682                              "LOG You consume this object.\n")
683         elif t == world_db["Things"][0]:
684             strong_write(io_db["file_out"],
685                          "LOG You try to use this object, but fail.\n")
686     elif t == world_db["Things"][0]:
687         strong_write(io_db["file_out"],
688                      "LOG You try to use an object, but you own none.\n")
689
690
691 def thingproliferation(t):
692     """To chance of 1/TT_PROLIFERATE, create t offspring in neighbor cell.
693
694     Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be
695     passable and not be inhabited by a Thing of the same type, or, if Thing is
696     animate, any other animate Thing. If there are several map cell candidates,
697     one is selected randomly.
698     """
699     def test_cell(t, y, x):
700         if "." == chr(world_db["MAP"][(y * world_db["MAP_LENGTH"]) + x]):
701             for id in [id for id in world_db["Things"]
702                        if y == world_db["Things"][id]["T_POSY"]
703                        if x == world_db["Things"][id]["T_POSX"]
704                        if (t["T_TYPE"] == world_db["Things"][id]["T_TYPE"])
705                        or (t["T_LIFEPOINTS"] and
706                            world_db["Things"][id]["T_LIFEPOINTS"])]:
707                 return False
708             return True
709         return False
710     prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
711     if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)):
712         candidates = []
713         for dir in [directions_db[key] for key in directions_db]:
714             mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
715             if mv_result[0] and test_cell(t, mv_result[1], mv_result[2]):
716                 candidates.append((mv_result[1], mv_result[2]))
717         if len(candidates):
718             i = rand.next() % len(candidates)
719             id = id_setter(-1, "Things")
720             newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
721             world_db["Things"][id] = newT
722
723
724 def try_healing(t):
725     """Grow t's HP to a 1/32 chance if < HP max, satiation > 0, and waiting.
726
727     On success, decrease satiation score by 32.
728     """
729     if t["T_SATIATION"] > 0 \
730        and t["T_LIFEPOINTS"] < \
731            world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"] \
732        and 0 == (rand.next() % 31) \
733        and t["T_COMMAND"] == [id for id in world_db["ThingActions"]
734                               if world_db["ThingActions"][id]["TA_NAME"] ==
735                                  "wait"][0]:
736         t["T_LIFEPOINTS"] += 1
737         t["T_SATIATION"] -= 32
738         if t == world_db["Things"][0]:
739             strong_write(io_db["file_out"], "LOG You heal.\n")
740         else:
741             name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
742             strong_write(io_db["file_out"], "LOG " + name + "heals.\n")
743
744
745 def hunger(t):
746     """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
747     if t["T_SATIATION"] > -32768:
748         t["T_SATIATION"] -= 1
749     testbase = t["T_SATIATION"] if t["T_SATIATION"] >= 0 else -t["T_SATIATION"]
750     if not world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]:
751         raise RuntimeError("A thing that should not hunger is hungering.")
752     stomach = int(32767 / world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"])
753     if int(int(testbase / stomach) / ((rand.next() % stomach) + 1)):
754         if t == world_db["Things"][0]:
755             strong_write(io_db["file_out"], "LOG You suffer from hunger.\n")
756         else:
757             name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
758             strong_write(io_db["file_out"], "LOG " + name +
759                                             " suffers from hunger.\n")
760         decrement_lifepoints(t)
761
762
763 def get_dir_to_target(t, filter):
764     """Try to set T_COMMAND/T_ARGUMENT for move to "filter"-determined target.
765
766     The path-wise nearest target is chosen, via the shortest available path.
767     Target must not be t. On succcess, return positive value, else False.
768     Filters:
769     "a": Thing in FOV is below a certain distance, animate, but of ThingType
770          that is not t's, and starts out weaker than t is; build path as
771          avoiding things of t's ThingType
772     "f": neighbor cell (not inhabited by any animate Thing) further away from
773          animate Thing not further than x steps away and in FOV and of a
774          ThingType that is not t's, and starts out stronger or as strong as t
775          is currently; or (cornered), if no such flight cell, but Thing of
776          above criteria is too near,1 a cell closer to it, or, if less near,
777          just wait
778     "c": Thing in memorized map is consumable
779     "s": memory map cell with greatest-reachable degree of unexploredness
780     """
781
782     def zero_score_map_where_char_on_memdepthmap(c):
783         maptype = ctypes.c_char * len(t["T_MEMDEPTHMAP"])
784         map = maptype.from_buffer(t["T_MEMDEPTHMAP"])
785         test = libpr.zero_score_map_where_char_on_memdepthmap(c, map)
786         if test:
787             raise RuntimeError("No score map allocated for "
788                                "zero_score_map_where_char_on_memdepthmap().")
789
790     def set_map_score(pos, score):
791         test = libpr.set_map_score(pos, score)
792         if test:
793             raise RuntimeError("No score map allocated for set_map_score().")
794
795     def get_map_score(pos):
796         result = libpr.get_map_score(pos)
797         if result < 0:
798             raise RuntimeError("No score map allocated for get_map_score().")
799         return result
800
801     def seeing_thing():
802         if t["fovmap"] and ("a" == filter or "f" == filter):
803             for id in world_db["Things"]:
804                 Thing = world_db["Things"][id]
805                 if Thing != t and Thing["T_LIFEPOINTS"] and \
806                    t["T_TYPE"] != Thing["T_TYPE"] and \
807                    'v' == chr(t["fovmap"][(Thing["T_POSY"]
808                                           * world_db["MAP_LENGTH"])
809                                           + Thing["T_POSX"]]):
810                     ThingType = world_db["ThingTypes"][Thing["T_TYPE"]]
811                     if ("f" == filter and ThingType["TT_LIFEPOINTS"] >=
812                                           t["T_LIFEPOINTS"]) \
813                        or ("a" == filter and ThingType["TT_LIFEPOINTS"] <
814                                              t["T_LIFEPOINTS"]):
815                         return True
816         elif t["T_MEMMAP"] and "c" == filter:
817             for mt in t["T_MEMTHING"]:
818                 if ' ' != chr(t["T_MEMMAP"][(mt[1] * world_db["MAP_LENGTH"])
819                                          + mt[2]]) \
820                    and world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]:
821                     return True
822         return False
823
824     def init_score_map():
825         test = libpr.init_score_map()
826         if test:
827             raise RuntimeError("Malloc error in init_score_map().")
828         ord_dot = ord(".")
829         ord_v = ord("v")
830         ord_blank = ord(" ")
831         for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
832                   if ord_dot == t["T_MEMMAP"][i]]:
833             set_map_score(i, 65535 - 1)
834         if "a" == filter:
835             for id in world_db["Things"]:
836                 Thing = world_db["Things"][id]
837                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
838                       + Thing["T_POSX"]
839                 if t != Thing and Thing["T_LIFEPOINTS"] and \
840                    t["T_TYPE"] != Thing["T_TYPE"] and \
841                    ord_v == t["fovmap"][pos] and \
842                    t["T_LIFEPOINTS"] > \
843                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
844                     set_map_score(pos, 0)
845                 elif t["T_TYPE"] == Thing["T_TYPE"]:
846                     set_map_score(pos, 65535)
847         elif "f" == filter:
848             for id in [id for id in world_db["Things"]
849                        if world_db["Things"][id]["T_LIFEPOINTS"]]:
850                 Thing = world_db["Things"][id]
851                 pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \
852                       + Thing["T_POSX"]
853                 if t["T_TYPE"] != Thing["T_TYPE"] and \
854                    ord_v == t["fovmap"][pos] and \
855                    t["T_LIFEPOINTS"] <= \
856                    world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]:
857                     set_map_score(pos, 0)
858         elif "c" == filter:
859             for mt in [mt for mt in t["T_MEMTHING"]
860                        if ord_blank != t["T_MEMMAP"][mt[1]
861                                                     * world_db["MAP_LENGTH"]
862                                                     + mt[2]]
863                        if world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]]:
864                 set_map_score(mt[1] * world_db["MAP_LENGTH"] + mt[2], 0)
865         elif "s" == filter:
866             zero_score_map_where_char_on_memdepthmap(mem_depth_c[0])
867
868     def rand_target_dir(neighbors, cmp, dirs):
869         candidates = []
870         n_candidates = 0
871         for i in range(len(dirs)):
872             if cmp == neighbors[i]:
873                 candidates.append(dirs[i])
874                 n_candidates += 1
875         return candidates[rand.next() % n_candidates] if n_candidates else 0
876
877     def get_neighbor_scores(dirs, eye_pos):
878         scores = []
879         if libpr.ready_neighbor_scores(eye_pos):
880             raise RuntimeError("No score map allocated for " +
881                                "ready_neighbor_scores.()")
882         for i in range(len(dirs)):
883             scores.append(libpr.get_neighbor_score(i))
884         return scores
885
886     def get_dir_from_neighbors():
887         dir_to_target = False
888         dirs = "edcxsw"
889         eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
890         neighbors = get_neighbor_scores(dirs, eye_pos)
891         if "f" == filter:
892             inhabited = [world_db["Things"][id]["T_POSY"]
893                          * world_db["MAP_LENGTH"]
894                          + world_db["Things"][id]["T_POSX"]
895                          for id in world_db["Things"]
896                          if world_db["Things"][id]["T_LIFEPOINTS"]]
897             for i in range(len(dirs)):
898                 mv_yx_in_dir_legal(dirs[i], t["T_POSY"], t["T_POSX"])
899                 pos_cmp = libpr.result_y() * world_db["MAP_LENGTH"] \
900                           + libpr.result_x()
901                 for pos in [pos for pos in inhabited if pos == pos_cmp]:
902                     neighbors[i] = 65535
903                     break
904         minmax_start = 0 if "f" == filter else 65535 - 1
905         minmax_neighbor = minmax_start
906         for i in range(len(dirs)):
907             if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
908                 minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
909                or ("f" != filter and minmax_neighbor > neighbors[i]):
910                 minmax_neighbor = neighbors[i]
911         if minmax_neighbor != minmax_start:
912             dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
913         if "f" == filter:
914             if not dir_to_target:
915                 if 1 == get_map_score(eye_pos):
916                     dir_to_target = rand_target_dir(neighbors, 0, dirs)
917                 elif 3 >= get_map_score(eye_pos):
918                     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
919                                       if
920                                       world_db["ThingActions"][id]["TA_NAME"]
921                                          == "wait"][0]
922                     return 1
923             elif dir_to_target and 3 < get_map_score(eye_pos):
924                 dir_to_target = 0
925         elif "a" == filter and 10 <= get_map_score(eye_pos):
926             dir_to_target = 0
927         return dir_to_target
928
929     dir_to_target = False
930     mem_depth_c = b' '
931     run_i = 9 + 1 if "s" == filter else 1
932     while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
933         run_i -= 1
934         init_score_map()
935         mem_depth_c = b'9' if b' ' == mem_depth_c \
936                            else bytes([mem_depth_c[0] - 1])
937         if libpr.dijkstra_map():
938             raise RuntimeError("No score map allocated for dijkstra_map().")
939         dir_to_target = get_dir_from_neighbors()
940         libpr.free_score_map()
941         if dir_to_target and str == type(dir_to_target):
942             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
943                               if world_db["ThingActions"][id]["TA_NAME"]
944                                  == "move"][0]
945             t["T_ARGUMENT"] = ord(dir_to_target)
946     return dir_to_target
947
948
949 def standing_on_consumable(t):
950     """Return True/False whether t is standing on a consumable."""
951     for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
952                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
953                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
954                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
955                           ["TT_CONSUMABLE"]]:
956         return True
957     return False
958
959
960 def get_inventory_slot_to_consume(t):
961     """Return slot Id of strongest consumable in t's inventory, else -1."""
962     cmp_consumability = 0
963     selection = -1
964     i = 0
965     for id in t["T_CARRIES"]:
966         type = world_db["Things"][id]["T_TYPE"]
967         if world_db["ThingTypes"][type]["TT_CONSUMABLE"] > cmp_consumability:
968             cmp_consumability = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
969             selection = i
970         i += 1
971     return selection
972
973
974 def ai(t):
975     """Determine next command/argment for actor t via AI algorithms.
976
977     AI will look for, and move towards, enemies (animate Things not of their
978     own ThingType); if they see none, they will consume consumables in their
979     inventory; if there are none, they will pick up what they stand on if they
980     stand on consumables; if they stand on none, they will move towards the
981     next consumable they see or remember on the map; if they see or remember
982     none, they will explore parts of the map unseen since ever or for at least
983     one turn; if there is nothing to explore, they will simply wait.
984     """
985     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
986                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
987     if not get_dir_to_target(t, "f"):
988         sel = get_inventory_slot_to_consume(t)
989         if -1 != sel:
990             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
991                               if world_db["ThingActions"][id]["TA_NAME"]
992                                  == "use"][0]
993             t["T_ARGUMENT"] = sel
994         elif standing_on_consumable(t):
995             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
996                               if world_db["ThingActions"][id]["TA_NAME"]
997                                  == "pick_up"][0]
998         elif (not get_dir_to_target(t, "c")) and \
999              (not get_dir_to_target(t, "a")):
1000             get_dir_to_target(t, "s")
1001
1002
1003 def turn_over():
1004     """Run game world and its inhabitants until new player input expected."""
1005     id = 0
1006     whilebreaker = False
1007     while world_db["Things"][0]["T_LIFEPOINTS"]:
1008         for id in [id for id in world_db["Things"]]:  # Only what's from start!
1009             if not id in world_db["Things"] or \
1010                world_db["Things"][id]["carried"]:   # May have been consumed or
1011                 continue                            # picked up during turn …
1012             Thing = world_db["Things"][id]
1013             if Thing["T_LIFEPOINTS"]:
1014                 if not Thing["T_COMMAND"]:
1015                     update_map_memory(Thing)
1016                     if 0 == id:
1017                         whilebreaker = True
1018                         break
1019                     ai(Thing)
1020                 try_healing(Thing)
1021                 Thing["T_PROGRESS"] += 1
1022                 taid = [a for a in world_db["ThingActions"]
1023                           if a == Thing["T_COMMAND"]][0]
1024                 ThingAction = world_db["ThingActions"][taid]
1025                 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
1026                     eval("actor_" + ThingAction["TA_NAME"])(Thing)
1027                     Thing["T_COMMAND"] = 0
1028                     Thing["T_PROGRESS"] = 0
1029                 hunger(Thing)
1030             thingproliferation(Thing)
1031         if whilebreaker:
1032             break
1033         world_db["TURN"] += 1
1034
1035
1036 def new_Thing(type, pos=(0, 0)):
1037     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
1038     thing = {
1039         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
1040         "T_ARGUMENT": 0,
1041         "T_PROGRESS": 0,
1042         "T_SATIATION": 0,
1043         "T_COMMAND": 0,
1044         "T_TYPE": type,
1045         "T_POSY": pos[0],
1046         "T_POSX": pos[1],
1047         "T_CARRIES": [],
1048         "carried": False,
1049         "T_MEMTHING": [],
1050         "T_MEMMAP": False,
1051         "T_MEMDEPTHMAP": False,
1052         "fovmap": False
1053     }
1054     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
1055         build_fov_map(thing)
1056     return thing
1057
1058
1059 def id_setter(id, category, id_store=False, start_at_1=False):
1060     """Set ID of object of category to manipulate ID unused? Create new one.
1061     The ID is stored as id_store.id (if id_store is set). If the integer of the
1062     input is valid (if start_at_1, >= 0, else >= -1), but <0 or (if start_at_1)
1063     <1, calculate new ID: lowest unused ID >=0 or (if start_at_1) >= 1. None is
1064     always returned when no new object is created, else the new object's ID.
1065     """
1066     min = 0 if start_at_1 else -1
1067     if str == type(id):
1068         id = integer_test(id, min)
1069     if None != id:
1070         if id in world_db[category]:
1071             if id_store:
1072                 id_store.id = id
1073             return None
1074         else:
1075             if (start_at_1 and 0 == id) \
1076                or ((not start_at_1) and (id < 0)):
1077                 id = 0 if start_at_1 else -1
1078                 while 1:
1079                     id = id + 1
1080                     if id not in world_db[category]:
1081                         break
1082             if id_store:
1083                 id_store.id = id
1084     return id
1085
1086
1087 def command_ping():
1088     """Send PONG line to server output file."""
1089     strong_write(io_db["file_out"], "PONG\n")
1090
1091
1092 def command_quit():
1093     """Abort server process."""
1094     save_world()
1095     atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True)
1096     raise SystemExit("received QUIT command")
1097
1098
1099 def command_thingshere(str_y, str_x):
1100     """Write to out file list of Things known to player at coordinate y, x."""
1101     if world_db["WORLD_ACTIVE"]:
1102         y = integer_test(str_y, 0, 255)
1103         x = integer_test(str_x, 0, 255)
1104         length = world_db["MAP_LENGTH"]
1105         if None != y and None != x and y < length and x < length:
1106             pos = (y * world_db["MAP_LENGTH"]) + x
1107             strong_write(io_db["file_out"], "THINGS_HERE START\n")
1108             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
1109                 for id in world_db["Things"]:
1110                     if y == world_db["Things"][id]["T_POSY"] \
1111                        and x == world_db["Things"][id]["T_POSX"] \
1112                        and not world_db["Things"][id]["carried"]:
1113                         type = world_db["Things"][id]["T_TYPE"]
1114                         name = world_db["ThingTypes"][type]["TT_NAME"]
1115                         strong_write(io_db["file_out"], name + "\n")
1116             else:
1117                 for mt in world_db["Things"][0]["T_MEMTHING"]:
1118                     if y == mt[1] and x == mt[2]:
1119                         name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
1120                         strong_write(io_db["file_out"], name + "\n")
1121             strong_write(io_db["file_out"], "THINGS_HERE END\n")
1122         else:
1123             print("Ignoring: Invalid map coordinates.")
1124     else:
1125         print("Ignoring: Command only works on existing worlds.")
1126
1127
1128 def play_commander(action, args=False):
1129     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
1130
1131     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
1132     """
1133
1134     def set_command():
1135         id = [x for x in world_db["ThingActions"]
1136                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
1137         world_db["Things"][0]["T_COMMAND"] = id
1138         turn_over()
1139
1140     def set_command_and_argument_int(str_arg):
1141         val = integer_test(str_arg, 0, 255)
1142         if None != val:
1143             world_db["Things"][0]["T_ARGUMENT"] = val
1144             set_command()
1145
1146     def set_command_and_argument_movestring(str_arg):
1147         if str_arg in directions_db:
1148             world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
1149             set_command()
1150         else:
1151             print("Ignoring: Argument must be valid direction string.")
1152
1153     if action == "move":
1154         return set_command_and_argument_movestring
1155     elif args:
1156         return set_command_and_argument_int
1157     else:
1158         return set_command
1159
1160
1161 def command_seedrandomness(seed_string):
1162     """Set rand seed to int(seed_string)."""
1163     val = integer_test(seed_string, 0, 4294967295)
1164     if None != val:
1165         rand.seed = val
1166
1167
1168 def command_seedmap(seed_string):
1169     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
1170     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
1171     remake_map()
1172
1173
1174 def command_makeworld(seed_string):
1175     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
1176
1177     Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
1178     "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
1179     TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
1180     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1181     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1182     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1183     other. Init player's memory map. Write "NEW_WORLD" line to out file.
1184     """
1185
1186     def free_pos():
1187         i = 0
1188         while 1:
1189             err = "Space to put thing on too hard to find. Map too small?"
1190             while 1:
1191                 y = rand.next() % world_db["MAP_LENGTH"]
1192                 x = rand.next() % world_db["MAP_LENGTH"]
1193                 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
1194                     break
1195                 i += 1
1196                 if i == 65535:
1197                     raise SystemExit(err)
1198             # Replica of C code, wrongly ignores animatedness of new Thing.
1199             pos_clear = (0 == len([id for id in world_db["Things"]
1200                                    if world_db["Things"][id]["T_LIFEPOINTS"]
1201                                    if world_db["Things"][id]["T_POSY"] == y
1202                                    if world_db["Things"][id]["T_POSX"] == x]))
1203             if pos_clear:
1204                 break
1205         return (y, x)
1206
1207     val = integer_test(seed_string, 0, 4294967295)
1208     if None == val:
1209         return
1210     rand.seed = val
1211     world_db["SEED_MAP"] = val
1212     player_will_be_generated = False
1213     playertype = world_db["PLAYER_TYPE"]
1214     for ThingType in world_db["ThingTypes"]:
1215         if playertype == ThingType:
1216             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1217                 player_will_be_generated = True
1218             break
1219     if not player_will_be_generated:
1220         print("Ignoring beyond SEED_MAP: " +
1221               "No player type with start number >0 defined.")
1222         return
1223     wait_action = False
1224     for ThingAction in world_db["ThingActions"]:
1225         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1226             wait_action = True
1227     if not wait_action:
1228         print("Ignoring beyond SEED_MAP: " +
1229               "No thing action with name 'wait' defined.")
1230         return
1231     world_db["Things"] = {}
1232     remake_map()
1233     world_db["WORLD_ACTIVE"] = 1
1234     world_db["TURN"] = 1
1235     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1236         id = id_setter(-1, "Things")
1237         world_db["Things"][id] = new_Thing(playertype, free_pos())
1238     update_map_memory(world_db["Things"][0])
1239     for type in world_db["ThingTypes"]:
1240         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1241             if type != playertype:
1242                 id = id_setter(-1, "Things")
1243                 world_db["Things"][id] = new_Thing(type, free_pos())
1244     strong_write(io_db["file_out"], "NEW_WORLD\n")
1245
1246
1247 def command_maplength(maplength_string):
1248     """Redefine map length. Invalidate map, therefore lose all things on it."""
1249     val = integer_test(maplength_string, 1, 256)
1250     if None != val:
1251         world_db["MAP_LENGTH"] = val
1252         set_world_inactive()
1253         world_db["Things"] = {}
1254         libpr.set_maplength(val)
1255
1256
1257 def command_worldactive(worldactive_string):
1258     """Toggle world_db["WORLD_ACTIVE"] if possible.
1259
1260     An active world can always be set inactive. An inactive world can only be
1261     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
1262     activation, rebuild all Things' FOVs, and the player's map memory.
1263     """
1264     # In original version, map existence was also tested (unnecessarily?).
1265     val = integer_test(worldactive_string, 0, 1)
1266     if val:
1267         if 0 != world_db["WORLD_ACTIVE"]:
1268             if 0 == val:
1269                 set_world_inactive()
1270             else:
1271                 print("World already active.")
1272         elif 0 == world_db["WORLD_ACTIVE"]:
1273             wait_exists = False
1274             for ThingAction in world_db["ThingActions"]:
1275                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1276                     wait_exists = True
1277                     break
1278             player_exists = False
1279             for Thing in world_db["Things"]:
1280                 if 0 == Thing:
1281                     player_exists = True
1282                     break
1283             if wait_exists and player_exists:
1284                 for id in world_db["Things"]:
1285                     if world_db["Things"][id]["T_LIFEPOINTS"]:
1286                         build_fov_map(world_db["Things"][id])
1287                         if 0 == id:
1288                             update_map_memory(world_db["Things"][id], False)
1289                 world_db["WORLD_ACTIVE"] = 1
1290
1291
1292 def test_for_id_maker(object, category):
1293     """Return decorator testing for object having "id" attribute."""
1294     def decorator(f):
1295         def helper(*args):
1296             if hasattr(object, "id"):
1297                 f(*args)
1298             else:
1299                 print("Ignoring: No " + category +
1300                       " defined to manipulate yet.")
1301         return helper
1302     return decorator
1303
1304
1305 def command_tid(id_string):
1306     """Set ID of Thing to manipulate. ID unused? Create new one.
1307
1308     Default new Thing's type to the first available ThingType, others: zero.
1309     """
1310     id = id_setter(id_string, "Things", command_tid)
1311     if None != id:
1312         if world_db["ThingTypes"] == {}:
1313             print("Ignoring: No ThingType to settle new Thing in.")
1314             return
1315         type = list(world_db["ThingTypes"].keys())[0]
1316         world_db["Things"][id] = new_Thing(type)
1317
1318
1319 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1320
1321
1322 @test_Thing_id
1323 def command_tcommand(str_int):
1324     """Set T_COMMAND of selected Thing."""
1325     val = integer_test(str_int, 0)
1326     if None != val:
1327         if 0 == val or val in world_db["ThingActions"]:
1328             world_db["Things"][command_tid.id]["T_COMMAND"] = val
1329         else:
1330             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1331
1332
1333 @test_Thing_id
1334 def command_ttype(str_int):
1335     """Set T_TYPE of selected Thing."""
1336     val = integer_test(str_int, 0)
1337     if None != val:
1338         if val in world_db["ThingTypes"]:
1339             world_db["Things"][command_tid.id]["T_TYPE"] = val
1340         else:
1341             print("Ignoring: ThingType ID belongs to no known ThingType.")
1342
1343
1344 @test_Thing_id
1345 def command_tcarries(str_int):
1346     """Append int(str_int) to T_CARRIES of selected Thing.
1347
1348     The ID int(str_int) must not be of the selected Thing, and must belong to a
1349     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1350     """
1351     val = integer_test(str_int, 0)
1352     if None != val:
1353         if val == command_tid.id:
1354             print("Ignoring: Thing cannot carry itself.")
1355         elif val in world_db["Things"] \
1356              and not world_db["Things"][val]["carried"]:
1357             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1358             world_db["Things"][val]["carried"] = True
1359         else:
1360             print("Ignoring: Thing not available for carrying.")
1361     # Note that the whole carrying structure is different from the C version:
1362     # Carried-ness is marked by a "carried" flag, not by Things containing
1363     # Things internally.
1364
1365
1366 @test_Thing_id
1367 def command_tmemthing(str_t, str_y, str_x):
1368     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1369
1370     The type must fit to an existing ThingType, and the position into the map.
1371     """
1372     type = integer_test(str_t, 0)
1373     posy = integer_test(str_y, 0, 255)
1374     posx = integer_test(str_x, 0, 255)
1375     if None != type and None != posy and None != posx:
1376         if type not in world_db["ThingTypes"] \
1377            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1378             print("Ignoring: Illegal value for thing type or position.")
1379         else:
1380             memthing = (type, posy, posx)
1381             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1382
1383
1384 def setter_map(maptype):
1385     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
1386
1387     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
1388     """
1389     @test_Thing_id
1390     def helper(str_int, mapline):
1391         val = integer_test(str_int, 0, 255)
1392         if None != val:
1393             if val >= world_db["MAP_LENGTH"]:
1394                 print("Illegal value for map line number.")
1395             elif len(mapline) != world_db["MAP_LENGTH"]:
1396                 print("Map line length is unequal map width.")
1397             else:
1398                 length = world_db["MAP_LENGTH"]
1399                 map = None
1400                 if not world_db["Things"][command_tid.id][maptype]:
1401                     map = bytearray(b' ' * (length ** 2))
1402                 else:
1403                     map = world_db["Things"][command_tid.id][maptype]
1404                 map[val * length:(val * length) + length] = mapline.encode()
1405                 world_db["Things"][command_tid.id][maptype] = map
1406     return helper
1407
1408
1409 def setter_tpos(axis):
1410     """Generate setter for T_POSX or  T_POSY of selected Thing.
1411
1412     If world is active, rebuilds animate things' fovmap, player's memory map.
1413     """
1414     @test_Thing_id
1415     def helper(str_int):
1416         val = integer_test(str_int, 0, 255)
1417         if None != val:
1418             if val < world_db["MAP_LENGTH"]:
1419                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1420                 if world_db["WORLD_ACTIVE"] \
1421                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1422                     build_fov_map(world_db["Things"][command_tid.id])
1423                     if 0 == command_tid.id:
1424                         update_map_memory(world_db["Things"][command_tid.id])
1425             else:
1426                 print("Ignoring: Position is outside of map.")
1427     return helper
1428
1429
1430 def command_ttid(id_string):
1431     """Set ID of ThingType to manipulate. ID unused? Create new one.
1432
1433     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1434     """
1435     id = id_setter(id_string, "ThingTypes", command_ttid)
1436     if None != id:
1437         world_db["ThingTypes"][id] = {
1438             "TT_NAME": "(none)",
1439             "TT_CONSUMABLE": 0,
1440             "TT_LIFEPOINTS": 0,
1441             "TT_PROLIFERATE": 0,
1442             "TT_START_NUMBER": 0,
1443             "TT_SYMBOL": "?",
1444             "TT_CORPSE_ID": id
1445         }
1446
1447
1448 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1449
1450
1451 @test_ThingType_id
1452 def command_ttname(name):
1453     """Set TT_NAME of selected ThingType."""
1454     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1455
1456
1457 @test_ThingType_id
1458 def command_ttsymbol(char):
1459     """Set TT_SYMBOL of selected ThingType. """
1460     if 1 == len(char):
1461         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1462     else:
1463         print("Ignoring: Argument must be single character.")
1464
1465
1466 @test_ThingType_id
1467 def command_ttcorpseid(str_int):
1468     """Set TT_CORPSE_ID of selected ThingType."""
1469     val = integer_test(str_int, 0)
1470     if None != val:
1471         if val in world_db["ThingTypes"]:
1472             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1473         else:
1474             print("Ignoring: Corpse ID belongs to no known ThignType.")
1475
1476
1477 def command_taid(id_string):
1478     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1479
1480     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1481     """
1482     id = id_setter(id_string, "ThingActions", command_taid, True)
1483     if None != id:
1484         world_db["ThingActions"][id] = {
1485             "TA_EFFORT": 1,
1486             "TA_NAME": "wait"
1487         }
1488
1489
1490 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1491
1492
1493 @test_ThingAction_id
1494 def command_taname(name):
1495     """Set TA_NAME of selected ThingAction.
1496
1497     The name must match a valid thing action function. If after the name
1498     setting no ThingAction with name "wait" remains, call set_world_inactive().
1499     """
1500     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1501        or name == "pick_up":
1502         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1503         if 1 == world_db["WORLD_ACTIVE"]:
1504             wait_defined = False
1505             for id in world_db["ThingActions"]:
1506                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1507                     wait_defined = True
1508                     break
1509             if not wait_defined:
1510                 set_world_inactive()
1511     else:
1512         print("Ignoring: Invalid action name.")
1513     # In contrast to the original,naming won't map a function to a ThingAction.
1514
1515
1516 def command_ai():
1517     """Call ai() on player Thing, then turn_over()."""
1518     ai(world_db["Things"][0])
1519     turn_over()
1520
1521
1522 """Commands database.
1523
1524 Map command start tokens to ([0]) number of expected command arguments, ([1])
1525 the command's meta-ness (i.e. is it to be written to the record file, is it to
1526 be ignored in replay mode if read from server input file), and ([2]) a function
1527 to be called on it.
1528 """
1529 commands_db = {
1530     "QUIT": (0, True, command_quit),
1531     "PING": (0, True, command_ping),
1532     "THINGS_HERE": (2, True, command_thingshere),
1533     "MAKE_WORLD": (1, False, command_makeworld),
1534     "SEED_MAP": (1, False, command_seedmap),
1535     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1536     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1537     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0)),
1538     "MAP_LENGTH": (1, False, command_maplength),
1539     "WORLD_ACTIVE": (1, False, command_worldactive),
1540     "TA_ID": (1, False, command_taid),
1541     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1542     "TA_NAME": (1, False, command_taname),
1543     "TT_ID": (1, False, command_ttid),
1544     "TT_NAME": (1, False, command_ttname),
1545     "TT_SYMBOL": (1, False, command_ttsymbol),
1546     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1547     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1548                                        0, 65535)),
1549     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1550                                          0, 255)),
1551     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1552                                         0, 255)),
1553     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1554     "T_ID": (1, False, command_tid),
1555     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1556     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1557     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1558     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1559     "T_COMMAND": (1, False, command_tcommand),
1560     "T_TYPE": (1, False, command_ttype),
1561     "T_CARRIES": (1, False, command_tcarries),
1562     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1563     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1564     "T_MEMTHING": (3, False, command_tmemthing),
1565     "T_POSY": (1, False, setter_tpos("Y")),
1566     "T_POSX": (1, False, setter_tpos("X")),
1567     "wait": (0, False, play_commander("wait")),
1568     "move": (1, False, play_commander("move")),
1569     "pick_up": (0, False, play_commander("pick_up")),
1570     "drop": (1, False, play_commander("drop", True)),
1571     "use": (1, False, play_commander("use", True)),
1572     "ai": (0, False, command_ai)
1573 }
1574
1575
1576 """World state database. With sane default values. (Randomness is in rand.)"""
1577 world_db = {
1578     "TURN": 0,
1579     "MAP_LENGTH": 64,
1580     "SEED_MAP": 0,
1581     "PLAYER_TYPE": 0,
1582     "WORLD_ACTIVE": 0,
1583     "ThingActions": {},
1584     "ThingTypes": {},
1585     "Things": {}
1586 }
1587
1588 """Mapping of direction names to internal direction chars."""
1589 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1590                  "west": "s", "north-west": "w", "north-east": "e"}
1591
1592 """File IO database."""
1593 io_db = {
1594     "path_save": "save",
1595     "path_record": "record_save",
1596     "path_worldconf": "confserver/world",
1597     "path_server": "server/",
1598     "path_in": "server/in",
1599     "path_out": "server/out",
1600     "path_worldstate": "server/worldstate",
1601     "tmp_suffix": "_tmp",
1602     "kicked_by_rival": False,
1603     "worldstate_updateable": False
1604 }
1605
1606
1607 try:
1608     libpr = prep_library()
1609     rand = RandomnessIO()
1610     opts = parse_command_line_arguments()
1611     if opts.savefile:
1612         io_db["path_save"] = opts.savefile
1613         io_db["path_record"] = "record_" + opts.savefile
1614     setup_server_io()
1615     if opts.verbose:
1616         io_db["verbose"] = True
1617     if None != opts.replay:
1618         replay_game()
1619     else:
1620         play_game()
1621 except SystemExit as exit:
1622     print("ABORTING: " + exit.args[0])
1623 except:
1624     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1625     raise
1626 finally:
1627     cleanup_server_io()