diff --git a/CHANGELOG.md b/CHANGELOG.md index 807de0fb..009c5d41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). + +## [1.6.5] - 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/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/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 4ab0d543..4b81670c 100644 --- a/client/src/components/game.ctrl.btns.top.jsx +++ b/client/src/components/game.ctrl.btns.top.jsx @@ -14,10 +14,15 @@ const addState = connect( return ws.sendInstanceAbandon(game.instance); } + function sendDraw() { + return ws.sendGameOfferDraw(game.id); + } + return { game, sendAbandon, + sendDraw, }; }, function receiveDispatch(dispatch) { @@ -37,10 +42,11 @@ function GameCtrlTopBtns(args) { leave, sendAbandon, + sendDraw, } = args; const finished = game && game.phase === 'Finished'; - const { abandonState } = this.state; + const { abandonState, drawState } = this.state; const abandonStateTrue = e => { e.stopPropagation(); @@ -48,16 +54,27 @@ 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 ? 'confirming' : ''}`; + const drawText = 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/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/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..eec93a0d 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(); }); @@ -412,38 +418,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 +781,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)?)),