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