[57] | 1 | #!/usr/bin/env python |
---|
| 2 | # -*- coding: utf-8 -*- |
---|
| 3 | # $Id$ |
---|
| 4 | |
---|
| 5 | from __future__ import with_statement |
---|
| 6 | |
---|
| 7 | import os |
---|
| 8 | import re |
---|
| 9 | import sys |
---|
| 10 | import cgi |
---|
[72] | 11 | import json |
---|
[57] | 12 | import glob |
---|
| 13 | import copy |
---|
| 14 | import fcntl |
---|
| 15 | import shutil |
---|
| 16 | import hashlib |
---|
| 17 | import ConfigParser |
---|
| 18 | |
---|
| 19 | try: |
---|
| 20 | import cStringIO as _StringIO |
---|
| 21 | except ImportError: |
---|
| 22 | import StringIO as _StringIO |
---|
| 23 | |
---|
| 24 | try: |
---|
| 25 | import cPickle as _pickle |
---|
| 26 | except ImportError: |
---|
| 27 | import pickle as _pickle |
---|
| 28 | |
---|
| 29 | from Cheetah.Template import Template |
---|
| 30 | import pycodebattler |
---|
| 31 | |
---|
| 32 | |
---|
| 33 | CONFIG = ConfigParser.SafeConfigParser() |
---|
| 34 | CONFIG.read('pycgibattler.conf') |
---|
| 35 | |
---|
| 36 | |
---|
| 37 | class ListChooser: |
---|
| 38 | def __init__(self, code): |
---|
| 39 | self._code = code |
---|
| 40 | self._digest = int(hashlib.sha512(code).hexdigest(), 16) |
---|
| 41 | |
---|
| 42 | def lslide(self, i): |
---|
| 43 | self._digest <<= i |
---|
| 44 | |
---|
| 45 | def rslide(self, i): |
---|
| 46 | self._digest >>= i |
---|
| 47 | |
---|
| 48 | def choose(self, list_): |
---|
| 49 | return list_[self._digest % len(list_)] |
---|
| 50 | |
---|
| 51 | |
---|
| 52 | class CharacterManager: |
---|
[66] | 53 | VALID_ID = re.compile(r'^[a-f0-9]{128}$') |
---|
[57] | 54 | DATA_DIR = os.path.expanduser(CONFIG.get('data', 'basedir')) |
---|
| 55 | |
---|
| 56 | def __init__(self, id_): |
---|
| 57 | if not self.VALID_ID.match(id_): |
---|
| 58 | raise ValueError('Invalid ID: %s' % id_) |
---|
| 59 | self._id = id_ |
---|
| 60 | self._locker = os.path.join(self.DATA_DIR, '.%s.lck' % id_) |
---|
| 61 | self._prefix = os.path.join(self.DATA_DIR, id_) |
---|
| 62 | self._codepath = os.path.join(self._prefix, '%s.py' % id_) |
---|
| 63 | self._datapath = os.path.join(self._prefix, '%s.dat' % id_) |
---|
| 64 | |
---|
| 65 | def id(self): |
---|
| 66 | return self._id |
---|
| 67 | |
---|
| 68 | def mtime(self): |
---|
| 69 | with open(self._locker) as fp: |
---|
| 70 | fcntl.flock(fp, fcntl.LOCK_SH) |
---|
[63] | 71 | return os.fstat(fp.fileno()).st_mtime |
---|
[57] | 72 | |
---|
| 73 | def delete(self): |
---|
| 74 | with open(self._locker, 'w') as fp: |
---|
| 75 | fcntl.flock(fp, fcntl.LOCK_EX) |
---|
| 76 | shutil.rmtree(self._prefix, True) |
---|
| 77 | os.remove(self._locker) # XXX |
---|
| 78 | |
---|
| 79 | def load(self): |
---|
| 80 | with open(self._locker) as fp: |
---|
| 81 | fcntl.flock(fp, fcntl.LOCK_SH) |
---|
| 82 | code = open(self._codepath).read() |
---|
| 83 | warrior = _pickle.load(open(self._datapath)) |
---|
| 84 | return code, warrior |
---|
| 85 | |
---|
| 86 | def flatten(self): |
---|
| 87 | code, warrior = self.load() |
---|
| 88 | return { |
---|
| 89 | 'id': self.id(), |
---|
| 90 | 'mtime': self.mtime(), |
---|
| 91 | 'code': code, |
---|
| 92 | 'warrior': warrior, |
---|
| 93 | 'skills': [warrior.skill(s) for s in warrior.skill_list()], |
---|
| 94 | } |
---|
| 95 | |
---|
| 96 | @classmethod |
---|
| 97 | def dump(klass, name, code): |
---|
| 98 | self = klass(hashlib.sha512(code).hexdigest()) |
---|
| 99 | cname = os.path.join(self._prefix, os.path.basename(name)) |
---|
| 100 | warrior = self.make_warrior(code) |
---|
| 101 | |
---|
| 102 | with open(self._locker, 'w') as fp: |
---|
| 103 | fcntl.flock(fp, fcntl.LOCK_EX) |
---|
| 104 | |
---|
| 105 | try: |
---|
| 106 | os.mkdir(self._prefix) |
---|
| 107 | except OSError: |
---|
| 108 | if not os.path.isdir(self._prefix): |
---|
| 109 | raise |
---|
| 110 | |
---|
| 111 | open(self._codepath, 'w').write(code) |
---|
| 112 | _pickle.dump(warrior, open(self._datapath, 'w')) |
---|
| 113 | |
---|
| 114 | try: |
---|
| 115 | os.symlink(os.path.basename(self._codepath), cname) |
---|
| 116 | except OSError: |
---|
| 117 | if not os.path.islink(cname): |
---|
| 118 | raise |
---|
| 119 | |
---|
| 120 | return self |
---|
| 121 | |
---|
| 122 | @classmethod |
---|
| 123 | def make_warrior(klass, code): |
---|
[67] | 124 | chara_names = [x.strip() for x in open(os.path.expanduser( |
---|
| 125 | CONFIG.get('character', 'name_list_file'))).xreadlines()] |
---|
[72] | 126 | skill_data = json.load(open(CONFIG.get('character', 'skill_list_file'))) |
---|
[67] | 127 | |
---|
[57] | 128 | lc = ListChooser(code) |
---|
[67] | 129 | name = lc.choose(chara_names) |
---|
[57] | 130 | skills = [] |
---|
| 131 | |
---|
| 132 | for i in range(lc.choose(range(1, 4))): |
---|
| 133 | lc.lslide(i) |
---|
[72] | 134 | sk = lc.choose(skill_data) |
---|
| 135 | skname = sk['name'] |
---|
| 136 | skpoint = sk['point'] |
---|
| 137 | sktype = getattr(pycodebattler.skill, sk['type']) |
---|
| 138 | sklevel = sk['level'] |
---|
[57] | 139 | sk = pycodebattler.skill.Skill(skname, skpoint, sktype, sklevel) |
---|
| 140 | skills.append(sk) |
---|
| 141 | |
---|
| 142 | return pycodebattler.warrior.Warrior( |
---|
| 143 | name, _StringIO.StringIO(code), skills) |
---|
| 144 | |
---|
| 145 | @classmethod |
---|
| 146 | def list(klass): |
---|
| 147 | return sorted((klass(os.path.basename(d)) for d in |
---|
| 148 | glob.iglob(os.path.join(klass.DATA_DIR, '*')) |
---|
| 149 | if klass.VALID_ID.match(os.path.basename(d))), |
---|
| 150 | cmp=klass.mtimecmp) |
---|
| 151 | |
---|
| 152 | @classmethod |
---|
| 153 | def sweep(klass, thresh=CONFIG.getint('limit', 'max_entries')): |
---|
| 154 | for self in klass.list()[thresh:]: |
---|
| 155 | self.delete() |
---|
| 156 | |
---|
| 157 | @staticmethod |
---|
| 158 | def mtimecmp(a, b): |
---|
| 159 | return int(b.mtime()) - int(a.mtime()) |
---|
| 160 | |
---|
| 161 | |
---|
| 162 | def do_battle(warriors, turn=CONFIG.getint('battle', 'max_turn')): |
---|
| 163 | proto = pycodebattler.battle.BattleProto(warriors, turn) |
---|
| 164 | result = { |
---|
| 165 | 'winner': None, |
---|
| 166 | 'history': [], |
---|
| 167 | } |
---|
| 168 | |
---|
| 169 | try: |
---|
| 170 | for turn in proto.battle(): |
---|
| 171 | actions = [] |
---|
| 172 | try: |
---|
| 173 | for attacker, skname, damages in turn: |
---|
| 174 | actions.append({ |
---|
| 175 | 'attacker': copy.deepcopy(attacker), |
---|
| 176 | 'skill': copy.deepcopy(skname), |
---|
| 177 | 'damages': copy.deepcopy(damages), |
---|
| 178 | }) |
---|
| 179 | except pycodebattler.battle.DrawGame: |
---|
| 180 | pass |
---|
| 181 | |
---|
| 182 | result['history'].append({ |
---|
| 183 | 'actions': actions, |
---|
| 184 | 'warriors': copy.deepcopy(warriors), |
---|
| 185 | }) |
---|
| 186 | |
---|
| 187 | result['winner'] = proto.winner() |
---|
| 188 | |
---|
| 189 | except pycodebattler.battle.DrawGame: |
---|
| 190 | pass |
---|
| 191 | |
---|
| 192 | return result |
---|
| 193 | |
---|
| 194 | |
---|
| 195 | def httpdump(template, entries=[], result={}): |
---|
| 196 | tmpl = Template(open(os.path.expanduser(template)).read()) |
---|
| 197 | tmpl.script = sys.argv[0] |
---|
| 198 | tmpl.config = CONFIG |
---|
| 199 | tmpl.entries = entries |
---|
| 200 | tmpl.result = result |
---|
| 201 | sys.stdout.write('Content-type: text/html; charset=UTF-8\r\n\r\n') |
---|
| 202 | sys.stdout.write(tmpl.respond().encode('utf-8')) |
---|
| 203 | sys.stdout.flush() |
---|
| 204 | |
---|
| 205 | |
---|
| 206 | def main(): |
---|
| 207 | form = cgi.FieldStorage() |
---|
| 208 | |
---|
| 209 | if 'id' in form: |
---|
| 210 | entries = [CharacterManager(form.getfirst('id')).flatten()] |
---|
| 211 | return httpdump(CONFIG.get('template', 'character'), entries=entries) |
---|
| 212 | |
---|
| 213 | if 'warrior' in form: |
---|
| 214 | ids = form['warrior'] |
---|
| 215 | if type(ids) is list: |
---|
| 216 | if len(ids) > CONFIG.getint('battle', 'max_entries'): |
---|
| 217 | raise ValueError('battle warriors too long') |
---|
| 218 | warriors = [CharacterManager(id_.value).load()[1] for id_ in ids] |
---|
| 219 | result = do_battle(warriors) |
---|
| 220 | return httpdump(CONFIG.get('template', 'battle'), result=result) |
---|
| 221 | |
---|
| 222 | if 'filename' in form: |
---|
| 223 | item = form['filename'] |
---|
| 224 | if item.file and item.filename: |
---|
| 225 | code = item.file.read(CONFIG.getint('limit', 'max_size')) |
---|
| 226 | if code: |
---|
| 227 | CharacterManager.dump(item.filename, code) |
---|
| 228 | |
---|
| 229 | CharacterManager.sweep() |
---|
| 230 | |
---|
| 231 | entries = [cm.flatten() for cm in CharacterManager.list()] |
---|
| 232 | return httpdump(CONFIG.get('template', 'index'), entries=entries) |
---|
| 233 | |
---|
| 234 | |
---|
| 235 | if __name__ == '__main__': |
---|
| 236 | try: |
---|
| 237 | main() |
---|
| 238 | except: |
---|
| 239 | httpdump(CONFIG.get('template', 'error')) |
---|