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