home · contact · privacy
Server/py: Add skeleton for turn_over().
[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 turn_over():
453     """Run game world and its inhabitants until new player input expected."""
454     id = 0
455     print("turning over")
456     whilebreaker = False
457     while world_db["Things"][0]["T_LIFEPOINTS"]:
458         for id in [id for id in world_db["Things"]
459                       if not world_db["Things"][id]["carried"]
460                       if world_db["Things"][id]["T_LIFEPOINTS"]]:
461             print(str(id))
462             Thing = world_db["Things"][id]
463             if Thing["T_LIFEPOINTS"]:
464                 print("  evaluating thing")
465                 if not Thing["T_COMMAND"]:
466                     print("    thing needs new command")
467                     update_map_memory(Thing)
468                     if 0 == id:
469                         whilebreaker = True
470                         break
471                     # DUMMY: ai(thing)
472                     print("    run AI")
473                     Thing["T_COMMAND"] = 1
474                 # DUMMY: try_healing
475                 Thing["T_PROGRESS"] += 1
476                 taid = [a for a in world_db["ThingActions"]
477                           if a == Thing["T_COMMAND"]][0]
478                 ThingAction = world_db["ThingActions"][taid]
479                 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
480                     print("  running thing action")
481                     # run_thing_action(action["TA_NAME"])
482                     Thing["T_COMMAND"] = 0
483                     Thing["T_PROGRESS"] = 0
484                 # DUMMY: hunger
485             # DUMMY: thingproliferation
486         if whilebreaker:
487             break
488         print("  turn finished")
489         world_db["TURN"] += 1
490         print("  new turn " + str(world_db["TURN"]))
491
492
493 def new_Thing(type):
494     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
495     thing = {
496         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
497         "T_ARGUMENT": 0,
498         "T_PROGRESS": 0,
499         "T_SATIATION": 0,
500         "T_COMMAND": 0,
501         "T_TYPE": type,
502         "T_POSY": 0,
503         "T_POSX": 0,
504         "T_CARRIES": [],
505         "carried": False,
506         "T_MEMTHING": [],
507         "T_MEMMAP": False,
508         "T_MEMDEPTHMAP": False,
509         "fovmap": False
510     }
511     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
512         build_fov_map(thing)
513     return thing
514
515
516 def id_setter(id, category, id_store=False, start_at_1=False):
517     """Set ID of object of category to manipulate ID unused? Create new one.
518
519     The ID is stored as id_store.id (if id_store is set). If the integer of the
520     input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
521     32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
522     >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
523     new object is created, otherwise the new object's ID.
524     """
525     min = 0 if start_at_1 else -32768
526     max = 255 if start_at_1 else 32767
527     if str == type(id):
528         id = integer_test(id, min, max)
529     if None != id:
530         if id in world_db[category]:
531             if id_store:
532                 id_store.id = id
533             return None
534         else:
535             if (start_at_1 and 0 == id) \
536                or ((not start_at_1) and (id < 0 or id > 255)):
537                 id = -1
538                 while 1:
539                     id = id + 1
540                     if id not in world_db[category]:
541                         break
542                 if id > 255:
543                     print("Ignoring: "
544                           "No unused ID available to add to ID list.")
545                     return None
546             if id_store:
547                 id_store.id = id
548     return id
549
550
551 def command_ping():
552     """Send PONG line to server output file."""
553     strong_write(io_db["file_out"], "PONG\n")
554
555
556 def command_quit():
557     """Abort server process."""
558     raise SystemExit("received QUIT command")
559
560
561 def command_thingshere(str_y, str_x):
562     """Write to out file list of Things known to player at coordinate y, x."""
563     def write_thing_if_here():
564         if y == world_db["Things"][id]["T_POSY"] \
565            and x == world_db["Things"][id]["T_POSX"]:
566             type = world_db["Things"][id]["T_TYPE"]
567             name = world_db["ThingTypes"][type]["TT_NAME"]
568             strong_write(io_db["file_out"], name + "\n")
569     if world_db["WORLD_ACTIVE"]:
570         y = integer_test(str_y, 0, 255)
571         x = integer_test(str_x, 0, 255)
572         length = world_db["MAP_LENGTH"]
573         if None != y and None != x and y < length and x < length:
574             pos = (y * world_db["MAP_LENGTH"]) + x
575             strong_write(io_db["file_out"], "THINGS_HERE START\n")
576             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
577                 for id in world_db["Things"]:
578                     write_thing_if_here()
579             else:
580                 for id in world_db["Things"]["T_MEMTHING"]:
581                     write_thing_if_here()
582             strong_write(io_db["file_out"], "THINGS_HERE END\n")
583         else:
584             print("Ignoring: Invalid map coordinates.")
585     else:
586         print("Ignoring: Command only works on existing worlds.")
587
588
589 def play_commander(action, args=False):
590     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
591
592     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
593     """
594
595     def set_command():
596         id = [x for x in world_db["ThingActions"]
597                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
598         world_db["Things"][0]["T_COMMAND"] = id
599         turn_over()
600         # TODO: call turn_over()
601
602     def set_command_and_argument_int(str_arg):
603         val = integer_test(str_arg, 0, 255)
604         if None != val:
605             world_db["Things"][0]["T_ARGUMENT"] = val
606             set_command()
607         else:
608             print("Ignoring: Argument must be integer >= 0 <=255.")
609
610     def set_command_and_argument_movestring(str_arg):
611         dirs = {"east": "d", "south-east": "c", "south-west": "x",
612                 "west": "s", "north-west": "w", "north-east": "e"}
613         if str_arg in dirs:
614             world_db["Things"][0]["T_ARGUMENT"] = dirs[str_arg]
615             set_command()
616         else:
617             print("Ignoring: Argument must be valid direction string.")
618
619     if action == "move":
620         return set_command_and_argument_movestring
621     elif args:
622         return set_command_and_argument_int
623     else:
624         return set_command
625
626
627 def command_seedmap(seed_string):
628     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
629     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
630     remake_map()
631
632
633 def command_makeworld(seed_string):
634     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
635
636     Make seed world_db["SEED_RANDOMNESS"] and world_db["SEED_MAP"]. Do more
637     only with a "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType
638     of TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
639     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
640     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
641     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
642     other. Init player's memory map. Write "NEW_WORLD" line to out file.
643     """
644     setter(None, "SEED_RANDOMNESS", 0, 4294967295)(seed_string)
645     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
646     player_will_be_generated = False
647     playertype = world_db["PLAYER_TYPE"]
648     for ThingType in world_db["ThingTypes"]:
649         if playertype == ThingType:
650             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
651                 player_will_be_generated = True
652             break
653     if not player_will_be_generated:
654         print("Ignoring beyond SEED_MAP: " +
655               "No player type with start number >0 defined.")
656         return
657     wait_action = False
658     for ThingAction in world_db["ThingActions"]:
659         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
660             wait_action = True
661     if not wait_action:
662         print("Ignoring beyond SEED_MAP: " +
663               "No thing action with name 'wait' defined.")
664         return
665     world_db["Things"] = {}
666     remake_map()
667     world_db["WORLD_ACTIVE"] = 1
668     world_db["TURN"] = 1
669     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
670         id = id_setter(-1, "Things")
671         world_db["Things"][id] = new_Thing(playertype)
672     # TODO: Positioning.
673     update_map_memory(world_db["Things"][0])
674     for type in world_db["ThingTypes"]:
675         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
676             if type != playertype:
677                 id = id_setter(-1, "Things")
678                 world_db["Things"][id] = new_Thing(type)
679     # TODO: Positioning.
680     strong_write(io_db["file_out"], "NEW_WORLD\n")
681
682
683 def command_maplength(maplength_string):
684     """Redefine map length. Invalidate map, therefore lose all things on it."""
685     set_world_inactive()
686     world_db["Things"] = {}
687     setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
688
689
690 def command_worldactive(worldactive_string):
691     """Toggle world_db["WORLD_ACTIVE"] if possible.
692
693     An active world can always be set inactive. An inactive world can only be
694     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
695     activation, rebuild all Things' FOVs, and the player's map memory.
696     """
697     # In original version, map existence was also tested (unnecessarily?).
698     val = integer_test(worldactive_string, 0, 1)
699     if val:
700         if 0 != world_db["WORLD_ACTIVE"]:
701             if 0 == val:
702                 set_world_inactive()
703             else:
704                 print("World already active.")
705         elif 0 == world_db["WORLD_ACTIVE"]:
706             wait_exists = False
707             for ThingAction in world_db["ThingActions"]:
708                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
709                     wait_exists = True
710                     break
711             player_exists = False
712             for Thing in world_db["Things"]:
713                 if 0 == Thing:
714                     player_exists = True
715                     break
716             if wait_exists and player_exists:
717                 for id in world_db["Things"]:
718                     if world_db["Things"][id]["T_LIFEPOINTS"]:
719                         build_fov_map(world_db["Things"][id])
720                         if 0 == id:
721                             update_map_memory(world_db["Things"][id])
722                 world_db["WORLD_ACTIVE"] = 1
723
724
725 def test_for_id_maker(object, category):
726     """Return decorator testing for object having "id" attribute."""
727     def decorator(f):
728         def helper(*args):
729             if hasattr(object, "id"):
730                 f(*args)
731             else:
732                 print("Ignoring: No " + category +
733                       " defined to manipulate yet.")
734         return helper
735     return decorator
736
737
738 def command_tid(id_string):
739     """Set ID of Thing to manipulate. ID unused? Create new one.
740
741     Default new Thing's type to the first available ThingType, others: zero.
742     """
743     id = id_setter(id_string, "Things", command_tid)
744     if None != id:
745         if world_db["ThingTypes"] == {}:
746             print("Ignoring: No ThingType to settle new Thing in.")
747             return
748         type = list(world_db["ThingTypes"].keys())[0]
749         world_db["Things"][id] = new_Thing(type)
750
751
752 test_Thing_id = test_for_id_maker(command_tid, "Thing")
753
754
755 @test_Thing_id
756 def command_tcommand(str_int):
757     """Set T_COMMAND of selected Thing."""
758     val = integer_test(str_int, 0, 255)
759     if None != val:
760         if 0 == val or val in world_db["ThingActions"]:
761             world_db["Things"][command_tid.id]["T_COMMAND"] = val
762         else:
763             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
764
765
766 @test_Thing_id
767 def command_ttype(str_int):
768     """Set T_TYPE of selected Thing."""
769     val = integer_test(str_int, 0, 255)
770     if None != val:
771         if val in world_db["ThingTypes"]:
772             world_db["Things"][command_tid.id]["T_TYPE"] = val
773         else:
774             print("Ignoring: ThingType ID belongs to no known ThingType.")
775
776
777 @test_Thing_id
778 def command_tcarries(str_int):
779     """Append int(str_int) to T_CARRIES of selected Thing.
780
781     The ID int(str_int) must not be of the selected Thing, and must belong to a
782     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
783     """
784     val = integer_test(str_int, 0, 255)
785     if None != val:
786         if val == command_tid.id:
787             print("Ignoring: Thing cannot carry itself.")
788         elif val in world_db["Things"] \
789              and not world_db["Things"][val]["carried"]:
790             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
791             world_db["Things"][val]["carried"] = True
792         else:
793             print("Ignoring: Thing not available for carrying.")
794     # Note that the whole carrying structure is different from the C version:
795     # Carried-ness is marked by a "carried" flag, not by Things containing
796     # Things internally.
797
798
799 @test_Thing_id
800 def command_tmemthing(str_t, str_y, str_x):
801     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
802
803     The type must fit to an existing ThingType, and the position into the map.
804     """
805     type = integer_test(str_t, 0, 255)
806     posy = integer_test(str_y, 0, 255)
807     posx = integer_test(str_x, 0, 255)
808     if None != type and None != posy and None != posx:
809         if type not in world_db["ThingTypes"] \
810            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
811             print("Ignoring: Illegal value for thing type or position.")
812         else:
813             memthing = (type, posy, posx)
814             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
815
816
817 def setter_map(maptype):
818     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
819
820     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
821     """
822     @test_Thing_id
823     def helper(str_int, mapline):
824         val = integer_test(str_int, 0, 255)
825         if None != val:
826             if val >= world_db["MAP_LENGTH"]:
827                 print("Illegal value for map line number.")
828             elif len(mapline) != world_db["MAP_LENGTH"]:
829                 print("Map line length is unequal map width.")
830             else:
831                 length = world_db["MAP_LENGTH"]
832                 map = None
833                 if not world_db["Things"][command_tid.id][maptype]:
834                     map = bytearray(b' ' * (length ** 2))
835                 else:
836                     map = world_db["Things"][command_tid.id][maptype]
837                 map[val * length:(val * length) + length] = mapline.encode()
838                 world_db["Things"][command_tid.id][maptype] = map
839     return helper
840
841
842 def setter_tpos(axis):
843     """Generate setter for T_POSX or  T_POSY of selected Thing.
844
845     If world is active, rebuilds animate things' fovmap, player's memory map.
846     """
847     @test_Thing_id
848     def helper(str_int):
849         val = integer_test(str_int, 0, 255)
850         if None != val:
851             if val < world_db["MAP_LENGTH"]:
852                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
853                 if world_db["WORLD_ACTIVE"] \
854                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
855                     build_fov_map(world_db["Things"][command_tid.id])
856                     if 0 == command_tid.id:
857                         update_map_memory(world_db["Things"][command_tid.id])
858             else:
859                 print("Ignoring: Position is outside of map.")
860     return helper
861
862
863 def command_ttid(id_string):
864     """Set ID of ThingType to manipulate. ID unused? Create new one.
865
866     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
867     """
868     id = id_setter(id_string, "ThingTypes", command_ttid)
869     if None != id:
870         world_db["ThingTypes"][id] = {
871             "TT_NAME": "(none)",
872             "TT_CONSUMABLE": 0,
873             "TT_LIFEPOINTS": 0,
874             "TT_PROLIFERATE": 0,
875             "TT_START_NUMBER": 0,
876             "TT_SYMBOL": "?",
877             "TT_CORPSE_ID": id
878         }
879
880
881 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
882
883
884 @test_ThingType_id
885 def command_ttname(name):
886     """Set TT_NAME of selected ThingType."""
887     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
888
889
890 @test_ThingType_id
891 def command_ttsymbol(char):
892     """Set TT_SYMBOL of selected ThingType. """
893     if 1 == len(char):
894         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
895     else:
896         print("Ignoring: Argument must be single character.")
897
898
899 @test_ThingType_id
900 def command_ttcorpseid(str_int):
901     """Set TT_CORPSE_ID of selected ThingType."""
902     val = integer_test(str_int, 0, 255)
903     if None != val:
904         if val in world_db["ThingTypes"]:
905             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
906         else:
907             print("Ignoring: Corpse ID belongs to no known ThignType.")
908
909
910 def command_taid(id_string):
911     """Set ID of ThingAction to manipulate. ID unused? Create new one.
912
913     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
914     """
915     id = id_setter(id_string, "ThingActions", command_taid, True)
916     if None != id:
917         world_db["ThingActions"][id] = {
918             "TA_EFFORT": 1,
919             "TA_NAME": "wait"
920         }
921
922
923 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
924
925
926 @test_ThingAction_id
927 def command_taname(name):
928     """Set TA_NAME of selected ThingAction.
929
930     The name must match a valid thing action function. If after the name
931     setting no ThingAction with name "wait" remains, call set_world_inactive().
932     """
933     if name == "wait" or name == "move" or name == "use" or name == "drop" \
934        or name == "pick_up":
935         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
936         if 1 == world_db["WORLD_ACTIVE"]:
937             wait_defined = False
938             for id in world_db["ThingActions"]:
939                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
940                     wait_defined = True
941                     break
942             if not wait_defined:
943                 set_world_inactive()
944     else:
945         print("Ignoring: Invalid action name.")
946     # In contrast to the original,naming won't map a function to a ThingAction.
947
948
949 """Commands database.
950
951 Map command start tokens to ([0]) number of expected command arguments, ([1])
952 the command's meta-ness (i.e. is it to be written to the record file, is it to
953 be ignored in replay mode if read from server input file), and ([2]) a function
954 to be called on it.
955 """
956 commands_db = {
957     "QUIT": (0, True, command_quit),
958     "PING": (0, True, command_ping),
959     "THINGS_HERE": (2, True, command_thingshere),
960     "MAKE_WORLD": (1, False, command_makeworld),
961     "SEED_MAP": (1, False, command_seedmap),
962     "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
963                                          0, 4294967295)),
964     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
965     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
966     "MAP_LENGTH": (1, False, command_maplength),
967     "WORLD_ACTIVE": (1, False, command_worldactive),
968     "TA_ID": (1, False, command_taid),
969     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
970     "TA_NAME": (1, False, command_taname),
971     "TT_ID": (1, False, command_ttid),
972     "TT_NAME": (1, False, command_ttname),
973     "TT_SYMBOL": (1, False, command_ttsymbol),
974     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
975     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
976                                        0, 65535)),
977     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
978                                          0, 255)),
979     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
980                                         0, 255)),
981     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
982     "T_ID": (1, False, command_tid),
983     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
984     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
985     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
986     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
987     "T_COMMAND": (1, False, command_tcommand),
988     "T_TYPE": (1, False, command_ttype),
989     "T_CARRIES": (1, False, command_tcarries),
990     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
991     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
992     "T_MEMTHING": (3, False, command_tmemthing),
993     "T_POSY": (1, False, setter_tpos("Y")),
994     "T_POSX": (1, False, setter_tpos("X")),
995     "wait": (0, False, play_commander("wait")),
996     "move": (1, False, play_commander("move")),
997     "pick_up": (0, False, play_commander("pick_up")),
998     "drop": (1, False, play_commander("drop", True)),
999     "use": (1, False, play_commander("use", True)),
1000 }
1001
1002
1003 """World state database. With sane default values."""
1004 world_db = {
1005     "TURN": 0,
1006     "SEED_MAP": 0,
1007     "SEED_RANDOMNESS": 0,
1008     "PLAYER_TYPE": 0,
1009     "MAP_LENGTH": 64,
1010     "WORLD_ACTIVE": 0,
1011     "ThingActions": {},
1012     "ThingTypes": {},
1013     "Things": {}
1014 }
1015
1016
1017 """File IO database."""
1018 io_db = {
1019     "path_save": "save",
1020     "path_record": "record",
1021     "path_worldconf": "confserver/world",
1022     "path_server": "server/",
1023     "path_in": "server/in",
1024     "path_out": "server/out",
1025     "path_worldstate": "server/worldstate",
1026     "tmp_suffix": "_tmp",
1027     "kicked_by_rival": False,
1028     "worldstate_updateable": False
1029 }
1030
1031
1032 try:
1033     opts = parse_command_line_arguments()
1034     setup_server_io()
1035     if None != opts.replay:
1036         replay_game()
1037     else:
1038         play_game()
1039 except SystemExit as exit:
1040     print("ABORTING: " + exit.args[0])
1041 except:
1042     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1043     raise
1044 finally:
1045     cleanup_server_io()