home · contact · privacy
Server/py: Add random positioning of new Things on world creation.
[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     return libpr
36
37
38 def strong_write(file, string):
39     """Apply write(string), flush(), and os.fsync() to file."""
40     file.write(string)
41     file.flush()
42     os.fsync(file)
43
44
45 def setup_server_io():
46     """Fill IO files DB with proper file( path)s. Write process IO test string.
47
48     Ensure IO files directory at server/. Remove any old input file if found.
49     Set up new input file for reading, and new output file for writing. Start
50     output file with process hash line of format PID + " " + floated UNIX time
51     (io_db["teststring"]). Raise SystemExit if file is found at path of either
52     record or save file plus io_db["tmp_suffix"].
53     """
54     def detect_atomic_leftover(path, tmp_suffix):
55         path_tmp = path + tmp_suffix
56         msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
57               "aborted previous attempt to write '" + path + "'. Aborting " \
58              "until matter is resolved by removing it from its current path."
59         if os.access(path_tmp, os.F_OK):
60             raise SystemExit(msg)
61     io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
62     os.makedirs(io_db["path_server"], exist_ok=True)
63     io_db["file_out"] = open(io_db["path_out"], "w")
64     strong_write(io_db["file_out"], io_db["teststring"] + "\n")
65     if os.access(io_db["path_in"], os.F_OK):
66         os.remove(io_db["path_in"])
67     io_db["file_in"] = open(io_db["path_in"], "w")
68     io_db["file_in"].close()
69     io_db["file_in"] = open(io_db["path_in"], "r")
70     detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
71     detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
72
73
74 def cleanup_server_io():
75     """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
76     def helper(file_key, path_key):
77         if file_key in io_db:
78             io_db[file_key].close()
79             if not io_db["kicked_by_rival"] \
80                and os.access(io_db[path_key], os.F_OK):
81                 os.remove(io_db[path_key])
82     helper("file_out", "path_out")
83     helper("file_in", "path_in")
84     helper("file_worldstate", "path_worldstate")
85     if "file_record" in io_db:
86         io_db["file_record"].close()
87
88
89 def obey(command, prefix, replay=False, do_record=False):
90     """Call function from commands_db mapped to command's first token.
91
92     Tokenize command string with shlex.split(comments=True). If replay is set,
93     a non-meta command from the commands_db merely triggers obey() on the next
94     command from the records file. If not, non-meta commands set
95     io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
96     do_record is set, are recorded via record(), and save_world() is called.
97     The prefix string is inserted into the server's input message between its
98     beginning 'input ' & ':'. All activity is preceded by a server_test() call.
99     """
100     server_test()
101     print("input " + prefix + ": " + command)
102     try:
103         tokens = shlex.split(command, comments=True)
104     except ValueError as err:
105         print("Can't tokenize command string: " + str(err) + ".")
106         return
107     if len(tokens) > 0 and tokens[0] in commands_db \
108        and len(tokens) == commands_db[tokens[0]][0] + 1:
109         if commands_db[tokens[0]][1]:
110             commands_db[tokens[0]][2](*tokens[1:])
111         elif replay:
112             print("Due to replay mode, reading command as 'go on in record'.")
113             line = io_db["file_record"].readline()
114             if len(line) > 0:
115                 obey(line.rstrip(), io_db["file_record"].prefix
116                      + str(io_db["file_record"].line_n))
117                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
118             else:
119                 print("Reached end of record file.")
120         else:
121             commands_db[tokens[0]][2](*tokens[1:])
122             if do_record:
123                 record(command)
124                 save_world()
125             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
126     elif 0 != len(tokens):
127         print("Invalid command/argument, or bad number of tokens.")
128
129
130 def atomic_write(path, text, do_append=False):
131     """Atomic write of text to file at path, appended if do_append is set."""
132     path_tmp = path + io_db["tmp_suffix"]
133     mode = "w"
134     if do_append:
135         mode = "a"
136         if os.access(path, os.F_OK):
137             shutil.copyfile(path, path_tmp)
138     file = open(path_tmp, mode)
139     strong_write(file, text)
140     file.close()
141     if os.access(path, os.F_OK):
142         os.remove(path)
143     os.rename(path_tmp, path)
144
145
146 def record(command):
147     """Append command string plus newline to record file. (Atomic.)"""
148     # This misses some optimizations from the original record(), namely only
149     # finishing the atomic write with expensive flush() and fsync() every 15
150     # seconds unless explicitely forced. Implement as needed.
151     atomic_write(io_db["path_record"], command + "\n", do_append=True)
152
153
154 def save_world():
155     """Save all commands needed to reconstruct current world state."""
156     # TODO: Misses same optimizations as record() from the original record().
157
158     def quote(string):
159         string = string.replace("\u005C", '\u005C\u005C')
160         return '"' + string.replace('"', '\u005C"') + '"'
161
162     def mapsetter(key):
163         def helper(id):
164             string = ""
165             if world_db["Things"][id][key]:
166                 map = world_db["Things"][id][key]
167                 length = world_db["MAP_LENGTH"]
168                 for i in range(length):
169                     line = map[i * length:(i * length) + length].decode()
170                     string = string + key + " " + str(i) + " " + quote(line) \
171                              + "\n"
172             return string
173         return helper
174
175     def memthing(id):
176         string = ""
177         for memthing in world_db["Things"][id]["T_MEMTHING"]:
178             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
179                      str(memthing[1]) + " " + str(memthing[2]) + "\n"
180         return string
181
182     def helper(category, id_string, special_keys={}):
183         string = ""
184         for id in world_db[category]:
185             string = string + id_string + " " + str(id) + "\n"
186             for key in world_db[category][id]:
187                 if not key in special_keys:
188                     x = world_db[category][id][key]
189                     argument = quote(x) if str == type(x) else str(x)
190                     string = string + key + " " + argument + "\n"
191                 elif special_keys[key]:
192                     string = string + special_keys[key](id)
193         return string
194
195     string = ""
196     for key in world_db:
197         if dict != type(world_db[key]) and key != "MAP":
198             string = string + key + " " + str(world_db[key]) + "\n"
199     string = string + helper("ThingActions", "TA_ID")
200     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
201     for id in world_db["ThingTypes"]:
202         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
203                  str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
204     string = string + helper("Things", "T_ID",
205                              {"T_CARRIES": False, "carried": False,
206                               "T_MEMMAP": mapsetter("T_MEMMAP"),
207                               "T_MEMTHING": memthing, "fovmap": False,
208                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
209     for id in world_db["Things"]:
210         if [] != world_db["Things"][id]["T_CARRIES"]:
211             string = string + "T_ID " + str(id) + "\n"
212             for carried_id in world_db["Things"][id]["T_CARRIES"]:
213                 string = string + "T_CARRIES " + str(carried_id) + "\n"
214     string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
215              "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
216     atomic_write(io_db["path_save"], string)
217
218
219 def obey_lines_in_file(path, name, do_record=False):
220     """Call obey() on each line of path's file, use name in input prefix."""
221     file = open(path, "r")
222     line_n = 1
223     for line in file.readlines():
224         obey(line.rstrip(), name + "file line " + str(line_n),
225              do_record=do_record)
226         line_n = line_n + 1
227     file.close()
228
229
230 def parse_command_line_arguments():
231     """Return settings values read from command line arguments."""
232     parser = argparse.ArgumentParser()
233     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
234                         action='store')
235     opts, unknown = parser.parse_known_args()
236     return opts
237
238
239 def server_test():
240     """Ensure valid server out file belonging to current process.
241
242     This is done by comparing io_db["teststring"] to what's found at the start
243     of the current file at io_db["path_out"]. On failure, set
244     io_db["kicked_by_rival"] and raise SystemExit.
245     """
246     if not os.access(io_db["path_out"], os.F_OK):
247         raise SystemExit("Server output file has disappeared.")
248     file = open(io_db["path_out"], "r")
249     test = file.readline().rstrip("\n")
250     file.close()
251     if test != io_db["teststring"]:
252         io_db["kicked_by_rival"] = True
253         msg = "Server test string in server output file does not match. This" \
254               " indicates that the current server process has been " \
255               "superseded by another one."
256         raise SystemExit(msg)
257
258
259 def read_command():
260     """Return next newline-delimited command from server in file.
261
262     Keep building return string until a newline is encountered. Pause between
263     unsuccessful reads, and after too much waiting, run server_test().
264     """
265     wait_on_fail = 1
266     max_wait = 5
267     now = time.time()
268     command = ""
269     while True:
270         add = io_db["file_in"].readline()
271         if len(add) > 0:
272             command = command + add
273             if len(command) > 0 and "\n" == command[-1]:
274                 command = command[:-1]
275                 break
276         else:
277             time.sleep(wait_on_fail)
278             if now + max_wait < time.time():
279                 server_test()
280                 now = time.time()
281     return command
282
283
284 def try_worldstate_update():
285     """Write worldstate file if io_db["worldstate_updateable"] is set."""
286     if io_db["worldstate_updateable"]:
287
288         def draw_visible_Things(map, run):
289             for id in world_db["Things"]:
290                 type = world_db["Things"][id]["T_TYPE"]
291                 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
292                 alive = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]
293                 if (0 == run and not consumable and not alive) \
294                    or (1 == run and consumable and not alive) \
295                    or (2 == run and alive):
296                     y = world_db["Things"][id]["T_POSY"]
297                     x = world_db["Things"][id]["T_POSX"]
298                     fovflag = world_db["Things"][0]["fovmap"][(y * length) + x]
299                     if 'v' == chr(fovflag):
300                         c = world_db["ThingTypes"][type]["TT_SYMBOL"]
301                         map[(y * length) + x] = ord(c)
302
303         def write_map(string, map):
304             for i in range(length):
305                 line = map[i * length:(i * length) + length].decode()
306                 string = string + line + "\n"
307             return string
308
309         inventory = ""
310         if [] == world_db["Things"][0]["T_CARRIES"]:
311             inventory = "(none)\n"
312         else:
313             for id in world_db["Things"][0]["T_CARRIES"]:
314                 type_id = world_db["Things"][id]["T_TYPE"]
315                 name = world_db["ThingTypes"][type_id]["TT_NAME"]
316                 inventory = inventory + name + "\n"
317         string = str(world_db["TURN"]) + "\n" + \
318                  str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
319                  str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
320                  inventory + "%\n" + \
321                  str(world_db["Things"][0]["T_POSY"]) + "\n" + \
322                  str(world_db["Things"][0]["T_POSX"]) + "\n" + \
323                  str(world_db["MAP_LENGTH"]) + "\n"
324         length = world_db["MAP_LENGTH"]
325         fov = bytearray(b' ' * (length ** 2))
326         for pos in range(length ** 2):
327             if 'v' == chr(world_db["Things"][0]["fovmap"][pos]):
328                 fov[pos] = world_db["MAP"][pos]
329         for i in range(3):
330             draw_visible_Things(fov, i)
331         string = write_map(string, fov)
332         mem = world_db["Things"][0]["T_MEMMAP"][:]
333         for i in range(2):
334             for memthing in world_db["Things"][0]["T_MEMTHING"]:
335                 type = world_db["Things"][memthing[0]]["T_TYPE"]
336                 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
337                 if (i == 0 and not consumable) or (i == 1 and consumable):
338                     c = world_db["ThingTypes"][type]["TT_SYMBOL"]
339                     mem[(memthing[1] * length) + memthing[2]] = ord(c)
340         string = write_map(string, mem)
341         atomic_write(io_db["path_worldstate"], string)
342         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
343         io_db["worldstate_updateable"] = False
344
345
346 def replay_game():
347     """Replay game from record file.
348
349     Use opts.replay as breakpoint turn to which to replay automatically before
350     switching to manual input by non-meta commands in server input file
351     triggering further reads of record file. Ensure opts.replay is at least 1.
352     Run try_worldstate_update() before each interactive obey()/read_command().
353     """
354     if opts.replay < 1:
355         opts.replay = 1
356     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
357           " (if so late a turn is to be found).")
358     if not os.access(io_db["path_record"], os.F_OK):
359         raise SystemExit("No record file found to replay.")
360     io_db["file_record"] = open(io_db["path_record"], "r")
361     io_db["file_record"].prefix = "record file line "
362     io_db["file_record"].line_n = 1
363     while world_db["TURN"] < opts.replay:
364         line = io_db["file_record"].readline()
365         if "" == line:
366             break
367         obey(line.rstrip(), io_db["file_record"].prefix
368              + str(io_db["file_record"].line_n))
369         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
370     while True:
371         try_worldstate_update()
372         obey(read_command(), "in file", replay=True)
373
374
375 def play_game():
376     """Play game by server input file commands. Before, load save file found.
377
378     If no save file is found, a new world is generated from the commands in the
379     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
380     command and all that follow via the server input file. Run
381     try_worldstate_update() before each interactive obey()/read_command().
382     """
383     if os.access(io_db["path_save"], os.F_OK):
384         obey_lines_in_file(io_db["path_save"], "save")
385     else:
386         if not os.access(io_db["path_worldconf"], os.F_OK):
387             msg = "No world config file from which to start a new world."
388             raise SystemExit(msg)
389         obey_lines_in_file(io_db["path_worldconf"], "world config ",
390                            do_record=True)
391         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
392     while True:
393         try_worldstate_update()
394         obey(read_command(), "in file", do_record=True)
395
396
397 def remake_map():
398     # DUMMY map creator.
399     world_db["MAP"] = bytearray(b'.' * (world_db["MAP_LENGTH"] ** 2))
400
401
402 def update_map_memory(t):
403     """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
404     if not t["T_MEMMAP"]:
405         t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
406     if not t["T_MEMDEPTHMAP"]:
407         t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
408     for pos in range(world_db["MAP_LENGTH"] ** 2):
409         if "v" == chr(t["fovmap"][pos]):
410             t["T_MEMDEPTHMAP"][pos] = ord("0")
411             if " " == chr(t["T_MEMMAP"][pos]):
412                 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
413             continue
414         # TODO: Aging of MEMDEPTHMAP.
415     for memthing in t["T_MEMTHING"]:
416         y = world_db["Things"][memthing[0]]["T_POSY"]
417         x = world_db["Things"][memthing[1]]["T_POSY"]
418         if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
419             t["T_MEMTHING"].remove(memthing)
420     for id in world_db["Things"]:
421         type = world_db["Things"][id]["T_TYPE"]
422         if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
423             y = world_db["Things"][id]["T_POSY"]
424             x = world_db["Things"][id]["T_POSY"]
425             if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
426                 t["T_MEMTHING"].append((type, y, x))
427
428
429 def set_world_inactive():
430     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
431     server_test()
432     if os.access(io_db["path_worldstate"], os.F_OK):
433         os.remove(io_db["path_worldstate"])
434     world_db["WORLD_ACTIVE"] = 0
435
436
437 def integer_test(val_string, min, max):
438     """Return val_string if possible integer >= min and <= max, else None."""
439     try:
440         val = int(val_string)
441         if val < min or val > max:
442             raise ValueError
443         return val
444     except ValueError:
445         print("Ignoring: Please use integer >= " + str(min) + " and <= " +
446               str(max) + ".")
447         return None
448
449
450 def setter(category, key, min, max):
451     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
452     if category is None:
453         def f(val_string):
454             val = integer_test(val_string, min, max)
455             if None != val:
456                 world_db[key] = val
457     else:
458         if category == "Thing":
459             id_store = command_tid
460             decorator = test_Thing_id
461         elif category == "ThingType":
462             id_store = command_ttid
463             decorator = test_ThingType_id
464         elif category == "ThingAction":
465             id_store = command_taid
466             decorator = test_ThingAction_id
467
468         @decorator
469         def f(val_string):
470             val = integer_test(val_string, min, max)
471             if None != val:
472                 world_db[category + "s"][id_store.id][key] = val
473     return f
474
475
476 def build_fov_map(t):
477     """Build Thing's FOV map."""
478     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
479     # DUMMY so far. Just builds an all-visible map.
480
481
482 def actor_wait(t):
483     """Make t do nothing (but loudly, if player avatar)."""
484     if t == world_db["Things"][0]:
485         strong_write(io_db["file_out"], "LOG You wait.\n")
486
487
488 def actor_move(t):
489     pass
490
491
492 def actor_pick_up(t):
493     """Make t pick up (topmost?) Thing from ground into inventory."""
494     # Topmostness is actually not defined so far.
495     ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
496            if not world_db["Things"][id]["carried"]
497            if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
498            if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
499     if len(ids):
500         world_db["Things"][ids[0]]["carried"] = True
501         t["T_CARRIES"].append(ids[0])
502         if t == world_db["Things"][0]:
503             strong_write(io_db["file_out"], "LOG You pick up an object.\n")
504     elif t == world_db["Things"][0]:
505         err = "You try to pick up an object, but there is none."
506         strong_write(io_db["file_out"], "LOG " + err + "\n")
507
508
509 def actor_drop(t):
510     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
511     # TODO: Handle case where T_ARGUMENT matches nothing.
512     if len(t["T_CARRIES"]):
513         id = t["T_CARRIES"][t["T_ARGUMENT"]]
514         t["T_CARRIES"].remove(id)
515         world_db["Things"][id]["carried"] = False
516         if t == world_db["Things"][0]:
517             strong_write(io_db["file_out"], "LOG You drop an object.\n")
518     elif t == world_db["Things"][0]:
519         err = "You try to drop an object, but you own none."
520         strong_write(io_db["file_out"], "LOG " + err + "\n")
521
522
523 def actor_use(t):
524     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
525     # Original wrongly featured lifepoints increase through consumable!
526     # TODO: Handle case where T_ARGUMENT matches nothing.
527     if len(t["T_CARRIES"]):
528         id = t["T_CARRIES"][t["T_ARGUMENT"]]
529         type = world_db["Things"][id]["T_TYPE"]
530         if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
531             t["T_CARRIES"].remove(id)
532             del world_db["Things"][id]
533             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
534             strong_write(io_db["file_out"], "LOG You consume this object.\n")
535         else:
536             strong_write(io_db["file_out"], "LOG You try to use this object," +
537                                             "but fail.\n")
538     else:
539         strong_write(io_db["file_out"], "LOG You try to use an object, but " +
540                                         "you own none.\n")
541
542
543 def turn_over():
544     """Run game world and its inhabitants until new player input expected."""
545     id = 0
546     whilebreaker = False
547     while world_db["Things"][0]["T_LIFEPOINTS"]:
548         for id in [id for id in world_db["Things"]
549                    if world_db["Things"][id]["T_LIFEPOINTS"]]:
550             Thing = world_db["Things"][id]
551             if Thing["T_LIFEPOINTS"]:
552                 if not Thing["T_COMMAND"]:
553                     update_map_memory(Thing)
554                     if 0 == id:
555                         whilebreaker = True
556                         break
557                     # DUMMY: ai(thing)
558                     Thing["T_COMMAND"] = 1
559                 # DUMMY: try_healing
560                 Thing["T_PROGRESS"] += 1
561                 taid = [a for a in world_db["ThingActions"]
562                           if a == Thing["T_COMMAND"]][0]
563                 ThingAction = world_db["ThingActions"][taid]
564                 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
565                     eval("actor_" + ThingAction["TA_NAME"])(Thing)
566                     Thing["T_COMMAND"] = 0
567                     Thing["T_PROGRESS"] = 0
568                 # DUMMY: hunger
569             # DUMMY: thingproliferation
570         if whilebreaker:
571             break
572         world_db["TURN"] += 1
573
574
575 def new_Thing(type, pos=(0,0)):
576     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
577     thing = {
578         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
579         "T_ARGUMENT": 0,
580         "T_PROGRESS": 0,
581         "T_SATIATION": 0,
582         "T_COMMAND": 0,
583         "T_TYPE": type,
584         "T_POSY": pos[0],
585         "T_POSX": pos[1],
586         "T_CARRIES": [],
587         "carried": False,
588         "T_MEMTHING": [],
589         "T_MEMMAP": False,
590         "T_MEMDEPTHMAP": False,
591         "fovmap": False
592     }
593     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
594         build_fov_map(thing)
595     return thing
596
597
598 def id_setter(id, category, id_store=False, start_at_1=False):
599     """Set ID of object of category to manipulate ID unused? Create new one.
600
601     The ID is stored as id_store.id (if id_store is set). If the integer of the
602     input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
603     32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
604     >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
605     new object is created, otherwise the new object's ID.
606     """
607     min = 0 if start_at_1 else -32768
608     max = 255 if start_at_1 else 32767
609     if str == type(id):
610         id = integer_test(id, min, max)
611     if None != id:
612         if id in world_db[category]:
613             if id_store:
614                 id_store.id = id
615             return None
616         else:
617             if (start_at_1 and 0 == id) \
618                or ((not start_at_1) and (id < 0 or id > 255)):
619                 id = -1
620                 while 1:
621                     id = id + 1
622                     if id not in world_db[category]:
623                         break
624                 if id > 255:
625                     print("Ignoring: "
626                           "No unused ID available to add to ID list.")
627                     return None
628             if id_store:
629                 id_store.id = id
630     return id
631
632
633 def command_ping():
634     """Send PONG line to server output file."""
635     strong_write(io_db["file_out"], "PONG\n")
636
637
638 def command_quit():
639     """Abort server process."""
640     raise SystemExit("received QUIT command")
641
642
643 def command_thingshere(str_y, str_x):
644     """Write to out file list of Things known to player at coordinate y, x."""
645     def write_thing_if_here():
646         if y == world_db["Things"][id]["T_POSY"] \
647            and x == world_db["Things"][id]["T_POSX"] \
648            and not world_db["Things"][id]["carried"]:
649             type = world_db["Things"][id]["T_TYPE"]
650             name = world_db["ThingTypes"][type]["TT_NAME"]
651             strong_write(io_db["file_out"], name + "\n")
652     if world_db["WORLD_ACTIVE"]:
653         y = integer_test(str_y, 0, 255)
654         x = integer_test(str_x, 0, 255)
655         length = world_db["MAP_LENGTH"]
656         if None != y and None != x and y < length and x < length:
657             pos = (y * world_db["MAP_LENGTH"]) + x
658             strong_write(io_db["file_out"], "THINGS_HERE START\n")
659             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
660                 for id in world_db["Things"]:
661                     write_thing_if_here()
662             else:
663                 for id in world_db["Things"]["T_MEMTHING"]:
664                     write_thing_if_here()
665             strong_write(io_db["file_out"], "THINGS_HERE END\n")
666         else:
667             print("Ignoring: Invalid map coordinates.")
668     else:
669         print("Ignoring: Command only works on existing worlds.")
670
671
672 def play_commander(action, args=False):
673     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
674
675     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
676     """
677
678     def set_command():
679         id = [x for x in world_db["ThingActions"]
680                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
681         world_db["Things"][0]["T_COMMAND"] = id
682         turn_over()
683
684     def set_command_and_argument_int(str_arg):
685         val = integer_test(str_arg, 0, 255)
686         if None != val:
687             world_db["Things"][0]["T_ARGUMENT"] = val
688             set_command()
689         else:
690             print("Ignoring: Argument must be integer >= 0 <=255.")
691
692     def set_command_and_argument_movestring(str_arg):
693         dirs = {"east": "d", "south-east": "c", "south-west": "x",
694                 "west": "s", "north-west": "w", "north-east": "e"}
695         if str_arg in dirs:
696             world_db["Things"][0]["T_ARGUMENT"] = dirs[str_arg]
697             set_command()
698         else:
699             print("Ignoring: Argument must be valid direction string.")
700
701     if action == "move":
702         return set_command_and_argument_movestring
703     elif args:
704         return set_command_and_argument_int
705     else:
706         return set_command
707
708
709 def command_seedrandomness(seed_string):
710     """Set rand seed to int(seed_string)."""
711     val = integer_test(seed_string, 0, 4294967295)
712     if None != val:
713         rand.seed = val
714     else:
715         print("Ignoring: Value must be integer >= 0, <= 4294967295.")
716
717
718 def command_seedmap(seed_string):
719     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
720     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
721     remake_map()
722
723
724 def command_makeworld(seed_string):
725     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
726
727     Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
728     "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
729     TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
730     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
731     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
732     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
733     other. Init player's memory map. Write "NEW_WORLD" line to out file.
734     """
735
736     def free_pos():
737         i = 0
738         while 1:
739             err = "Space to put thing on too hard to find. Map too small?"
740             while 1:
741                 y = rand.next() % world_db["MAP_LENGTH"]
742                 x = rand.next() % world_db["MAP_LENGTH"]
743                 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
744                     break
745                 i += 1
746                 if i == 65535:
747                     raise SystemExit(err)
748             # Replica of C code, wrongly ignores animatedness of new Thing.
749             pos_clear = (0 == len([id for id in world_db["Things"]
750                                    if world_db["Things"][id]["T_LIFEPOINTS"]
751                                    if world_db["Things"][id]["T_POSY"] == y
752                                    if world_db["Things"][id]["T_POSX"] == x]))
753             if pos_clear:
754                 break
755         return (y, x)
756
757     val = integer_test(seed_string, 0, 4294967295)
758     if None == val:
759         print("Ignoring: Value must be integer >= 0, <= 4294967295.")
760         return
761     rand.seed = val
762     world_db["SEED_MAP"] = val
763     player_will_be_generated = False
764     playertype = world_db["PLAYER_TYPE"]
765     for ThingType in world_db["ThingTypes"]:
766         if playertype == ThingType:
767             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
768                 player_will_be_generated = True
769             break
770     if not player_will_be_generated:
771         print("Ignoring beyond SEED_MAP: " +
772               "No player type with start number >0 defined.")
773         return
774     wait_action = False
775     for ThingAction in world_db["ThingActions"]:
776         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
777             wait_action = True
778     if not wait_action:
779         print("Ignoring beyond SEED_MAP: " +
780               "No thing action with name 'wait' defined.")
781         return
782     world_db["Things"] = {}
783     remake_map()
784     world_db["WORLD_ACTIVE"] = 1
785     world_db["TURN"] = 1
786     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
787         id = id_setter(-1, "Things")
788         world_db["Things"][id] = new_Thing(playertype, free_pos())
789     update_map_memory(world_db["Things"][0])
790     for type in world_db["ThingTypes"]:
791         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
792             if type != playertype:
793                 id = id_setter(-1, "Things")
794                 world_db["Things"][id] = new_Thing(type, free_pos())
795     strong_write(io_db["file_out"], "NEW_WORLD\n")
796
797
798 def command_maplength(maplength_string):
799     """Redefine map length. Invalidate map, therefore lose all things on it."""
800     set_world_inactive()
801     world_db["Things"] = {}
802     setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
803
804
805 def command_worldactive(worldactive_string):
806     """Toggle world_db["WORLD_ACTIVE"] if possible.
807
808     An active world can always be set inactive. An inactive world can only be
809     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
810     activation, rebuild all Things' FOVs, and the player's map memory.
811     """
812     # In original version, map existence was also tested (unnecessarily?).
813     val = integer_test(worldactive_string, 0, 1)
814     if val:
815         if 0 != world_db["WORLD_ACTIVE"]:
816             if 0 == val:
817                 set_world_inactive()
818             else:
819                 print("World already active.")
820         elif 0 == world_db["WORLD_ACTIVE"]:
821             wait_exists = False
822             for ThingAction in world_db["ThingActions"]:
823                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
824                     wait_exists = True
825                     break
826             player_exists = False
827             for Thing in world_db["Things"]:
828                 if 0 == Thing:
829                     player_exists = True
830                     break
831             if wait_exists and player_exists:
832                 for id in world_db["Things"]:
833                     if world_db["Things"][id]["T_LIFEPOINTS"]:
834                         build_fov_map(world_db["Things"][id])
835                         if 0 == id:
836                             update_map_memory(world_db["Things"][id])
837                 world_db["WORLD_ACTIVE"] = 1
838
839
840 def test_for_id_maker(object, category):
841     """Return decorator testing for object having "id" attribute."""
842     def decorator(f):
843         def helper(*args):
844             if hasattr(object, "id"):
845                 f(*args)
846             else:
847                 print("Ignoring: No " + category +
848                       " defined to manipulate yet.")
849         return helper
850     return decorator
851
852
853 def command_tid(id_string):
854     """Set ID of Thing to manipulate. ID unused? Create new one.
855
856     Default new Thing's type to the first available ThingType, others: zero.
857     """
858     id = id_setter(id_string, "Things", command_tid)
859     if None != id:
860         if world_db["ThingTypes"] == {}:
861             print("Ignoring: No ThingType to settle new Thing in.")
862             return
863         type = list(world_db["ThingTypes"].keys())[0]
864         world_db["Things"][id] = new_Thing(type)
865
866
867 test_Thing_id = test_for_id_maker(command_tid, "Thing")
868
869
870 @test_Thing_id
871 def command_tcommand(str_int):
872     """Set T_COMMAND of selected Thing."""
873     val = integer_test(str_int, 0, 255)
874     if None != val:
875         if 0 == val or val in world_db["ThingActions"]:
876             world_db["Things"][command_tid.id]["T_COMMAND"] = val
877         else:
878             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
879
880
881 @test_Thing_id
882 def command_ttype(str_int):
883     """Set T_TYPE of selected Thing."""
884     val = integer_test(str_int, 0, 255)
885     if None != val:
886         if val in world_db["ThingTypes"]:
887             world_db["Things"][command_tid.id]["T_TYPE"] = val
888         else:
889             print("Ignoring: ThingType ID belongs to no known ThingType.")
890
891
892 @test_Thing_id
893 def command_tcarries(str_int):
894     """Append int(str_int) to T_CARRIES of selected Thing.
895
896     The ID int(str_int) must not be of the selected Thing, and must belong to a
897     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
898     """
899     val = integer_test(str_int, 0, 255)
900     if None != val:
901         if val == command_tid.id:
902             print("Ignoring: Thing cannot carry itself.")
903         elif val in world_db["Things"] \
904              and not world_db["Things"][val]["carried"]:
905             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
906             world_db["Things"][val]["carried"] = True
907         else:
908             print("Ignoring: Thing not available for carrying.")
909     # Note that the whole carrying structure is different from the C version:
910     # Carried-ness is marked by a "carried" flag, not by Things containing
911     # Things internally.
912
913
914 @test_Thing_id
915 def command_tmemthing(str_t, str_y, str_x):
916     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
917
918     The type must fit to an existing ThingType, and the position into the map.
919     """
920     type = integer_test(str_t, 0, 255)
921     posy = integer_test(str_y, 0, 255)
922     posx = integer_test(str_x, 0, 255)
923     if None != type and None != posy and None != posx:
924         if type not in world_db["ThingTypes"] \
925            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
926             print("Ignoring: Illegal value for thing type or position.")
927         else:
928             memthing = (type, posy, posx)
929             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
930
931
932 def setter_map(maptype):
933     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
934
935     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
936     """
937     @test_Thing_id
938     def helper(str_int, mapline):
939         val = integer_test(str_int, 0, 255)
940         if None != val:
941             if val >= world_db["MAP_LENGTH"]:
942                 print("Illegal value for map line number.")
943             elif len(mapline) != world_db["MAP_LENGTH"]:
944                 print("Map line length is unequal map width.")
945             else:
946                 length = world_db["MAP_LENGTH"]
947                 map = None
948                 if not world_db["Things"][command_tid.id][maptype]:
949                     map = bytearray(b' ' * (length ** 2))
950                 else:
951                     map = world_db["Things"][command_tid.id][maptype]
952                 map[val * length:(val * length) + length] = mapline.encode()
953                 world_db["Things"][command_tid.id][maptype] = map
954     return helper
955
956
957 def setter_tpos(axis):
958     """Generate setter for T_POSX or  T_POSY of selected Thing.
959
960     If world is active, rebuilds animate things' fovmap, player's memory map.
961     """
962     @test_Thing_id
963     def helper(str_int):
964         val = integer_test(str_int, 0, 255)
965         if None != val:
966             if val < world_db["MAP_LENGTH"]:
967                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
968                 if world_db["WORLD_ACTIVE"] \
969                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
970                     build_fov_map(world_db["Things"][command_tid.id])
971                     if 0 == command_tid.id:
972                         update_map_memory(world_db["Things"][command_tid.id])
973             else:
974                 print("Ignoring: Position is outside of map.")
975     return helper
976
977
978 def command_ttid(id_string):
979     """Set ID of ThingType to manipulate. ID unused? Create new one.
980
981     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
982     """
983     id = id_setter(id_string, "ThingTypes", command_ttid)
984     if None != id:
985         world_db["ThingTypes"][id] = {
986             "TT_NAME": "(none)",
987             "TT_CONSUMABLE": 0,
988             "TT_LIFEPOINTS": 0,
989             "TT_PROLIFERATE": 0,
990             "TT_START_NUMBER": 0,
991             "TT_SYMBOL": "?",
992             "TT_CORPSE_ID": id
993         }
994
995
996 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
997
998
999 @test_ThingType_id
1000 def command_ttname(name):
1001     """Set TT_NAME of selected ThingType."""
1002     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1003
1004
1005 @test_ThingType_id
1006 def command_ttsymbol(char):
1007     """Set TT_SYMBOL of selected ThingType. """
1008     if 1 == len(char):
1009         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1010     else:
1011         print("Ignoring: Argument must be single character.")
1012
1013
1014 @test_ThingType_id
1015 def command_ttcorpseid(str_int):
1016     """Set TT_CORPSE_ID of selected ThingType."""
1017     val = integer_test(str_int, 0, 255)
1018     if None != val:
1019         if val in world_db["ThingTypes"]:
1020             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1021         else:
1022             print("Ignoring: Corpse ID belongs to no known ThignType.")
1023
1024
1025 def command_taid(id_string):
1026     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1027
1028     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1029     """
1030     id = id_setter(id_string, "ThingActions", command_taid, True)
1031     if None != id:
1032         world_db["ThingActions"][id] = {
1033             "TA_EFFORT": 1,
1034             "TA_NAME": "wait"
1035         }
1036
1037
1038 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1039
1040
1041 @test_ThingAction_id
1042 def command_taname(name):
1043     """Set TA_NAME of selected ThingAction.
1044
1045     The name must match a valid thing action function. If after the name
1046     setting no ThingAction with name "wait" remains, call set_world_inactive().
1047     """
1048     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1049        or name == "pick_up":
1050         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1051         if 1 == world_db["WORLD_ACTIVE"]:
1052             wait_defined = False
1053             for id in world_db["ThingActions"]:
1054                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1055                     wait_defined = True
1056                     break
1057             if not wait_defined:
1058                 set_world_inactive()
1059     else:
1060         print("Ignoring: Invalid action name.")
1061     # In contrast to the original,naming won't map a function to a ThingAction.
1062
1063
1064 """Commands database.
1065
1066 Map command start tokens to ([0]) number of expected command arguments, ([1])
1067 the command's meta-ness (i.e. is it to be written to the record file, is it to
1068 be ignored in replay mode if read from server input file), and ([2]) a function
1069 to be called on it.
1070 """
1071 commands_db = {
1072     "QUIT": (0, True, command_quit),
1073     "PING": (0, True, command_ping),
1074     "THINGS_HERE": (2, True, command_thingshere),
1075     "MAKE_WORLD": (1, False, command_makeworld),
1076     "SEED_MAP": (1, False, command_seedmap),
1077     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1078     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1079     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1080     "MAP_LENGTH": (1, False, command_maplength),
1081     "WORLD_ACTIVE": (1, False, command_worldactive),
1082     "TA_ID": (1, False, command_taid),
1083     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1084     "TA_NAME": (1, False, command_taname),
1085     "TT_ID": (1, False, command_ttid),
1086     "TT_NAME": (1, False, command_ttname),
1087     "TT_SYMBOL": (1, False, command_ttsymbol),
1088     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1089     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1090                                        0, 65535)),
1091     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1092                                          0, 255)),
1093     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1094                                         0, 255)),
1095     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1096     "T_ID": (1, False, command_tid),
1097     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1098     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1099     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1100     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1101     "T_COMMAND": (1, False, command_tcommand),
1102     "T_TYPE": (1, False, command_ttype),
1103     "T_CARRIES": (1, False, command_tcarries),
1104     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1105     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1106     "T_MEMTHING": (3, False, command_tmemthing),
1107     "T_POSY": (1, False, setter_tpos("Y")),
1108     "T_POSX": (1, False, setter_tpos("X")),
1109     "wait": (0, False, play_commander("wait")),
1110     "move": (1, False, play_commander("move")),
1111     "pick_up": (0, False, play_commander("pick_up")),
1112     "drop": (1, False, play_commander("drop", True)),
1113     "use": (1, False, play_commander("use", True)),
1114 }
1115
1116
1117 """World state database. With sane default values. (Randomness is in rand.)"""
1118 world_db = {
1119     "TURN": 0,
1120     "SEED_MAP": 0,
1121     "PLAYER_TYPE": 0,
1122     "MAP_LENGTH": 64,
1123     "WORLD_ACTIVE": 0,
1124     "ThingActions": {},
1125     "ThingTypes": {},
1126     "Things": {}
1127 }
1128
1129
1130 """File IO database."""
1131 io_db = {
1132     "path_save": "save",
1133     "path_record": "record",
1134     "path_worldconf": "confserver/world",
1135     "path_server": "server/",
1136     "path_in": "server/in",
1137     "path_out": "server/out",
1138     "path_worldstate": "server/worldstate",
1139     "tmp_suffix": "_tmp",
1140     "kicked_by_rival": False,
1141     "worldstate_updateable": False
1142 }
1143
1144
1145 try:
1146     libpr = prep_library()
1147     rand = RandomnessIO()
1148     opts = parse_command_line_arguments()
1149     setup_server_io()
1150     if None != opts.replay:
1151         replay_game()
1152     else:
1153         play_game()
1154 except SystemExit as exit:
1155     print("ABORTING: " + exit.args[0])
1156 except:
1157     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1158     raise
1159 finally:
1160     cleanup_server_io()