#!/usr/bin/env python # -*- coding: utf-8 -*- # $Id$ from __future__ import with_statement import os import re import sys import cgi import glob import copy import fcntl import shutil import hashlib import ConfigParser try: import cStringIO as _StringIO except ImportError: import StringIO as _StringIO try: import cPickle as _pickle except ImportError: import pickle as _pickle from Cheetah.Template import Template import pycodebattler CONFIG = ConfigParser.SafeConfigParser() CONFIG.read('pycgibattler.conf') class ListChooser: def __init__(self, code): self._code = code self._digest = int(hashlib.sha512(code).hexdigest(), 16) def lslide(self, i): self._digest <<= i def rslide(self, i): self._digest >>= i def choose(self, list_): return list_[self._digest % len(list_)] class CharacterManager: VALID_ID = re.compile(r'^[a-z0-9]{128}$') DATA_DIR = os.path.expanduser(CONFIG.get('data', 'basedir')) CHARA_NAMES = [x.strip() for x in open(os.path.expanduser( CONFIG.get('character', 'name_list_file'))).xreadlines()] SKILL_NAMES = [x.strip() for x in open(os.path.expanduser( CONFIG.get('character', 'skill_list_file'))).xreadlines()] SKILL_TYPES = [ #pycodebattler.skill.HealType, #pycodebattler.skill.ResurrectionType, pycodebattler.skill.SingleAttackType, pycodebattler.skill.RangeAttackType, pycodebattler.skill.MultiAttackType, pycodebattler.skill.SuicideAttackType, ] def __init__(self, id_): if not self.VALID_ID.match(id_): raise ValueError('Invalid ID: %s' % id_) self._id = id_ self._locker = os.path.join(self.DATA_DIR, '.%s.lck' % id_) self._prefix = os.path.join(self.DATA_DIR, id_) self._codepath = os.path.join(self._prefix, '%s.py' % id_) self._datapath = os.path.join(self._prefix, '%s.dat' % id_) def id(self): return self._id def mtime(self): with open(self._locker) as fp: fcntl.flock(fp, fcntl.LOCK_SH) return os.fstat(fp.fileno()).st_mtime def delete(self): with open(self._locker, 'w') as fp: fcntl.flock(fp, fcntl.LOCK_EX) shutil.rmtree(self._prefix, True) os.remove(self._locker) # XXX def load(self): with open(self._locker) as fp: fcntl.flock(fp, fcntl.LOCK_SH) code = open(self._codepath).read() warrior = _pickle.load(open(self._datapath)) return code, warrior def flatten(self): code, warrior = self.load() return { 'id': self.id(), 'mtime': self.mtime(), 'code': code, 'warrior': warrior, 'skills': [warrior.skill(s) for s in warrior.skill_list()], } @classmethod def dump(klass, name, code): self = klass(hashlib.sha512(code).hexdigest()) cname = os.path.join(self._prefix, os.path.basename(name)) warrior = self.make_warrior(code) with open(self._locker, 'w') as fp: fcntl.flock(fp, fcntl.LOCK_EX) try: os.mkdir(self._prefix) except OSError: if not os.path.isdir(self._prefix): raise open(self._codepath, 'w').write(code) _pickle.dump(warrior, open(self._datapath, 'w')) try: os.symlink(os.path.basename(self._codepath), cname) except OSError: if not os.path.islink(cname): raise return self @classmethod def make_warrior(klass, code): lc = ListChooser(code) name = lc.choose(klass.CHARA_NAMES) skills = [] for i in range(lc.choose(range(1, 4))): lc.lslide(i) skname = lc.choose(klass.SKILL_NAMES) sktype = lc.choose(klass.SKILL_TYPES) sklevel = lc.choose(range(1, 4)) skpoint = lc.choose(range(sklevel * 4, sklevel * 7)) sk = pycodebattler.skill.Skill(skname, skpoint, sktype, sklevel) skills.append(sk) return pycodebattler.warrior.Warrior( name, _StringIO.StringIO(code), skills) @classmethod def list(klass): return sorted((klass(os.path.basename(d)) for d in glob.iglob(os.path.join(klass.DATA_DIR, '*')) if klass.VALID_ID.match(os.path.basename(d))), cmp=klass.mtimecmp) @classmethod def sweep(klass, thresh=CONFIG.getint('limit', 'max_entries')): for self in klass.list()[thresh:]: self.delete() @staticmethod def mtimecmp(a, b): return int(b.mtime()) - int(a.mtime()) def do_battle(warriors, turn=CONFIG.getint('battle', 'max_turn')): proto = pycodebattler.battle.BattleProto(warriors, turn) result = { 'winner': None, 'history': [], } try: for turn in proto.battle(): actions = [] try: for attacker, skname, damages in turn: actions.append({ 'attacker': copy.deepcopy(attacker), 'skill': copy.deepcopy(skname), 'damages': copy.deepcopy(damages), }) except pycodebattler.battle.DrawGame: pass result['history'].append({ 'actions': actions, 'warriors': copy.deepcopy(warriors), }) result['winner'] = proto.winner() except pycodebattler.battle.DrawGame: pass return result def httpdump(template, entries=[], result={}): tmpl = Template(open(os.path.expanduser(template)).read()) tmpl.script = sys.argv[0] tmpl.config = CONFIG tmpl.entries = entries tmpl.result = result sys.stdout.write('Content-type: text/html; charset=UTF-8\r\n\r\n') sys.stdout.write(tmpl.respond().encode('utf-8')) sys.stdout.flush() def main(): form = cgi.FieldStorage() if 'id' in form: entries = [CharacterManager(form.getfirst('id')).flatten()] return httpdump(CONFIG.get('template', 'character'), entries=entries) if 'warrior' in form: ids = form['warrior'] if type(ids) is list: if len(ids) > CONFIG.getint('battle', 'max_entries'): raise ValueError('battle warriors too long') warriors = [CharacterManager(id_.value).load()[1] for id_ in ids] result = do_battle(warriors) return httpdump(CONFIG.get('template', 'battle'), result=result) if 'filename' in form: item = form['filename'] if item.file and item.filename: code = item.file.read(CONFIG.getint('limit', 'max_size')) if code: CharacterManager.dump(item.filename, code) CharacterManager.sweep() entries = [cm.flatten() for cm in CharacterManager.list()] return httpdump(CONFIG.get('template', 'index'), entries=entries) if __name__ == '__main__': try: main() except: httpdump(CONFIG.get('template', 'error'))