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