#!/usr/bin/env python3 import os import random import urllib import uuid import flask import flask_socketio import pony from pony.flask import Pony word_file = "/usr/share/dict/words" WORDS = open(word_file).read().splitlines() app = flask.Flask(__name__) socketio = flask_socketio.SocketIO(app) db = None # @TODO Change this into using redis or something else for storage across # threads etc. session_map = {} @app.route('/') @pony.orm.db_session def index(): r = flask.Response() uid, user = get_uid(r) negotiations = pony.orm.select(n for n in Negotiation if n.owner == user) suggestion = suggest_unused_name() r.data = render_page('index.html', { 'negotiations': list(negotiations), 'suggestion': suggestion, }) return r @pony.orm.db_session def get_uid(r, create = True): """ Returns a tuple (uid, created) where created is true if the user was new. r is the flask response, so that the cookie uid may be set if necessary. """ uid = flask.request.cookies.get('uid') if not uid: if create: uid = generate_uid() r.set_cookie('uid', uid) else: return None, None user = pony.orm.select(u for u in User if u.uid == uid).first() if not user: # Recreate the database user if it doesn't exist user = User(uid = uid) return uid, user @app.route('/negotiation/create', methods = ['POST']) @pony.orm.db_session def negotiation_create(): n = flask.request.form.get('negotiation') uid, user = get_uid(None, False) if not uid or not user: return flask.redirect('/') # @TODO Signal error to user. They don't have a uid cookie if len(n) > 128: return flask.redirect('/') # @TODO Signal error to user. Request too Large nego = get_negotiation(n) if nego: # @TODO Signal to the user that the negotiation already exists and they # are being sent to it instead. return flask.redirect('/negotiations/{}'.format(n)) nego = Negotiation(name = n, owner = user.id, taker_sway = 1, market_sway = 6, is_first_negotiation = True, manual_negotiation = True, current_phase = 'marketprep', ) pony.orm.commit() return flask.redirect('/negotiations/{}'.format(n)) @app.route('/negotiations/') def negotiation_redir(): return flask.redirect('/') @app.route('/negotiations/') def negotiation(negotiation): r = flask.Response() uid, user = get_uid(r) nego = get_negotiation(negotiation) if not nego: return "Error!" c = { 'negotiation': nego, 'user': uid, 'room_owner': nego.owner, 'room_owner_display_name': nego.owner.display_name if nego.owner.display_name else nego.owner.uid, 'participant_count': len(get_room_participants(negotiation)), 'phase': get_negotiation_phase(nego), } r.data = render_page('negotiation.html', c) return r @socketio.on('join negotiation') def handle_join_negotiation(json): r = flask.Response() app.logger.info('Received join request: {}'.format(json)) uid, user = get_uid(r) flask_socketio.join_room(json['room']) session_map[flask.request.sid] = uid; app.logger.info('{} has joined room "{}" (sid: {})'.format(uid, json['room'], flask.request.sid)) participants = get_room_participants(json['room']) app.logger.info('{} has {} connections'.format( json['room'], len(participants))) flask_socketio.emit('participants changed', {'participants': participants}, room = json['room']) # Update the user's negotation state with what is stored on the server nego = get_negotiation(json['room']) # Send the newly arrived use the computed phase information phase = get_negotiation_phase(nego); return {'negotiation': nego.to_dict(), 'phase': phase} phases = { 'marketprep': { 'previous': None, 'next': 'takerprep', 'display_name': 'Market Prep', 'description': 'Set up the negotiation\'s basic information in the UI, under "Negotiation Settings". When you\'re done, click on the "Next" button (right hand swip image)', 'description_taker': 'The Market is setting up the negotiation, hang on.', 'has_sub_phases': False, }, 'takerprep': { 'previous': 'marketprep', 'next': 'negotiatorselection', 'display_name': 'Taker Prep', 'description': 'Each taker may take one of the prep actions: x, y, z', 'has_sub_phases': False, }, 'negotiatorselection': { 'previous': 'takerprep', 'next': 'firstimpression', 'display_name': 'Negotiator Selection', 'description': 'The crew selectons one member to be the negotiator. This character will perform all of the negotiation actions, but may be supported by the rest of the crew during scams.', 'has_sub_phases': False, }, 'firstimpression': { 'previous': 'negotiatorselection', 'next': 'negotiation', 'display_name': 'First Impression', 'description': 'The taker crew\'s negotiator rolls a leadership check. The results of this check determine the number of rounds that a negotiation lasts, the taker\'s starting position on the sway tracker, if the negotiation length is known to the takers, and who will lead in each negotiation round.', 'has_sub_phases': False, }, 'negotiation': { 'previous': 'firstimpression', 'next': 'wrapup', 'display_name': 'Negotiation', 'description': 'A round robin of: both negotiators playing tactics in order, checking if the negotiation should end, and a scam if not all the remaining takers have done one already.', 'has_sub_phases': True, 'sub_phases_repeat': True, 'sub_phases': [ 'first_negotiator_tactic', 'second_negotiatior_tactic', 'check_negotiation_end', 'scam', ], }, 'wrapup': { 'previous': 'negotiation', 'next': 'done', 'display_name': 'Wrap-Up', 'description': 'Final details to close out the negotiation', 'has_sub_phases': True, 'sub_phases_repeat': False, 'sub_phases': [ 'leadership', 'accept_backout', 'undercut', ], }, 'done': { 'previous': 'wrapup', 'next': None, 'display_name': 'Done', 'description': 'The negotiation is concluded', 'has_sub_phases': False, }, } def get_negotiation_phase(nego): r = dict(); r['current_phase'] = nego.current_phase r['current_sub_phase'] = nego.current_sub_phase r['display_name'] = phases[r['current_phase']]['display_name'] if r['current_phase'] in phases else 'Unknown phase "{}"'.format(r['current_phase']) r['description'] = phases[r['current_phase']]['description'] if r['current_phase'] in phases else 'Unknown phase "{}"'.format(r['current_phase']) r['description_taker'] = phases[r['current_phase']]['description_taker'] if 'description_taker' in phases[r['current_phase']].keys() else r['description'] # Can the takers see the round information? r['visible_rounds'] = True r['round_current'] = nego.negotiation_round try: r['round_max'] = max(floor(nego.first_impression_black / 2), 5) except: r['round_max'] = 'First impression not yet done' r['previous_phase'] = phases[nego.current_phase]['previous'] r['next_phase'] = phases[nego.current_phase]['next'] return r @socketio.on('set next phase') @pony.orm.db_session def handle_set_next_phase(json): r = flask.Response() app.logger.info('Received set next phase request: {}'.format(json)) uid, user = get_uid(r) nego = get_negotiation(json['room']) app.logger.info('Room owner is {}'.format(nego.owner.uid)) if nego.owner.uid != uid: # Refuse the update from non-owners app.logger.warning('Refusing set next phase of {} from non-owner {}'.format(json['room'], uid)) return False current_phase = nego.current_phase next_phase_key = phases[current_phase]['next']; if next_phase_key is None: # @TODO. Anything here? return False # @TODO Handle sub phases nego.current_phase = next_phase_key pony.orm.commit() data = { 'phase': get_negotiation_phase(nego), 'transitioned_from': current_phase, } flask_socketio.emit('negotiation state changed', {**data}, room = json['room']) return True @socketio.on('set previous phase') @pony.orm.db_session def handle_set_prev_phase(json): r = flask.Response() app.logger.info('Received set previous phase request: {}'.format(json)) uid, user = get_uid(r) nego = get_negotiation(json['room']) app.logger.info('Room owner is {}'.format(nego.owner.uid)) if nego.owner.uid != uid: # Refuse the update from non-owners app.logger.warning('Refusing set previous phase of {} from non-owner {}'.format(json['room'], uid)) return False current_phase = nego.current_phase next_phase_key = phases[current_phase]['previous']; if next_phase_key is None: # @TODO. Anything here? return False # @TODO Handle sub phases nego.current_phase = next_phase_key pony.orm.commit() data = { 'phase': get_negotiation_phase(nego), 'transitioned_from': current_phase, } flask_socketio.emit('negotiation state changed', {**data}, room = json['room']) return True def get_room_participants(room): if '/' not in socketio.server.manager.get_namespaces(): return [] try: sessions = [s for s in socketio.server.manager.get_participants('/', room)] participants = [p for s,p in session_map.items() if s in sessions] except: return [] app.logger.debug(participants) return participants @socketio.on('update negotiation') @pony.orm.db_session def handle_update_negotiation(json): r = flask.Response() app.logger.info('Received update request: {}'.format(json)) uid, user = get_uid(r) nego = get_negotiation(json['room']) app.logger.info('Room owner is {}'.format(nego.owner.uid)) if nego.owner.uid != uid: # Refuse the update from non-owners app.logger.warning('Refusing update of {} from non-owner {}'.format(json['room'], uid)) return False del json['room'] # If a negotiation was set to manual, it may not be set back to automatic. if nego.manual_negotiation and 'manual_negotiation' in json: del json['manual_negotiation'] nego.set(**json) pony.orm.commit() flask_socketio.emit('negotiation updated', {**json}, room = nego.name) return True @socketio.on('leave negotiation') def handle_leave_disconnect(json): r = flask.Response() app.logger.info('Received join request: {}'.format(json)) uid, user = get_uid(r) del session_map[flask.request.sid] app.logger.info('{} has disconnected from {} (sid: {})'.format(uid, json['room'],flask.request.sid)) participants = get_room_participants(json['room']) flask_socketio.emit('participants changed', {'participants': participants}, room = json['room']) return True @pony.orm.db_session def get_negotiation(negotiation): return pony.orm.select(nego for nego in Negotiation if nego.name == negotiation).first() @pony.orm.db_session def generate_uid(): while True: uid = str(uuid.uuid4()) if pony.orm.exists(u for u in User if u.uid == uid): continue u = User(uid = uid) pony.orm.commit() return uid @pony.orm.db_session def suggest_unused_name(): tries = 0; while True: tries += 1 w = "{}{}{}{}".format( random.choice(WORDS).title(), random.choice(WORDS).title(), random.choice(WORDS).title(), random.choice(WORDS).title() ) if not w.isalpha(): continue if pony.orm.exists(n.name for n in Negotiation if n.name == w): continue app.logger.info('Took {} tries to generate a room suggestion'.format(tries)) return w def render_page(template, context): app_context = { 'LANGUAGE': flask.request.cookies.get('language') if flask.request.cookies.get('language') else app.config['DEFAULT_LANGUAGE'], } context = {**context, **app_context} return flask.render_template(template, **context) if __name__ == '__main__': port = os.environ.get('APP_PORT', 5000) appkey = os.environ.get('APP_KEY', 'test') bindaddr = os.environ.get('APP_BINDADDRESS', '127.0.0.1') app.secret_key = appkey app.config.update(dict( PONY = { 'provider': 'sqlite', 'filename': 'negotiation.db', 'create_db': True, }, DEFAULT_LANGUAGE = 'en', )) db = pony.orm.Database() class User(db.Entity): uid = pony.orm.Required(str) display_name = pony.orm.Optional(str) negotiations = pony.orm.Set('Negotiation') # Negotiations have a state. Maybe storing a state machine object (pickle?) # would be an idea. But maybe not. # # Let's start with understanding negotiations # 1. Prep Work: Before a negotiation begins, each take may do Prep Work # 2. The takers decide who their Lead Negotiator will be. Once set, this # cannot be changed. # 3. The Lead Negotiation makes a First Impression # This is a leadership check which has four effects: # 1. The number of rounds of negotiation: max(floor(black+ld/2), 5) # 2. The starting position (crit success +1 sway, crit failure: -1 sway) # 3. Whether or not the negotiation length is known to takers. T/F based # simple succes. # 4. Who leads the negotiations (starting round): takers on success, Market otherwise. # 4. Negotiation Round(s) # 1. Lead negotiator plays a negotiation tactic # 2. Other negotiator plays a negotiation tactic # 3. Check negotiation end condition # 4. If not ended, a non-negotiator taker may play scam # 5. Wrap-up # 1. Taker negotiator rolls Leadership. Success moves up to meet Market, # failure moves market down to meet takers. # 2a. Takers may accept the deal. # 2b. Takers may back out. Next negotiation starts on lowest tracker, has # no prep work, scams, or will. # 3. Competition undercuts. If able, the competition will undercut by one # spot on the way tracker. _Any_ taker may make a CHA check to stop this, # but on failure the takers must lower theire price to match the competition. # class Negotiation(db.Entity): name = pony.orm.PrimaryKey(str) owner = pony.orm.Required(User) client_name = pony.orm.Optional(str) negotiator_name = pony.orm.Optional(str) taker_crew_name = pony.orm.Optional(str) # Manual negotation manual_negotiation = pony.orm.Optional(bool) # marketprep, takerprep, negotiatorselection, firstimpression, # negotiation (first, second, checkendcondition, scam) # wrapup ( leadership, accept_refuse, undercut) current_phase = pony.orm.Optional(str) current_sub_phase = pony.orm.Optional(str) # Pre-negotiation details takers = pony.orm.Optional(int) lead_negotiator = pony.orm.Optional(str) is_first_negotiation = pony.orm.Optional(bool) first_impression_black = pony.orm.Optional(int) # One of critfail, fail, success, critsuccess first_impression_state = pony.orm.Optional(str) negotiation_round = pony.orm.Optional(int) # One of leadnego, secondnego, scam negotiation_phase = pony.orm.Optional(str) taker_sway = pony.orm.Optional(int) market_sway = pony.orm.Optional(int) # If negotiation_round = (calculed length) && negotiation_phase = scam then # the negotiation is no into the wrap up section # One of critfail, fail, success, critsuccess wrapup_state = pony.orm.Optional(int) job_accepted = pony.orm.Optional(bool) may_be_undercut = pony.orm.Optional(bool) undercut = pony.orm.Optional(bool) db.bind(**app.config['PONY']) db.generate_mapping(create_tables=True) Pony(app) socketio.run(app, host = bindaddr, port = port)