diff --git a/CHANGELOG.md b/CHANGELOG.md index 807de0fb..9a6107c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [1.6.6] - 2019-10-27 +# Added +- Offering of draws + - Neither player receives a point if they agree to a draw + - Bots automatically agree to draws + +## [1.6.5] - 2019-10-25 +# Fixed +- Stripe being blocked no longer causes unrecoverable error +- Automatic ready up is now throttled after abandons +- Player width styling + +# Changed +- Improved wiggle animation +- Intercept is now considered defensive by bots +- Password restrictions relaxed + ## [1.6.4] - 2019-10-24 ### Changed - Animations processing on client side reduced. diff --git a/VERSION b/VERSION index 49ebdd60..83d1a5eb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6.5 \ No newline at end of file +1.6.6 \ No newline at end of file diff --git a/acp/package.json b/acp/package.json index 2591fabe..a1935bb7 100644 --- a/acp/package.json +++ b/acp/package.json @@ -1,6 +1,6 @@ { "name": "mnml-client", - "version": "1.6.5", + "version": "1.6.6", "description": "", "main": "index.js", "scripts": { diff --git a/client/assets/styles/controls.less b/client/assets/styles/controls.less index 2ff3ee04..54bcb375 100644 --- a/client/assets/styles/controls.less +++ b/client/assets/styles/controls.less @@ -144,7 +144,15 @@ aside { &:active, &.confirming { background: @red; color: black; - border: 2px solid black; + border: 2px solid @red; + } +} + +.draw:not([disabled]) { + &:active, &.confirming { + background: @gray-hover; + color: black; + border: 2px solid @gray-hover; } } diff --git a/client/package.json b/client/package.json index 6c6d56fe..b6b1c359 100644 --- a/client/package.json +++ b/client/package.json @@ -1,12 +1,12 @@ { "name": "mnml-client", - "version": "1.6.5", + "version": "1.6.6", "description": "", "main": "index.js", "scripts": { "start": "parcel watch index.html --out-dir /var/lib/mnml/public/current", "anims": "parcel watch animations.html --no-hmr --out-dir /var/lib/mnml/public/current", - "build": "parcel build index.html", + "build": "parcel build index.html --no-source-maps", "scss": "node-sass --watch assets/scss -o assets/styles", "lint": "eslint --fix --ext .jsx src/", "test": "echo \"Error: no test specified\" && exit 1" diff --git a/client/src/components/game.ctrl.btns.jsx b/client/src/components/game.ctrl.btns.jsx index 671e9d62..3918b5ca 100644 --- a/client/src/components/game.ctrl.btns.jsx +++ b/client/src/components/game.ctrl.btns.jsx @@ -75,7 +75,7 @@ function GameCtrlBtns(args) { } = args; if (!game) return false; - const finished = game.phase === 'Finish'; + const finished = game.phase === 'Finished'; function quitClick() { getInstanceState(); diff --git a/client/src/components/game.ctrl.btns.top.jsx b/client/src/components/game.ctrl.btns.top.jsx index 2e54b54b..270721c9 100644 --- a/client/src/components/game.ctrl.btns.top.jsx +++ b/client/src/components/game.ctrl.btns.top.jsx @@ -9,15 +9,23 @@ const addState = connect( ws, game, animating, + account, } = state; function sendAbandon() { return ws.sendInstanceAbandon(game.instance); } + function sendDraw() { + return ws.sendGameOfferDraw(game.id); + } + return { game, + account, + sendAbandon, + sendDraw, animating, }; }, @@ -36,13 +44,19 @@ const addState = connect( function GameCtrlTopBtns(args) { const { game, + account, + leave, sendAbandon, + sendDraw, animating, } = args; - const finished = game && game.phase === 'Finish'; - const { abandonState } = this.state; + const finished = game && game.phase === 'Finished'; + const { abandonState, drawState } = this.state; + + const player = game.players.find(p => p.id === account.id); + const drawOffered = player && player.draw_offered; const abandonStateTrue = e => { e.stopPropagation(); @@ -50,16 +64,29 @@ function GameCtrlTopBtns(args) { setTimeout(() => this.setState({ abandonState: false }), 2000); }; + const drawStateTrue = e => { + e.stopPropagation(); + this.setState({ drawState: true }); + setTimeout(() => this.setState({ drawState: false }), 2000); + }; + const abandonClasses = `abandon ${abandonState ? 'confirming' : ''}`; const abandonText = abandonState ? 'Confirm' : 'Abandon'; const abandonAction = abandonState ? sendAbandon : abandonStateTrue; const abandonBtn = ; - const leaveBtn = ; + + const drawClasses = `draw ${drawState || drawOffered ? 'confirming' : ''}`; + const drawText = drawOffered + ? 'Offered' + : drawState ? 'Draw' : 'Offer'; + const drawAction = drawState ? sendDraw : drawStateTrue; + const drawBtn = ; return (
- {finished ? leaveBtn : abandonBtn} + {abandonBtn} + {drawBtn}
); } diff --git a/client/src/components/game.footer.jsx b/client/src/components/game.footer.jsx index 101318a4..4a1589b0 100644 --- a/client/src/components/game.footer.jsx +++ b/client/src/components/game.footer.jsx @@ -82,7 +82,7 @@ function GameFooter(props) { const now = Date.now(); const end = Date.parse(game.phase_end); const timerPct = ((now - zero) / (end - zero) * 100); - const displayPct = game.phase === 'Finish' || !game.phase_end + const displayPct = game.phase === 'Finished' || !game.phase_end ? 0 : Math.min(timerPct, 100); @@ -108,7 +108,7 @@ function GameFooter(props) { return ( ); diff --git a/client/src/components/player.box.jsx b/client/src/components/player.box.jsx index 2790d69f..6cace8a4 100644 --- a/client/src/components/player.box.jsx +++ b/client/src/components/player.box.jsx @@ -68,6 +68,11 @@ function Scoreboard(args) { }; const winner = player.score === 'Win'; + const chatText = chat + ? chat + : player.draw_offered + ? 'draw' + : '\u00A0'; if (!isPlayer) { const nameClass = `name ${player.img ? 'subscriber' : ''}`; @@ -77,7 +82,7 @@ function Scoreboard(args) {
{scoreText()}
{player.name}
-
{chat || '\u00A0'}
+
{chatText}
); } @@ -87,7 +92,7 @@ function Scoreboard(args) { return (
-
{chat || '\u00A0'}
+
{chatText}
{scoreText()}
{player.name}
diff --git a/client/src/socket.jsx b/client/src/socket.jsx index 517ee04f..85d81154 100644 --- a/client/src/socket.jsx +++ b/client/src/socket.jsx @@ -123,6 +123,11 @@ function createSocket(events) { events.setActiveSkill(null); } + function sendGameOfferDraw(gameId) { + send(['GameOfferDraw', { game_id: gameId }]); + events.setActiveSkill(null); + } + function sendGameTarget(gameId, constructId, skillId) { send(['GameTarget', { game_id: gameId, construct_id: constructId, skill_id: skillId }]); events.setActiveSkill(null); @@ -362,6 +367,7 @@ function createSocket(events) { sendGameReady, sendGameSkill, sendGameSkillClear, + sendGameOfferDraw, sendGameTarget, sendInstanceAbandon, diff --git a/ops/package.json b/ops/package.json index 40211410..ae05c7f0 100755 --- a/ops/package.json +++ b/ops/package.json @@ -1,6 +1,6 @@ { "name": "mnml-ops", - "version": "1.6.5", + "version": "1.6.6", "description": "", "main": "index.js", "scripts": { diff --git a/server/Cargo.toml b/server/Cargo.toml index 4df35fa1..c80430f6 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mnml" -version = "1.6.5" +version = "1.6.6" authors = ["ntr "] [dependencies] diff --git a/server/src/game.rs b/server/src/game.rs index 0421ab65..12a175b4 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -27,7 +27,7 @@ pub enum Phase { Start, Skill, Resolve, - Finish, + Finished, } #[derive(Debug,Clone,Serialize,Deserialize)] @@ -325,6 +325,30 @@ impl Game { return Ok(self); } + fn offer_draw(mut self, player_id: Uuid) -> Result { + if self.phase != Phase::Skill { + return Err(err_msg("game not in skill phase")); + } + + { + let player = self.player_by_id(player_id)?; + player.draw_offered = true; + } + + // bots automatically accept draws + for player in self.players.iter_mut() { + if player.bot { + player.draw_offered = true; + } + } + + if self.players.iter().all(|p| p.draw_offered) { + return Ok(self.finish()); + } + + return Ok(self); + } + fn clear_skill(&mut self, player_id: Uuid) -> Result<&mut Game, Error> { self.player_by_id(player_id)?; if self.phase != Phase::Skill { @@ -564,15 +588,18 @@ impl Game { // } pub fn finished(&self) -> bool { - self.players.iter().any(|t| t.constructs.iter().all(|c| c.is_ko())) + self.phase == Phase::Finished || self.players.iter().any(|t| t.constructs.iter().all(|c| c.is_ko())) } pub fn winner(&self) -> Option<&Player> { - self.players.iter().find(|t| t.constructs.iter().any(|c| !c.is_ko())) + match self.players.iter().any(|t| t.constructs.iter().all(|c| c.is_ko())) { + true => self.players.iter().find(|t| t.constructs.iter().any(|c| !c.is_ko())), + false => None, + } } fn finish(mut self) -> Game { - self.phase = Phase::Finish; + self.phase = Phase::Finished; // self.log.push(format!("Game finished.")); // { @@ -594,7 +621,7 @@ impl Game { } pub fn upkeep(mut self) -> Game { - if self.phase == Phase::Finish { + if self.phase == Phase::Finished { return self; } @@ -903,6 +930,15 @@ pub fn game_skill(tx: &mut Transaction, account: &Account, game_id: Uuid, constr Ok(game) } +pub fn game_offer_draw(tx: &mut Transaction, account: &Account, game_id: Uuid) -> Result { + let game = game_get(tx, game_id)? + .offer_draw(account.id)?; + + game_update(tx, &game)?; + + Ok(game) +} + pub fn game_skill_clear(tx: &mut Transaction, account: &Account, game_id: Uuid) -> Result { let mut game = game_get(tx, game_id)?; @@ -1051,7 +1087,7 @@ mod tests { game = game.resolve_phase_start(); - assert!([Phase::Skill, Phase::Finish].contains(&game.phase)); + assert!([Phase::Skill, Phase::Finished].contains(&game.phase)); return; } @@ -1117,7 +1153,7 @@ mod tests { game = game.resolve_phase_start(); assert!(!game.player_by_id(y_player.id).unwrap().constructs[0].is_stunned()); - assert!(game.phase == Phase::Finish); + assert!(game.phase == Phase::Finished); } #[test] @@ -1423,7 +1459,7 @@ mod tests { assert!(game.skill_phase_finished()); game = game.resolve_phase_start(); - assert!([Phase::Skill, Phase::Finish].contains(&game.phase)); + assert!([Phase::Skill, Phase::Finished].contains(&game.phase)); // kill a construct game.player_by_id(i_player.id).unwrap().construct_by_id(i_construct.id).unwrap().green_life.reduce(u64::max_value()); diff --git a/server/src/instance.rs b/server/src/instance.rs index 8c92a821..2e65b9d6 100644 --- a/server/src/instance.rs +++ b/server/src/instance.rs @@ -317,7 +317,13 @@ impl Instance { self.phase_start = Utc::now(); self.phase_end = self.time_control.vbox_phase_end(); + let bits = match self.rounds.len() > 0 { + true => 12 + 6 * self.rounds.len(), + false => 0, + }; + self.players.iter_mut().for_each(|p| { + p.vbox.balance_add(bits.into()); p.set_ready(false); p.vbox.fill(); }); @@ -329,28 +335,18 @@ impl Instance { } fn finish_condition(&mut self) -> bool { - // tennis - for player in self.players.iter() { - if player.score == Score::Win { - self.winner = Some(player.id); - return true; - } - } - // Game defaults to lose otherwise - if self.rounds.len() < 4 { - return false; - } - - // both players afk - if self.players.iter().all(|p| p.score == Score::Zero) { - return true; - } - - return false; + self.players.iter().any(|p| p.score == Score::Win) } pub fn finish(&mut self) -> &mut Instance { self.phase = InstancePhase::Finished; + + for player in self.players.iter() { + if player.score == Score::Win { + self.winner = Some(player.id); + } + } + self } @@ -412,38 +408,14 @@ impl Instance { } // if you don't win, you lose - // ties can happen if both players forfeit - // in this case we just finish the game and - // dock them 10k mmr - let winner_id = match game.winner() { - Some(w) => w.id, - None => return Ok(self.finish()), - }; - - let bits = 12 + 6 * self.rounds.len(); - - { - let loser = self.players.iter() - .find(|p| p.id != winner_id) - .map(|p| p.score) - .unwrap(); - + // ties can happen if both players agree to a draw + // or ticks fire and knock everybody out + if let Some(winner) = game.winner() { let winner = self.players.iter_mut() - .find(|p| p.id == winner_id) + .find(|p| p.id == winner.id) .unwrap(); - - winner.score = winner.score.add_win(&loser); - winner.vbox.balance_add(bits.into()); - } - - { - let loser = self.players.iter_mut() - .find(|p| p.id != winner_id) - .unwrap(); - - loser.score = loser.score.add_loss(); - loser.vbox.balance_add(bits.into()); - } + winner.score = winner.score.add_win(&Score::Zero); + }; if self.all_games_finished() { self.next_round(); @@ -799,7 +771,7 @@ pub fn instance_state(tx: &mut Transaction, instance_id: Uuid) -> Result Ok(RpcMessage::GameState(game_ready(&mut tx, account, id)?)), + RpcRequest::GameOfferDraw { game_id } => + Ok(RpcMessage::GameState(game_offer_draw(&mut tx, account, game_id)?)), + RpcRequest::InstancePractice {} => Ok(RpcMessage::InstanceState(instance_practice(&mut tx, account)?)),