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)?)),