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