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