home · contact · privacy
Server/py: Add randomness infrastructure, libplomrogue.so ctypes import.
[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):
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": 0,
585         "T_POSX": 0,
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         # TODO: call turn_over()
684
685     def set_command_and_argument_int(str_arg):
686         val = integer_test(str_arg, 0, 255)
687         if None != val:
688             world_db["Things"][0]["T_ARGUMENT"] = val
689             set_command()
690         else:
691             print("Ignoring: Argument must be integer >= 0 <=255.")
692
693     def set_command_and_argument_movestring(str_arg):
694         dirs = {"east": "d", "south-east": "c", "south-west": "x",
695                 "west": "s", "north-west": "w", "north-east": "e"}
696         if str_arg in dirs:
697             world_db["Things"][0]["T_ARGUMENT"] = dirs[str_arg]
698             set_command()
699         else:
700             print("Ignoring: Argument must be valid direction string.")
701
702     if action == "move":
703         return set_command_and_argument_movestring
704     elif args:
705         return set_command_and_argument_int
706     else:
707         return set_command
708
709
710 def command_seedrandomness(seed_string):
711     """Set rand seed to int(seed_string)."""
712     val = integer_test(seed_string, 0, 4294967295)
713     if None != val:
714         rand.seed = val
715     else:
716         print("Ignoring: Value must be integer >= 0, <= 4294967295.")
717
718
719 def command_seedmap(seed_string):
720     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
721     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
722     remake_map()
723
724
725 def command_makeworld(seed_string):
726     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
727
728     Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
729     "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
730     TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
731     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
732     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
733     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
734     other. Init player's memory map. Write "NEW_WORLD" line to out file.
735     """
736     val = integer_test(seed_string, 0, 4294967295)
737     if None == val:
738         print("Ignoring: Value must be integer >= 0, <= 4294967295.")
739         return
740     rand.seed = val
741     world_db["SEED_MAP"] = val
742     player_will_be_generated = False
743     playertype = world_db["PLAYER_TYPE"]
744     for ThingType in world_db["ThingTypes"]:
745         if playertype == ThingType:
746             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
747                 player_will_be_generated = True
748             break
749     if not player_will_be_generated:
750         print("Ignoring beyond SEED_MAP: " +
751               "No player type with start number >0 defined.")
752         return
753     wait_action = False
754     for ThingAction in world_db["ThingActions"]:
755         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
756             wait_action = True
757     if not wait_action:
758         print("Ignoring beyond SEED_MAP: " +
759               "No thing action with name 'wait' defined.")
760         return
761     world_db["Things"] = {}
762     remake_map()
763     world_db["WORLD_ACTIVE"] = 1
764     world_db["TURN"] = 1
765     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
766         id = id_setter(-1, "Things")
767         world_db["Things"][id] = new_Thing(playertype)
768     # TODO: Positioning.
769     update_map_memory(world_db["Things"][0])
770     for type in world_db["ThingTypes"]:
771         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
772             if type != playertype:
773                 id = id_setter(-1, "Things")
774                 world_db["Things"][id] = new_Thing(type)
775     # TODO: Positioning.
776     strong_write(io_db["file_out"], "NEW_WORLD\n")
777
778
779 def command_maplength(maplength_string):
780     """Redefine map length. Invalidate map, therefore lose all things on it."""
781     set_world_inactive()
782     world_db["Things"] = {}
783     setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
784
785
786 def command_worldactive(worldactive_string):
787     """Toggle world_db["WORLD_ACTIVE"] if possible.
788
789     An active world can always be set inactive. An inactive world can only be
790     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
791     activation, rebuild all Things' FOVs, and the player's map memory.
792     """
793     # In original version, map existence was also tested (unnecessarily?).
794     val = integer_test(worldactive_string, 0, 1)
795     if val:
796         if 0 != world_db["WORLD_ACTIVE"]:
797             if 0 == val:
798                 set_world_inactive()
799             else:
800                 print("World already active.")
801         elif 0 == world_db["WORLD_ACTIVE"]:
802             wait_exists = False
803             for ThingAction in world_db["ThingActions"]:
804                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
805                     wait_exists = True
806                     break
807             player_exists = False
808             for Thing in world_db["Things"]:
809                 if 0 == Thing:
810                     player_exists = True
811                     break
812             if wait_exists and player_exists:
813                 for id in world_db["Things"]:
814                     if world_db["Things"][id]["T_LIFEPOINTS"]:
815                         build_fov_map(world_db["Things"][id])
816                         if 0 == id:
817                             update_map_memory(world_db["Things"][id])
818                 world_db["WORLD_ACTIVE"] = 1
819
820
821 def test_for_id_maker(object, category):
822     """Return decorator testing for object having "id" attribute."""
823     def decorator(f):
824         def helper(*args):
825             if hasattr(object, "id"):
826                 f(*args)
827             else:
828                 print("Ignoring: No " + category +
829                       " defined to manipulate yet.")
830         return helper
831     return decorator
832
833
834 def command_tid(id_string):
835     """Set ID of Thing to manipulate. ID unused? Create new one.
836
837     Default new Thing's type to the first available ThingType, others: zero.
838     """
839     id = id_setter(id_string, "Things", command_tid)
840     if None != id:
841         if world_db["ThingTypes"] == {}:
842             print("Ignoring: No ThingType to settle new Thing in.")
843             return
844         type = list(world_db["ThingTypes"].keys())[0]
845         world_db["Things"][id] = new_Thing(type)
846
847
848 test_Thing_id = test_for_id_maker(command_tid, "Thing")
849
850
851 @test_Thing_id
852 def command_tcommand(str_int):
853     """Set T_COMMAND of selected Thing."""
854     val = integer_test(str_int, 0, 255)
855     if None != val:
856         if 0 == val or val in world_db["ThingActions"]:
857             world_db["Things"][command_tid.id]["T_COMMAND"] = val
858         else:
859             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
860
861
862 @test_Thing_id
863 def command_ttype(str_int):
864     """Set T_TYPE of selected Thing."""
865     val = integer_test(str_int, 0, 255)
866     if None != val:
867         if val in world_db["ThingTypes"]:
868             world_db["Things"][command_tid.id]["T_TYPE"] = val
869         else:
870             print("Ignoring: ThingType ID belongs to no known ThingType.")
871
872
873 @test_Thing_id
874 def command_tcarries(str_int):
875     """Append int(str_int) to T_CARRIES of selected Thing.
876
877     The ID int(str_int) must not be of the selected Thing, and must belong to a
878     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
879     """
880     val = integer_test(str_int, 0, 255)
881     if None != val:
882         if val == command_tid.id:
883             print("Ignoring: Thing cannot carry itself.")
884         elif val in world_db["Things"] \
885              and not world_db["Things"][val]["carried"]:
886             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
887             world_db["Things"][val]["carried"] = True
888         else:
889             print("Ignoring: Thing not available for carrying.")
890     # Note that the whole carrying structure is different from the C version:
891     # Carried-ness is marked by a "carried" flag, not by Things containing
892     # Things internally.
893
894
895 @test_Thing_id
896 def command_tmemthing(str_t, str_y, str_x):
897     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
898
899     The type must fit to an existing ThingType, and the position into the map.
900     """
901     type = integer_test(str_t, 0, 255)
902     posy = integer_test(str_y, 0, 255)
903     posx = integer_test(str_x, 0, 255)
904     if None != type and None != posy and None != posx:
905         if type not in world_db["ThingTypes"] \
906            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
907             print("Ignoring: Illegal value for thing type or position.")
908         else:
909             memthing = (type, posy, posx)
910             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
911
912
913 def setter_map(maptype):
914     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
915
916     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
917     """
918     @test_Thing_id
919     def helper(str_int, mapline):
920         val = integer_test(str_int, 0, 255)
921         if None != val:
922             if val >= world_db["MAP_LENGTH"]:
923                 print("Illegal value for map line number.")
924             elif len(mapline) != world_db["MAP_LENGTH"]:
925                 print("Map line length is unequal map width.")
926             else:
927                 length = world_db["MAP_LENGTH"]
928                 map = None
929                 if not world_db["Things"][command_tid.id][maptype]:
930                     map = bytearray(b' ' * (length ** 2))
931                 else:
932                     map = world_db["Things"][command_tid.id][maptype]
933                 map[val * length:(val * length) + length] = mapline.encode()
934                 world_db["Things"][command_tid.id][maptype] = map
935     return helper
936
937
938 def setter_tpos(axis):
939     """Generate setter for T_POSX or  T_POSY of selected Thing.
940
941     If world is active, rebuilds animate things' fovmap, player's memory map.
942     """
943     @test_Thing_id
944     def helper(str_int):
945         val = integer_test(str_int, 0, 255)
946         if None != val:
947             if val < world_db["MAP_LENGTH"]:
948                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
949                 if world_db["WORLD_ACTIVE"] \
950                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
951                     build_fov_map(world_db["Things"][command_tid.id])
952                     if 0 == command_tid.id:
953                         update_map_memory(world_db["Things"][command_tid.id])
954             else:
955                 print("Ignoring: Position is outside of map.")
956     return helper
957
958
959 def command_ttid(id_string):
960     """Set ID of ThingType to manipulate. ID unused? Create new one.
961
962     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
963     """
964     id = id_setter(id_string, "ThingTypes", command_ttid)
965     if None != id:
966         world_db["ThingTypes"][id] = {
967             "TT_NAME": "(none)",
968             "TT_CONSUMABLE": 0,
969             "TT_LIFEPOINTS": 0,
970             "TT_PROLIFERATE": 0,
971             "TT_START_NUMBER": 0,
972             "TT_SYMBOL": "?",
973             "TT_CORPSE_ID": id
974         }
975
976
977 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
978
979
980 @test_ThingType_id
981 def command_ttname(name):
982     """Set TT_NAME of selected ThingType."""
983     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
984
985
986 @test_ThingType_id
987 def command_ttsymbol(char):
988     """Set TT_SYMBOL of selected ThingType. """
989     if 1 == len(char):
990         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
991     else:
992         print("Ignoring: Argument must be single character.")
993
994
995 @test_ThingType_id
996 def command_ttcorpseid(str_int):
997     """Set TT_CORPSE_ID of selected ThingType."""
998     val = integer_test(str_int, 0, 255)
999     if None != val:
1000         if val in world_db["ThingTypes"]:
1001             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1002         else:
1003             print("Ignoring: Corpse ID belongs to no known ThignType.")
1004
1005
1006 def command_taid(id_string):
1007     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1008
1009     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1010     """
1011     id = id_setter(id_string, "ThingActions", command_taid, True)
1012     if None != id:
1013         world_db["ThingActions"][id] = {
1014             "TA_EFFORT": 1,
1015             "TA_NAME": "wait"
1016         }
1017
1018
1019 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1020
1021
1022 @test_ThingAction_id
1023 def command_taname(name):
1024     """Set TA_NAME of selected ThingAction.
1025
1026     The name must match a valid thing action function. If after the name
1027     setting no ThingAction with name "wait" remains, call set_world_inactive().
1028     """
1029     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1030        or name == "pick_up":
1031         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1032         if 1 == world_db["WORLD_ACTIVE"]:
1033             wait_defined = False
1034             for id in world_db["ThingActions"]:
1035                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1036                     wait_defined = True
1037                     break
1038             if not wait_defined:
1039                 set_world_inactive()
1040     else:
1041         print("Ignoring: Invalid action name.")
1042     # In contrast to the original,naming won't map a function to a ThingAction.
1043
1044
1045 """Commands database.
1046
1047 Map command start tokens to ([0]) number of expected command arguments, ([1])
1048 the command's meta-ness (i.e. is it to be written to the record file, is it to
1049 be ignored in replay mode if read from server input file), and ([2]) a function
1050 to be called on it.
1051 """
1052 commands_db = {
1053     "QUIT": (0, True, command_quit),
1054     "PING": (0, True, command_ping),
1055     "THINGS_HERE": (2, True, command_thingshere),
1056     "MAKE_WORLD": (1, False, command_makeworld),
1057     "SEED_MAP": (1, False, command_seedmap),
1058     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1059     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1060     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1061     "MAP_LENGTH": (1, False, command_maplength),
1062     "WORLD_ACTIVE": (1, False, command_worldactive),
1063     "TA_ID": (1, False, command_taid),
1064     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1065     "TA_NAME": (1, False, command_taname),
1066     "TT_ID": (1, False, command_ttid),
1067     "TT_NAME": (1, False, command_ttname),
1068     "TT_SYMBOL": (1, False, command_ttsymbol),
1069     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1070     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1071                                        0, 65535)),
1072     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1073                                          0, 255)),
1074     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1075                                         0, 255)),
1076     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1077     "T_ID": (1, False, command_tid),
1078     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1079     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1080     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1081     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1082     "T_COMMAND": (1, False, command_tcommand),
1083     "T_TYPE": (1, False, command_ttype),
1084     "T_CARRIES": (1, False, command_tcarries),
1085     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1086     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1087     "T_MEMTHING": (3, False, command_tmemthing),
1088     "T_POSY": (1, False, setter_tpos("Y")),
1089     "T_POSX": (1, False, setter_tpos("X")),
1090     "wait": (0, False, play_commander("wait")),
1091     "move": (1, False, play_commander("move")),
1092     "pick_up": (0, False, play_commander("pick_up")),
1093     "drop": (1, False, play_commander("drop", True)),
1094     "use": (1, False, play_commander("use", True)),
1095 }
1096
1097
1098 """World state database. With sane default values. (Randomness is in rand.)"""
1099 world_db = {
1100     "TURN": 0,
1101     "SEED_MAP": 0,
1102     "PLAYER_TYPE": 0,
1103     "MAP_LENGTH": 64,
1104     "WORLD_ACTIVE": 0,
1105     "ThingActions": {},
1106     "ThingTypes": {},
1107     "Things": {}
1108 }
1109
1110
1111 """File IO database."""
1112 io_db = {
1113     "path_save": "save",
1114     "path_record": "record",
1115     "path_worldconf": "confserver/world",
1116     "path_server": "server/",
1117     "path_in": "server/in",
1118     "path_out": "server/out",
1119     "path_worldstate": "server/worldstate",
1120     "tmp_suffix": "_tmp",
1121     "kicked_by_rival": False,
1122     "worldstate_updateable": False
1123 }
1124
1125
1126 try:
1127     libpr = prep_library()
1128     rand = RandomnessIO()
1129     opts = parse_command_line_arguments()
1130     setup_server_io()
1131     if None != opts.replay:
1132         replay_game()
1133     else:
1134         play_game()
1135 except SystemExit as exit:
1136     print("ABORTING: " + exit.args[0])
1137 except:
1138     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1139     raise
1140 finally:
1141     cleanup_server_io()