2020-04-19 03:38:31 +00:00
#!/usr/bin/env python3
import os
import random
import urllib
import uuid
import flask
2020-04-19 23:52:13 +00:00
import flask_socketio
2020-04-19 03:38:31 +00:00
import pony
from pony . flask import Pony
word_file = " /usr/share/dict/words "
WORDS = open ( word_file ) . read ( ) . splitlines ( )
app = flask . Flask ( __name__ )
2020-04-19 23:52:13 +00:00
socketio = flask_socketio . SocketIO ( app )
2020-04-19 03:38:31 +00:00
db = None
2020-04-19 23:52:13 +00:00
# @TODO Change this into using redis or something else for storage across
# threads etc.
session_map = { }
2020-04-19 03:38:31 +00:00
@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
2020-04-19 23:52:13 +00:00
nego = get_negotiation ( n )
if nego :
2020-04-19 03:38:31 +00:00
# @TODO Signal to the user that the negotiation already exists and they
# are being sent to it instead.
return flask . redirect ( ' /negotiations/ {} ' . format ( n ) )
2020-04-23 00:19:22 +00:00
nego = Negotiation ( name = n , owner = user . id , taker_sway = 1 ,
2020-04-23 04:25:49 +00:00
market_sway = 6 , is_first_negotiation = True ,
manual_negotiation = True , current_phase = ' marketprep ' ,
)
2020-04-19 03:38:31 +00:00
pony . orm . commit ( )
return flask . redirect ( ' /negotiations/ {} ' . format ( n ) )
@app.route ( ' /negotiations/ ' )
def negotiation_redir ( ) :
return flask . redirect ( ' / ' )
@app.route ( ' /negotiations/<negotiation> ' )
def negotiation ( negotiation ) :
r = flask . Response ( )
2020-04-19 23:52:13 +00:00
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 ) ) ,
2020-04-23 04:25:49 +00:00
' phase ' : get_negotiation_phase ( nego ) ,
2020-04-19 23:52:13 +00:00
}
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 ' ] )
2020-04-20 05:42:52 +00:00
# Update the user's negotation state with what is stored on the server
nego = get_negotiation ( json [ ' room ' ] )
2020-04-23 04:25:49 +00:00
# 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
2020-04-19 23:52:13 +00:00
def get_room_participants ( room ) :
2020-04-20 05:42:52 +00:00
if ' / ' not in socketio . server . manager . get_namespaces ( ) :
return [ ]
2020-04-20 05:49:15 +00:00
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 [ ]
2020-04-19 23:52:13 +00:00
app . logger . debug ( participants )
return participants
2020-04-20 05:42:52 +00:00
@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 ' ]
2020-04-21 02:34:04 +00:00
# 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 ' ]
2020-04-20 05:42:52 +00:00
nego . set ( * * json )
pony . orm . commit ( )
flask_socketio . emit ( ' negotiation updated ' , { * * json } , room = nego . name )
return True
2020-04-19 23:52:13 +00:00
@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 ( )
2020-04-19 03:38:31 +00:00
@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 ' )
2020-04-20 05:42:52 +00:00
# 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.
#
2020-04-19 03:38:31 +00:00
class Negotiation ( db . Entity ) :
name = pony . orm . PrimaryKey ( str )
owner = pony . orm . Required ( User )
2020-04-20 05:42:52 +00:00
client_name = pony . orm . Optional ( str )
2020-04-21 02:34:04 +00:00
negotiator_name = pony . orm . Optional ( str )
2020-04-20 05:42:52 +00:00
taker_crew_name = pony . orm . Optional ( str )
# Manual negotation
manual_negotiation = pony . orm . Optional ( bool )
2020-04-23 04:25:49 +00:00
# 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 )
2020-04-20 05:42:52 +00:00
# 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 )
2020-04-19 03:38:31 +00:00
db . bind ( * * app . config [ ' PONY ' ] )
db . generate_mapping ( create_tables = True )
Pony ( app )
2020-04-19 23:52:13 +00:00
socketio . run ( app , host = bindaddr , port = port )