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