home · contact · privacy
Server/py: Fix thing proliferation bug in turn_over().
[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"]]: # Only what is from start!
824             if not id in world_db["Things"] or \
825                world_db["Things"][id]["carried"]:# Thing may have been consumed
826                 continue                         # or picked up during turn …
827             Thing = world_db["Things"][id]
828             if Thing["T_LIFEPOINTS"]:
829                 if not Thing["T_COMMAND"]:
830                     update_map_memory(Thing)
831                     if 0 == id:
832                         whilebreaker = True
833                         break
834                     ai(Thing)
835                     Thing["T_COMMAND"] = 1
836                 try_healing(Thing)
837                 Thing["T_PROGRESS"] += 1
838                 taid = [a for a in world_db["ThingActions"]
839                           if a == Thing["T_COMMAND"]][0]
840                 ThingAction = world_db["ThingActions"][taid]
841                 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
842                     eval("actor_" + ThingAction["TA_NAME"])(Thing)
843                     Thing["T_COMMAND"] = 0
844                     Thing["T_PROGRESS"] = 0
845                 hunger(Thing)
846             thingproliferation(Thing)
847         if whilebreaker:
848             break
849         world_db["TURN"] += 1
850
851
852 def new_Thing(type, pos=(0, 0)):
853     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
854     thing = {
855         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
856         "T_ARGUMENT": 0,
857         "T_PROGRESS": 0,
858         "T_SATIATION": 0,
859         "T_COMMAND": 0,
860         "T_TYPE": type,
861         "T_POSY": pos[0],
862         "T_POSX": pos[1],
863         "T_CARRIES": [],
864         "carried": False,
865         "T_MEMTHING": [],
866         "T_MEMMAP": False,
867         "T_MEMDEPTHMAP": False,
868         "fovmap": False
869     }
870     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
871         build_fov_map(thing)
872     return thing
873
874
875 def id_setter(id, category, id_store=False, start_at_1=False):
876     """Set ID of object of category to manipulate ID unused? Create new one.
877
878     The ID is stored as id_store.id (if id_store is set). If the integer of the
879     input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
880     32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
881     >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
882     new object is created, otherwise the new object's ID.
883     """
884     min = 0 if start_at_1 else -32768
885     max = 255 if start_at_1 else 32767
886     if str == type(id):
887         id = integer_test(id, min, max)
888     if None != id:
889         if id in world_db[category]:
890             if id_store:
891                 id_store.id = id
892             return None
893         else:
894             if (start_at_1 and 0 == id) \
895                or ((not start_at_1) and (id < 0 or id > 255)):
896                 id = -1
897                 while 1:
898                     id = id + 1
899                     if id not in world_db[category]:
900                         break
901                 if id > 255:
902                     print("Ignoring: "
903                           "No unused ID available to add to ID list.")
904                     return None
905             if id_store:
906                 id_store.id = id
907     return id
908
909
910 def command_ping():
911     """Send PONG line to server output file."""
912     strong_write(io_db["file_out"], "PONG\n")
913
914
915 def command_quit():
916     """Abort server process."""
917     raise SystemExit("received QUIT command")
918
919
920 def command_thingshere(str_y, str_x):
921     """Write to out file list of Things known to player at coordinate y, x."""
922     if world_db["WORLD_ACTIVE"]:
923         y = integer_test(str_y, 0, 255)
924         x = integer_test(str_x, 0, 255)
925         length = world_db["MAP_LENGTH"]
926         if None != y and None != x and y < length and x < length:
927             pos = (y * world_db["MAP_LENGTH"]) + x
928             strong_write(io_db["file_out"], "THINGS_HERE START\n")
929             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
930                 for id in world_db["Things"]:
931                     # write_thing_if_here()
932                     if y == world_db["Things"][id]["T_POSY"] \
933                        and x == world_db["Things"][id]["T_POSX"] \
934                        and not world_db["Things"][id]["carried"]:
935                         type = world_db["Things"][id]["T_TYPE"]
936                         name = world_db["ThingTypes"][type]["TT_NAME"]
937                         strong_write(io_db["file_out"], name + "\n")
938             else:
939                 for mt in world_db["Things"][0]["T_MEMTHING"]:
940                     if y == mt[1] and x == mt[2]:
941                         name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
942                         strong_write(io_db["file_out"], name + "\n")
943             strong_write(io_db["file_out"], "THINGS_HERE END\n")
944         else:
945             print("Ignoring: Invalid map coordinates.")
946     else:
947         print("Ignoring: Command only works on existing worlds.")
948
949
950 def play_commander(action, args=False):
951     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
952
953     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
954     """
955
956     def set_command():
957         id = [x for x in world_db["ThingActions"]
958                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
959         world_db["Things"][0]["T_COMMAND"] = id
960         turn_over()
961
962     def set_command_and_argument_int(str_arg):
963         val = integer_test(str_arg, 0, 255)
964         if None != val:
965             world_db["Things"][0]["T_ARGUMENT"] = val
966             set_command()
967
968     def set_command_and_argument_movestring(str_arg):
969         if str_arg in directions_db:
970             world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
971             set_command()
972         else:
973             print("Ignoring: Argument must be valid direction string.")
974
975     if action == "move":
976         return set_command_and_argument_movestring
977     elif args:
978         return set_command_and_argument_int
979     else:
980         return set_command
981
982
983 def command_seedrandomness(seed_string):
984     """Set rand seed to int(seed_string)."""
985     val = integer_test(seed_string, 0, 4294967295)
986     if None != val:
987         rand.seed = val
988
989
990 def command_seedmap(seed_string):
991     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
992     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
993     remake_map()
994
995
996 def command_makeworld(seed_string):
997     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
998
999     Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
1000     "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
1001     TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
1002     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1003     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1004     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1005     other. Init player's memory map. Write "NEW_WORLD" line to out file.
1006     """
1007
1008     def free_pos():
1009         i = 0
1010         while 1:
1011             err = "Space to put thing on too hard to find. Map too small?"
1012             while 1:
1013                 y = rand.next() % world_db["MAP_LENGTH"]
1014                 x = rand.next() % world_db["MAP_LENGTH"]
1015                 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
1016                     break
1017                 i += 1
1018                 if i == 65535:
1019                     raise SystemExit(err)
1020             # Replica of C code, wrongly ignores animatedness of new Thing.
1021             pos_clear = (0 == len([id for id in world_db["Things"]
1022                                    if world_db["Things"][id]["T_LIFEPOINTS"]
1023                                    if world_db["Things"][id]["T_POSY"] == y
1024                                    if world_db["Things"][id]["T_POSX"] == x]))
1025             if pos_clear:
1026                 break
1027         return (y, x)
1028
1029     val = integer_test(seed_string, 0, 4294967295)
1030     if None == val:
1031         return
1032     rand.seed = val
1033     world_db["SEED_MAP"] = val
1034     player_will_be_generated = False
1035     playertype = world_db["PLAYER_TYPE"]
1036     for ThingType in world_db["ThingTypes"]:
1037         if playertype == ThingType:
1038             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1039                 player_will_be_generated = True
1040             break
1041     if not player_will_be_generated:
1042         print("Ignoring beyond SEED_MAP: " +
1043               "No player type with start number >0 defined.")
1044         return
1045     wait_action = False
1046     for ThingAction in world_db["ThingActions"]:
1047         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1048             wait_action = True
1049     if not wait_action:
1050         print("Ignoring beyond SEED_MAP: " +
1051               "No thing action with name 'wait' defined.")
1052         return
1053     world_db["Things"] = {}
1054     remake_map()
1055     world_db["WORLD_ACTIVE"] = 1
1056     world_db["TURN"] = 1
1057     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1058         id = id_setter(-1, "Things")
1059         world_db["Things"][id] = new_Thing(playertype, free_pos())
1060     update_map_memory(world_db["Things"][0])
1061     for type in world_db["ThingTypes"]:
1062         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1063             if type != playertype:
1064                 id = id_setter(-1, "Things")
1065                 world_db["Things"][id] = new_Thing(type, free_pos())
1066     strong_write(io_db["file_out"], "NEW_WORLD\n")
1067
1068
1069 def command_maplength(maplength_string):
1070     """Redefine map length. Invalidate map, therefore lose all things on it."""
1071     val = integer_test(maplength_string, 1, 256)
1072     if None != val:
1073         world_db["MAP_LENGTH"] = val
1074         set_world_inactive()
1075         world_db["Things"] = {}
1076         libpr.set_maplength(val)
1077
1078
1079 def command_worldactive(worldactive_string):
1080     """Toggle world_db["WORLD_ACTIVE"] if possible.
1081
1082     An active world can always be set inactive. An inactive world can only be
1083     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
1084     activation, rebuild all Things' FOVs, and the player's map memory.
1085     """
1086     # In original version, map existence was also tested (unnecessarily?).
1087     val = integer_test(worldactive_string, 0, 1)
1088     if val:
1089         if 0 != world_db["WORLD_ACTIVE"]:
1090             if 0 == val:
1091                 set_world_inactive()
1092             else:
1093                 print("World already active.")
1094         elif 0 == world_db["WORLD_ACTIVE"]:
1095             wait_exists = False
1096             for ThingAction in world_db["ThingActions"]:
1097                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1098                     wait_exists = True
1099                     break
1100             player_exists = False
1101             for Thing in world_db["Things"]:
1102                 if 0 == Thing:
1103                     player_exists = True
1104                     break
1105             if wait_exists and player_exists:
1106                 for id in world_db["Things"]:
1107                     if world_db["Things"][id]["T_LIFEPOINTS"]:
1108                         build_fov_map(world_db["Things"][id])
1109                         if 0 == id:
1110                             update_map_memory(world_db["Things"][id])
1111                 world_db["WORLD_ACTIVE"] = 1
1112
1113
1114 def test_for_id_maker(object, category):
1115     """Return decorator testing for object having "id" attribute."""
1116     def decorator(f):
1117         def helper(*args):
1118             if hasattr(object, "id"):
1119                 f(*args)
1120             else:
1121                 print("Ignoring: No " + category +
1122                       " defined to manipulate yet.")
1123         return helper
1124     return decorator
1125
1126
1127 def command_tid(id_string):
1128     """Set ID of Thing to manipulate. ID unused? Create new one.
1129
1130     Default new Thing's type to the first available ThingType, others: zero.
1131     """
1132     id = id_setter(id_string, "Things", command_tid)
1133     if None != id:
1134         if world_db["ThingTypes"] == {}:
1135             print("Ignoring: No ThingType to settle new Thing in.")
1136             return
1137         type = list(world_db["ThingTypes"].keys())[0]
1138         world_db["Things"][id] = new_Thing(type)
1139
1140
1141 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1142
1143
1144 @test_Thing_id
1145 def command_tcommand(str_int):
1146     """Set T_COMMAND of selected Thing."""
1147     val = integer_test(str_int, 0, 255)
1148     if None != val:
1149         if 0 == val or val in world_db["ThingActions"]:
1150             world_db["Things"][command_tid.id]["T_COMMAND"] = val
1151         else:
1152             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1153
1154
1155 @test_Thing_id
1156 def command_ttype(str_int):
1157     """Set T_TYPE of selected Thing."""
1158     val = integer_test(str_int, 0, 255)
1159     if None != val:
1160         if val in world_db["ThingTypes"]:
1161             world_db["Things"][command_tid.id]["T_TYPE"] = val
1162         else:
1163             print("Ignoring: ThingType ID belongs to no known ThingType.")
1164
1165
1166 @test_Thing_id
1167 def command_tcarries(str_int):
1168     """Append int(str_int) to T_CARRIES of selected Thing.
1169
1170     The ID int(str_int) must not be of the selected Thing, and must belong to a
1171     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1172     """
1173     val = integer_test(str_int, 0, 255)
1174     if None != val:
1175         if val == command_tid.id:
1176             print("Ignoring: Thing cannot carry itself.")
1177         elif val in world_db["Things"] \
1178              and not world_db["Things"][val]["carried"]:
1179             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1180             world_db["Things"][val]["carried"] = True
1181         else:
1182             print("Ignoring: Thing not available for carrying.")
1183     # Note that the whole carrying structure is different from the C version:
1184     # Carried-ness is marked by a "carried" flag, not by Things containing
1185     # Things internally.
1186
1187
1188 @test_Thing_id
1189 def command_tmemthing(str_t, str_y, str_x):
1190     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1191
1192     The type must fit to an existing ThingType, and the position into the map.
1193     """
1194     type = integer_test(str_t, 0, 255)
1195     posy = integer_test(str_y, 0, 255)
1196     posx = integer_test(str_x, 0, 255)
1197     if None != type and None != posy and None != posx:
1198         if type not in world_db["ThingTypes"] \
1199            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1200             print("Ignoring: Illegal value for thing type or position.")
1201         else:
1202             memthing = (type, posy, posx)
1203             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1204
1205
1206 def setter_map(maptype):
1207     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
1208
1209     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
1210     """
1211     @test_Thing_id
1212     def helper(str_int, mapline):
1213         val = integer_test(str_int, 0, 255)
1214         if None != val:
1215             if val >= world_db["MAP_LENGTH"]:
1216                 print("Illegal value for map line number.")
1217             elif len(mapline) != world_db["MAP_LENGTH"]:
1218                 print("Map line length is unequal map width.")
1219             else:
1220                 length = world_db["MAP_LENGTH"]
1221                 map = None
1222                 if not world_db["Things"][command_tid.id][maptype]:
1223                     map = bytearray(b' ' * (length ** 2))
1224                 else:
1225                     map = world_db["Things"][command_tid.id][maptype]
1226                 map[val * length:(val * length) + length] = mapline.encode()
1227                 world_db["Things"][command_tid.id][maptype] = map
1228     return helper
1229
1230
1231 def setter_tpos(axis):
1232     """Generate setter for T_POSX or  T_POSY of selected Thing.
1233
1234     If world is active, rebuilds animate things' fovmap, player's memory map.
1235     """
1236     @test_Thing_id
1237     def helper(str_int):
1238         val = integer_test(str_int, 0, 255)
1239         if None != val:
1240             if val < world_db["MAP_LENGTH"]:
1241                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1242                 if world_db["WORLD_ACTIVE"] \
1243                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1244                     build_fov_map(world_db["Things"][command_tid.id])
1245                     if 0 == command_tid.id:
1246                         update_map_memory(world_db["Things"][command_tid.id])
1247             else:
1248                 print("Ignoring: Position is outside of map.")
1249     return helper
1250
1251
1252 def command_ttid(id_string):
1253     """Set ID of ThingType to manipulate. ID unused? Create new one.
1254
1255     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1256     """
1257     id = id_setter(id_string, "ThingTypes", command_ttid)
1258     if None != id:
1259         world_db["ThingTypes"][id] = {
1260             "TT_NAME": "(none)",
1261             "TT_CONSUMABLE": 0,
1262             "TT_LIFEPOINTS": 0,
1263             "TT_PROLIFERATE": 0,
1264             "TT_START_NUMBER": 0,
1265             "TT_SYMBOL": "?",
1266             "TT_CORPSE_ID": id
1267         }
1268
1269
1270 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1271
1272
1273 @test_ThingType_id
1274 def command_ttname(name):
1275     """Set TT_NAME of selected ThingType."""
1276     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1277
1278
1279 @test_ThingType_id
1280 def command_ttsymbol(char):
1281     """Set TT_SYMBOL of selected ThingType. """
1282     if 1 == len(char):
1283         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1284     else:
1285         print("Ignoring: Argument must be single character.")
1286
1287
1288 @test_ThingType_id
1289 def command_ttcorpseid(str_int):
1290     """Set TT_CORPSE_ID of selected ThingType."""
1291     val = integer_test(str_int, 0, 255)
1292     if None != val:
1293         if val in world_db["ThingTypes"]:
1294             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1295         else:
1296             print("Ignoring: Corpse ID belongs to no known ThignType.")
1297
1298
1299 def command_taid(id_string):
1300     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1301
1302     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1303     """
1304     id = id_setter(id_string, "ThingActions", command_taid, True)
1305     if None != id:
1306         world_db["ThingActions"][id] = {
1307             "TA_EFFORT": 1,
1308             "TA_NAME": "wait"
1309         }
1310
1311
1312 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1313
1314
1315 @test_ThingAction_id
1316 def command_taname(name):
1317     """Set TA_NAME of selected ThingAction.
1318
1319     The name must match a valid thing action function. If after the name
1320     setting no ThingAction with name "wait" remains, call set_world_inactive().
1321     """
1322     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1323        or name == "pick_up":
1324         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1325         if 1 == world_db["WORLD_ACTIVE"]:
1326             wait_defined = False
1327             for id in world_db["ThingActions"]:
1328                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1329                     wait_defined = True
1330                     break
1331             if not wait_defined:
1332                 set_world_inactive()
1333     else:
1334         print("Ignoring: Invalid action name.")
1335     # In contrast to the original,naming won't map a function to a ThingAction.
1336
1337
1338 def command_ai():
1339     """Call ai() on player Thing, then turn_over()."""
1340     ai(world_db["Things"][0])
1341     turn_over()
1342
1343
1344 """Commands database.
1345
1346 Map command start tokens to ([0]) number of expected command arguments, ([1])
1347 the command's meta-ness (i.e. is it to be written to the record file, is it to
1348 be ignored in replay mode if read from server input file), and ([2]) a function
1349 to be called on it.
1350 """
1351 commands_db = {
1352     "QUIT": (0, True, command_quit),
1353     "PING": (0, True, command_ping),
1354     "THINGS_HERE": (2, True, command_thingshere),
1355     "MAKE_WORLD": (1, False, command_makeworld),
1356     "SEED_MAP": (1, False, command_seedmap),
1357     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1358     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1359     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1360     "MAP_LENGTH": (1, False, command_maplength),
1361     "WORLD_ACTIVE": (1, False, command_worldactive),
1362     "TA_ID": (1, False, command_taid),
1363     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1364     "TA_NAME": (1, False, command_taname),
1365     "TT_ID": (1, False, command_ttid),
1366     "TT_NAME": (1, False, command_ttname),
1367     "TT_SYMBOL": (1, False, command_ttsymbol),
1368     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1369     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1370                                        0, 65535)),
1371     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1372                                          0, 255)),
1373     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1374                                         0, 255)),
1375     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1376     "T_ID": (1, False, command_tid),
1377     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1378     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1379     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1380     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1381     "T_COMMAND": (1, False, command_tcommand),
1382     "T_TYPE": (1, False, command_ttype),
1383     "T_CARRIES": (1, False, command_tcarries),
1384     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1385     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1386     "T_MEMTHING": (3, False, command_tmemthing),
1387     "T_POSY": (1, False, setter_tpos("Y")),
1388     "T_POSX": (1, False, setter_tpos("X")),
1389     "wait": (0, False, play_commander("wait")),
1390     "move": (1, False, play_commander("move")),
1391     "pick_up": (0, False, play_commander("pick_up")),
1392     "drop": (1, False, play_commander("drop", True)),
1393     "use": (1, False, play_commander("use", True)),
1394     "ai": (0, False, command_ai)
1395 }
1396
1397
1398 """World state database. With sane default values. (Randomness is in rand.)"""
1399 world_db = {
1400     "TURN": 0,
1401     "MAP_LENGTH": 64,
1402     "SEED_MAP": 0,
1403     "PLAYER_TYPE": 0,
1404     "WORLD_ACTIVE": 0,
1405     "ThingActions": {},
1406     "ThingTypes": {},
1407     "Things": {}
1408 }
1409
1410 """Mapping of direction names to internal direction chars."""
1411 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
1412                  "west": "s", "north-west": "w", "north-east": "e"}
1413
1414 """File IO database."""
1415 io_db = {
1416     "path_save": "save",
1417     "path_record": "record",
1418     "path_worldconf": "confserver/world",
1419     "path_server": "server/",
1420     "path_in": "server/in",
1421     "path_out": "server/out",
1422     "path_worldstate": "server/worldstate",
1423     "tmp_suffix": "_tmp",
1424     "kicked_by_rival": False,
1425     "worldstate_updateable": False
1426 }
1427
1428
1429 try:
1430     libpr = prep_library()
1431     rand = RandomnessIO()
1432     opts = parse_command_line_arguments()
1433     setup_server_io()
1434     if None != opts.replay:
1435         replay_game()
1436     else:
1437         play_game()
1438 except SystemExit as exit:
1439     print("ABORTING: " + exit.args[0])
1440 except:
1441     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1442     raise
1443 finally:
1444     cleanup_server_io()