redmarkets-negotiation/negotiation.py

280 lines
10 KiB
Python
Raw Normal View History

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))
nego = Negotiation(name = n, owner = user.id, taker_sway = 1,
market_sway = 6, is_first_negotiation = 1)
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)),
}
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'])
return nego.to_dict()
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 []
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)
# 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)