#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2010 by Atzm WATANABE # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License (version 2) as # published by the Free Software Foundation. It is distributed in the # hope that it will be useful, but WITHOUT ANY WARRANTY; without even the # implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # # $Id$ # from __future__ import with_statement import os import re import sys import cgi import json import glob import copy import fcntl import base64 import shutil import hashlib import xmlrpclib 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-f0-9]{128}$') DATA_DIR = os.path.expanduser(CONFIG.get('data', 'basedir')) 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 entry(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()], } def flatten(self): code, warrior = self.load() skills = [] for name in warrior.skill_list(): sk = warrior.skill(name) skills.append({ 'name': name, 'type': str(sk.skilltype()).split('.')[-1], 'level': sk.level(), 'point': sk.point(), }) return { 'id': self.id(), 'mtime': int(self.mtime()), 'name': warrior.name(), 'hitpoint': warrior.hitpoint(), 'skillpoint': warrior.skillpoint(), 'strength': warrior.strength(), 'concentration': warrior.concentration(), 'defense': warrior.defense(), 'agility': warrior.agility(), 'luck': warrior.luck(), 'skills': skills, } @classmethod def dump(klass, name, code, data): self = klass(hashlib.sha512(code).hexdigest()) cname = os.path.join(self._prefix, os.path.basename(name)) warrior = self.make_warrior(code, data) 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, data): chara_names = [x.strip() for x in open(os.path.expanduser( CONFIG.get('character', 'name_list_file'))).xreadlines()] skill_data = json.load(open(os.path.expanduser( CONFIG.get('character', 'skill_list_file')))) lc = ListChooser(code) name = lc.choose(chara_names) skills = [] for i in range(lc.choose(range(1, 4))): lc.lslide(i) sk = lc.choose(skill_data) skname = sk['name'].encode('utf-8', 'replace') skpoint = sk['point'] sktype = getattr(pycodebattler.skill, sk['type']) sklevel = sk['level'] sk = pycodebattler.skill.Skill(skname, skpoint, sktype, sklevel) skills.append(sk) return pycodebattler.warrior.Warrior( name, int(data['hitpoint']), int(data['skillpoint']), int(data['strength']), int(data['concentration']), int(data['defense']), int(data['agility']), int(data['luck']), 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', 'replace')) sys.stdout.flush() def jsondump(data): sys.stdout.write('Content-type: application/json; charset=UTF-8\r\n\r\n') sys.stdout.write(json.dumps(data, ensure_ascii=False) + '\n') sys.stdout.flush() def scouter(lang, code): if lang not in CONFIG.options('scouter_api'): raise ValueError('invalid language') proxy = xmlrpclib.ServerProxy(CONFIG.get('scouter_api', lang)) data = proxy.Scout({'version': 1.0, 'code': base64.standard_b64encode(code)}) if type(data) is not dict: raise ValueError('invalid api response') if 'version' not in data: raise ValueError('invalid api response (version not found)') if data['version'] != 1.0: raise ValueError('invalid api response (invalid version)') if 'result' not in data: raise ValueError('invalid api response (result not found)') if type(data['result']) is not dict: raise ValueError('invalid api response (invalid result)') return data['result'] def handle_mode_json(form): if 'id' not in form: ids = [cm.id() for cm in CharacterManager.list()] else: tmp = form['id'] if type(tmp) is not list: tmp = [form['id']] ids = [id_.value for id_ in tmp] data = {} try: for id_ in ids: data[id_] = CharacterManager(id_).flatten() except: return jsondump({'result': {}}) return jsondump({'result': data}) def handle_id(form): entries = [CharacterManager(form.getfirst('id')).entry()] return httpdump(CONFIG.get('template', 'character'), entries=entries) def handle_warrior(form): ids = form['warrior'] if type(ids) is not list: ids = [ids] 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) def handle_file(form): item = form['filename'] lang = form.getfirst('lang') if not item.file or not item.filename or not lang: raise ValueError('missing parameter') code = item.file.read(CONFIG.getint('limit', 'max_size')) data = scouter(lang, code) CharacterManager.dump(item.filename, code, data) return True def main(): form = cgi.FieldStorage() if 'mode' in form and form.getfirst('mode') == 'json': return handle_mode_json(form) if 'id' in form: return handle_id(form) if 'warrior' in form: return handle_warrior(form) if 'filename' in form and 'lang' in form: if not handle_file(form): return httpdump(CONFIG.get('template', 'error')) CharacterManager.sweep() entries = [cm.entry() for cm in CharacterManager.list()] return httpdump(CONFIG.get('template', 'index'), entries=entries) if __name__ == '__main__': try: main() except: httpdump(CONFIG.get('template', 'error'))