team setting and postgres constraint

This commit is contained in:
ntr 2019-07-29 00:11:46 +10:00
parent 9fdda7c52c
commit 8cd0f1af36
10 changed files with 121 additions and 75 deletions

View File

@ -102,7 +102,7 @@ nav h2 {
nav hr { nav hr {
margin: 1em 0; margin: 1em 0;
border-color: whitesmoke; border-color: #444;
} }
nav button { nav button {
@ -120,12 +120,12 @@ nav button.active {
} }
nav button[disabled], nav button[disabled]:hover { nav button[disabled], nav button[disabled]:hover {
color: #333333; color: #333;
text-decoration: none; text-decoration: none;
} }
nav button:hover { nav button:hover {
color: #888; color: whitesmoke;
text-decoration: underline; text-decoration: underline;
} }

View File

@ -31,9 +31,11 @@ export const setShowLog = value => ({ type: 'SET_SHOW_LOG', value });
export const setShowNav = value => ({ type: 'SET_SHOW_NAV', value }); export const setShowNav = value => ({ type: 'SET_SHOW_NAV', value });
export const setSkip = value => ({ type: 'SET_SKIP', value }); export const setSkip = value => ({ type: 'SET_SKIP', value });
export const setShop = value => ({ type: 'SET_SHOP', value }); export const setShop = value => ({ type: 'SET_SHOP', value });
export const setTeam = value => ({ type: 'SET_SELECTED_CONSTRUCTS', value: Array.from(value) });
export const setVboxHighlight = value => ({ type: 'SET_VBOX_HIGHLIGHT', value });
export const setTeam = value => ({ type: 'SET_TEAM', value: Array.from(value) });
export const setTeamSelect = value => ({ type: 'SET_TEAM_SELECT', value: Array.from(value) });
export const setVboxHighlight = value => ({ type: 'SET_VBOX_HIGHLIGHT', value });
export const setVboxSelected = value => ({ type: 'SET_VBOX_SELECTED', value }); export const setVboxSelected = value => ({ type: 'SET_VBOX_SELECTED', value });
export const setWs = value => ({ type: 'SET_WS', value }); export const setWs = value => ({ type: 'SET_WS', value });

View File

@ -5,26 +5,29 @@ const actions = require('./../actions');
const addState = connect( const addState = connect(
function receiveState(state) { function receiveState(state) {
const { team, showNav } = state; const {
teamSelect,
showNav,
ws,
} = state;
function sendAccountSetTeam() {
return ws.sendAccountSetTeam(teamSelect);
}
return { return {
team, sendAccountSetTeam,
teamSelect,
showNav, showNav,
}; };
}, },
function receiveDispatch(dispatch) { function receiveDispatch(dispatch) {
function navToList() {
dispatch(actions.setGame(null));
dispatch(actions.setInstance(null));
return dispatch(actions.setNav('list'));
}
function setShowNav(v) { function setShowNav(v) {
return dispatch(actions.setShowNav(v)); return dispatch(actions.setShowNav(v));
} }
return { return {
navToList,
setShowNav, setShowNav,
}; };
} }
@ -33,20 +36,18 @@ const addState = connect(
function TeamFooter(args) { function TeamFooter(args) {
const { const {
showNav, showNav,
team, teamSelect,
navToList, sendAccountSetTeam,
setShowNav, setShowNav,
} = args; } = args;
if (!team) return false;
return ( return (
<footer> <footer>
<button id="nav-btn" onClick={() => setShowNav(!showNav)} ></button> <button id="nav-btn" onClick={() => setShowNav(!showNav)} ></button>
<button <button
disabled={team.some(c => !c)} disabled={teamSelect.some(c => !c)}
onClick={() => navToList()}> onClick={sendAccountSetTeam}>
Confirm Set Team
</button> </button>
</footer> </footer>
); );

View File

@ -8,11 +8,9 @@ const { stringSort } = require('./../utils');
const SpawnButton = require('./spawn.button'); const SpawnButton = require('./spawn.button');
const { ConstructAvatar } = require('./construct'); const { ConstructAvatar } = require('./construct');
const idSort = stringSort('id');
const addState = connect( const addState = connect(
function receiveState(state) { function receiveState(state) {
const { ws, constructs, team } = state; const { ws, constructs, teamSelect } = state;
function sendConstructSpawn(name) { function sendConstructSpawn(name) {
return ws.sendMtxConstructSpawn(name); return ws.sendMtxConstructSpawn(name);
@ -20,27 +18,26 @@ const addState = connect(
return { return {
constructs, constructs,
team, teamSelect,
sendConstructSpawn, sendConstructSpawn,
}; };
}, },
function receiveDispatch(dispatch) { function receiveDispatch(dispatch) {
function setTeam(constructIds) { function setTeam(constructIds) {
localStorage.setItem('team', JSON.stringify(constructIds)); dispatch(actions.setTeamSelect(constructIds));
dispatch(actions.setTeam(constructIds));
} }
return { return {
setTeam, setTeam,
}; };
} }
); );
function Team(args) { function Team(args) {
const { const {
constructs, constructs,
team, teamSelect,
setTeam, setTeam,
sendConstructSpawn, sendConstructSpawn,
} = args; } = args;
@ -51,27 +48,25 @@ function Team(args) {
// so much for dumb components // so much for dumb components
function selectConstruct(id) { function selectConstruct(id) {
// remove // remove
const i = team.findIndex(sid => sid === id); const i = teamSelect.findIndex(sid => sid === id);
if (i > -1) { if (i > -1) {
team[i] = null; teamSelect[i] = null;
return setTeam(team); return setTeam(teamSelect);
} }
// window insert // window insert
const insert = team.findIndex(j => j === null); const insert = teamSelect.findIndex(j => j === null);
if (insert === -1) return setTeam([id, null, null]); if (insert === -1) return setTeam([id, null, null]);
team[insert] = id; teamSelect[insert] = id;
return setTeam(team); return setTeam(teamSelect);
} }
const constructPanels = constructs.map(construct => { const constructPanels = constructs.map(construct => {
const colour = team.indexOf(construct.id); const colour = teamSelect.indexOf(construct.id);
const selected = colour > -1; const selected = colour > -1;
const borderColour = selected ? COLOURS[colour] : '#000000'; const borderColour = selected ? COLOURS[colour] : '#000000';
// <button disabled={true} ></button>
return ( return (
<div <div
key={construct.id} key={construct.id}

View File

@ -43,7 +43,9 @@ module.exports = {
resolution: createReducer(null, 'SET_RESOLUTION'), resolution: createReducer(null, 'SET_RESOLUTION'),
skip: createReducer(false, 'SET_SKIP'), skip: createReducer(false, 'SET_SKIP'),
shop: createReducer(false, 'SET_SHOP'), shop: createReducer(false, 'SET_SHOP'),
team: createReducer([null, null, null], 'SET_SELECTED_CONSTRUCTS'),
team: createReducer([], 'SET_TEAM'),
teamSelect: createReducer([null, null, null], 'SET_TEAM_SELECT'),
vboxHighlight: createReducer([], 'SET_VBOX_HIGHLIGHT'), vboxHighlight: createReducer([], 'SET_VBOX_HIGHLIGHT'),
vboxSelected: createReducer([], 'SET_VBOX_SELECTED'), vboxSelected: createReducer([], 'SET_VBOX_SELECTED'),

View File

@ -28,7 +28,7 @@ function createSocket(events) {
// Outgoing // Outgoing
// ------------- // -------------
function send(msg) { function send(msg) {
// if (msg[0] !== 'Ping') console.log('outgoing msg', msg); if (msg[0] !== 'Ping') console.log('outgoing msg', msg);
ws.send(cbor.encode(msg)); ws.send(cbor.encode(msg));
} }
@ -46,6 +46,10 @@ function createSocket(events) {
send(['AccountInstances', {}]); send(['AccountInstances', {}]);
} }
function sendAccountSetTeam(ids) {
send(['AccountSetTeam', { ids }]);
}
function sendGameState(id) { function sendGameState(id) {
send(['GameState', { id }]); send(['GameState', { id }]);
} }
@ -108,6 +112,11 @@ function createSocket(events) {
send(['InstancePractice', {}]); send(['InstancePractice', {}]);
} }
function sendInstanceQueue() {
send(['InstancePractice', {}]);
}
function sendInstanceReady(instanceId) { function sendInstanceReady(instanceId) {
send(['InstanceReady', { instance_id: instanceId }]); send(['InstanceReady', { instance_id: instanceId }]);
} }
@ -184,6 +193,7 @@ function createSocket(events) {
InstanceState: onInstanceState, InstanceState: onInstanceState,
ItemInfo: onItemInfo, ItemInfo: onItemInfo,
Pong: onPong, Pong: onPong,
Error: errHandler,
}; };
function logout() { function logout() {
@ -208,7 +218,6 @@ function createSocket(events) {
// decode binary msg from server // decode binary msg from server
const blob = new Uint8Array(event.data); const blob = new Uint8Array(event.data);
const res = cbor.decode(blob); const res = cbor.decode(blob);
if (res.err) return errHandler(res.err);
const [msgType, params] = res; const [msgType, params] = res;
if (msgType !== 'Pong') console.log(res); if (msgType !== 'Pong') console.log(res);
@ -268,6 +277,7 @@ function createSocket(events) {
return { return {
sendAccountConstructs, sendAccountConstructs,
sendAccountInstances, sendAccountInstances,
sendAccountSetTeam,
sendGameState, sendGameState,
sendGameReady, sendGameReady,
@ -276,6 +286,7 @@ function createSocket(events) {
sendInstanceReady, sendInstanceReady,
sendInstancePractice, sendInstancePractice,
sendInstanceQueue,
sendInstanceState, sendInstanceState,
sendVboxAccept, sendVboxAccept,

View File

@ -1,5 +1,39 @@
const team_size_trigger = `
CREATE OR REPLACE FUNCTION enforce_team_size() RETURNS trigger AS $$
DECLARE
team_size INTEGER := 3;
team_count INTEGER := 0;
BEGIN
IF (TG_OP = 'UPDATE' OR TG_OP = 'INSERT') THEN
SELECT INTO team_count COUNT(id)
FROM constructs
WHERE account = NEW.account
AND team = true;
ELSIF (TG_OP = 'DELETE') THEN
SELECT INTO team_count COUNT(id)
FROM constructs
WHERE account = OLD.account
AND team = true;
END IF;
IF team_count != team_size THEN
RAISE EXCEPTION 'You must have exactly % constructs in your team.', team_size;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE CONSTRAINT TRIGGER check_team_size
AFTER INSERT OR UPDATE OR DELETE
ON constructs
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE enforce_team_size();
`;
exports.up = async knex => { exports.up = async knex => {
return knex.schema.createTable('constructs', table => { await knex.schema.createTable('constructs', table => {
table.uuid('id').primary(); table.uuid('id').primary();
table.timestamps(true, true); table.timestamps(true, true);
table.uuid('account').notNullable() table.uuid('account').notNullable()
@ -13,6 +47,10 @@ exports.up = async knex => {
.notNullable() .notNullable()
.defaultTo(false); .defaultTo(false);
}); });
await knex.raw(team_size_trigger);
return true;
}; };
exports.down = async () => {}; exports.down = async () => {};

View File

@ -175,12 +175,12 @@ pub fn debit(tx: &mut Transaction, id: Uuid, debit: i64) -> Result<Account, Erro
"; ";
let result = tx let result = tx
.query(query, &[&debit, &id])?; .query(query, &[&debit, &id])
.or(Err(err_msg("insufficient balance")))?;
let row = result.iter().next() let row = result.iter().next()
.ok_or(format_err!("account not found {:?}", id))?; .ok_or(format_err!("account not found {:?}", id))?;
let name: String = row.get(1); let name: String = row.get(1);
let db_balance: i64 = row.get(2); let db_balance: i64 = row.get(2);
let balance = u32::try_from(db_balance) let balance = u32::try_from(db_balance)
@ -301,8 +301,7 @@ pub fn account_team(tx: &mut Transaction, account: &Account) -> Result<Vec<Const
SELECT data SELECT data
FROM constructs FROM constructs
WHERE account = $1 WHERE account = $1
AND team = true AND team = true;
LIMIT 3;
"; ";
let result = tx let result = tx
@ -325,39 +324,32 @@ pub fn account_team(tx: &mut Transaction, account: &Account) -> Result<Vec<Const
} }
let mut constructs = constructs.unwrap(); let mut constructs = constructs.unwrap();
if constructs.len() != 3 {
return Err(format_err!("team not size 3 account={:?}", account));
}
constructs.sort_by_key(|c| c.id); constructs.sort_by_key(|c| c.id);
return Ok(constructs); return Ok(constructs);
} }
pub fn account_set_team(tx: &mut Transaction, account: &Account, ids: Vec<Uuid>) -> Result<Vec<Construct>, Error> { // there is a trigger constraint on the table that enforces
// exactly 3 constructs in a team
pub fn set_team(tx: &mut Transaction, account: &Account, ids: Vec<Uuid>) -> Result<Vec<Construct>, Error> {
let query = " let query = "
UPDATE constructs UPDATE constructs
SET team = false SET team =
CASE
WHEN id = ANY($2) THEN true
ELSE false
END
WHERE account = $1; WHERE account = $1;
"; ";
let updated = tx let _updated = tx
.execute(query, &[&account.id, &ids])?; .execute(query, &[&account.id, &ids])?;
if updated > 3 {
warn!("team members >3 account={:?} count={:?}", account, updated);
}
let query = "
UPDATE constructs
SET team = true
WHERE account = $1
AND id in $2
RETURNING data;
";
let updated = tx
.execute(query, &[&account.id, &ids])?;
if updated != 3 {
return Err(format_err!("could not create team of 3 account={:?} updated={:?}", account, updated));
}
account_team(tx, account) account_team(tx, account)
} }

View File

@ -6,6 +6,7 @@ use uuid::Uuid;
use failure::Error; use failure::Error;
use failure::err_msg; use failure::err_msg;
use account;
use pg::{Db}; use pg::{Db};
use construct::{Construct}; use construct::{Construct};
use game::{Game, game_state, game_skill, game_ready}; use game::{Game, game_state, game_skill, game_ready};
@ -54,6 +55,7 @@ enum RpcRequest {
AccountState {}, AccountState {},
AccountShop {}, AccountShop {},
AccountConstructs {}, AccountConstructs {},
AccountSetTeam { ids: Vec<Uuid> },
InstancePvp {}, InstancePvp {},
InstancePractice {}, InstancePractice {},
@ -100,11 +102,12 @@ pub fn receive(data: Vec<u8>, db: &Db, begin: Instant, account: &Option<Account>
RpcRequest::AccountConstructs {} => RpcRequest::AccountConstructs {} =>
Ok(RpcMessage::AccountConstructs(account_constructs(&mut tx, &account)?)), Ok(RpcMessage::AccountConstructs(account_constructs(&mut tx, &account)?)),
RpcRequest::AccountSetTeam { ids } =>
Ok(RpcMessage::AccountConstructs(account::set_team(&mut tx, &account, ids)?)),
// RpcRequest::AccountShop {} => // RpcRequest::AccountShop {} =>
// Ok(RpcMessage::AccountShop(mtx::account_shop(&mut tx, &account)?)), // Ok(RpcMessage::AccountShop(mtx::account_shop(&mut tx, &account)?)),
// RpcRequest::ConstructDelete" => handle_construct_delete(data, &mut tx, account), // RpcRequest::ConstructDelete" => handle_construct_delete(data, &mut tx, account),
RpcRequest::GameState { id } => RpcRequest::GameState { id } =>

View File

@ -83,6 +83,8 @@ impl Warden {
let db = self.pool.get()?; let db = self.pool.get()?;
let tx = db.transaction()?; let tx = db.transaction()?;
info!("received pair={:?}", pair);
Ok(()) Ok(())
} }