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