Skip to content

Commit 88b77c9

Browse files
Byo-Yomi Clocks synchronised with game-state
The player's clocks are now synchronised with the game rules & state whenever the server sends a message indicating that relevant state has changed. Byo-Yomi time systems must still be implemented on the client side.
1 parent 7b75d3e commit 88b77c9

File tree

11 files changed

+368
-171
lines changed

11 files changed

+368
-171
lines changed

controllers/channels/ChallengeChannel.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,18 @@ namespace Controllers {
7575
proposalForm.komi = (gameChannel.proposal.rules.komi)? gameChannel.proposal.rules.komi : KGS.Constants.DefaultKomi;
7676
}
7777

78-
proposalForm.gameActions = this.database.gameActions[this.channelId];
78+
proposalForm.gameActions = gameChannel.actions;
7979
}
8080

8181
private submitProposal(proposalForm: Views.GameProposal) {
8282
let gameChannel = this.channel as Models.GameChannel;
83-
let actions: number = this.database.gameActions[this.channelId];
8483
let messageType: string;
8584
let nigiri: boolean;
8685
let handicap: number;
8786
let komi: number;
8887
let players: KGS.UpstreamProposalPlayer[];
8988

90-
if ((actions & Models.GameActions.ChallengeAccept) == Models.GameActions.ChallengeAccept) {
89+
if (gameChannel.hasAction(Models.GameActions.ChallengeAccept)) {
9190
messageType = KGS.Upstream._CHALLENGE_ACCEPT;
9291
nigiri = gameChannel.proposal.nigiri;
9392
handicap = gameChannel.proposal.rules.handicap;
@@ -117,9 +116,8 @@ namespace Controllers {
117116
];
118117
}
119118

120-
actions |= Models.GameActions.ChallengeSubmitted;
121-
this.database.gameActions[this.channelId] = actions;
122-
proposalForm.gameActions = actions;
119+
gameChannel.enableAction(Models.GameActions.ChallengeSubmitted);
120+
proposalForm.gameActions = gameChannel.actions;
123121

124122
let response: KGS.Upstream.ChallengeResponse = {
125123
type: messageType,

controllers/channels/GameChannel.ts

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,72 @@ namespace Controllers {
1616

1717
private initialiseBoard() {
1818
let gameChannel = this.channel as Models.GameChannel;
19-
let board = (gameChannel.size)? new Views.GoBoard(gameChannel.size) : new Views.GoBoard();
19+
let whiteUser = this.database.users[gameChannel.playerWhite];
20+
let blackUser = this.database.users[gameChannel.playerBlack];
21+
22+
let awayUser: Models.User;
23+
let homeUser: Models.User;
24+
let homeColour: Models.GameStone;
25+
if (gameChannel.playerWhite == this.database.username) {
26+
homeColour = Models.GameStone.White;
27+
}
28+
else if (gameChannel.playerBlack == this.database.username) {
29+
homeColour = Models.GameStone.Black;
30+
}
31+
else {
32+
awayUser = this.database.users[gameChannel.playerWhite];
33+
homeUser = this.database.users[gameChannel.playerBlack];
34+
homeColour = Models.GameStone.Black;
35+
36+
if (Models.User.compare(homeUser, awayUser) > 0) {
37+
let temp = awayUser;
38+
awayUser = homeUser;
39+
homeUser = temp;
40+
homeColour = Models.GameStone.White;
41+
}
42+
}
2043

44+
let board = (gameChannel.size)? new Views.GoBoard(gameChannel.size) : new Views.GoBoard();
2145
board.playCallback = (x, y) => this.tryPlay(board, x, y);
2246

2347
this.registerView(board, LayoutZone.Main, (digest?: KGS.DataDigest) => {
48+
let gameState = this.database.games[this.channelId];
2449
if ((digest == null) || (digest.gameTrees[this.channelId])) {
25-
let gameTree = this.database.games[this.channelId];
26-
if (!gameTree) {
50+
if ((!gameState) || (!gameState.tree)) {
2751
board.clear();
2852
}
2953
else {
30-
board.update(gameTree.position);
54+
board.update(gameState.tree.position);
55+
}
56+
}
57+
58+
if ((digest == null) || (digest.gameClocks[this.channelId])) {
59+
if (homeColour == Models.GameStone.White) {
60+
board.playerHome.clock.update(gameState.clockWhite);
61+
board.playerAway.clock.update(gameState.clockBlack);
62+
}
63+
else {
64+
board.playerHome.clock.update(gameState.clockBlack);
65+
board.playerAway.clock.update(gameState.clockWhite);
3166
}
3267
}
3368
});
3469
}
3570

3671
private tryPlay(board: Views.GoBoard, x: number, y: number): boolean {
37-
let actions = this.database.gameActions[this.channelId];
38-
if ((actions & Models.GameActions.Move) != Models.GameActions.Move) {
72+
let gameChannel = this.channel as Models.GameChannel;
73+
if (!gameChannel.hasAction(Models.GameActions.Move)) {
3974
console.log("move action not available");
4075
return false;
4176
}
4277

43-
let gameTree = this.database.games[this.channelId];
44-
if (!gameTree) return;
78+
let gameState = this.database.games[this.channelId];
79+
if ((!gameState) || (!gameState.tree)) return;
4580

46-
let r = gameTree.tryPlay(x, y);
81+
let r = gameState.tree.tryPlay(x, y);
4782
switch (r as Models.GameMoveError) {
4883
case Models.GameMoveError.Success:
49-
actions &= ~Models.GameActions.Move;
50-
this.database.gameActions[this.channelId] = actions;
84+
gameChannel.disableAction(Models.GameActions.Move);
5185
this.client.post(<KGS.Upstream.GAME_MOVE>{
5286
type: KGS.Upstream._GAME_MOVE,
5387
channelId: this.channelId,

kgs/DataDigest.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace KGS {
22
export class DataDigest {
33
public timestamp: Date = new Date();
4+
public perfstamp: number = performance.now();
45

56
public username: boolean = false;
67
public joinedChannelIds: boolean = false;
@@ -25,6 +26,8 @@ namespace KGS {
2526

2627
public gameTrees: { [channelId: number]: boolean } = {};
2728
public touchGameTree(channelId: number) { this.gameTrees[channelId] = true; }
29+
public gameClocks: { [channelId: number]: boolean } = {};
30+
public touchGameClocks(channelId: number) { this.gameClocks[channelId] = true; }
2831
public gameActions: { [channelId: number]: boolean } = {};
2932
public touchGameActions(channelId: number) { this.gameActions[channelId] = true; }
3033
}

kgs/Database.ts

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ namespace KGS {
1313
public users: { [name: string]: Models.User };
1414
public get userNames(): string[] { return Object.keys(this.users); }
1515

16-
public games: DatabaseDictionary<Models.GameTree>;
17-
public gameActions: { [channelId: number]: Models.GameActions };
16+
public games: DatabaseDictionary<Models.GameState>;
1817
}
1918

2019
class DatabaseInternal extends KGS.Database {
@@ -28,11 +27,8 @@ namespace KGS {
2827
this.joinedChannelIds = [];
2928

3029
this.channels = {};
31-
3230
this.users = {};
33-
3431
this.games = {};
35-
this.gameActions = {};
3632
}
3733

3834
public _createChannel(digest: KGS.DataDigest, channelId: number, channelType: Models.ChannelType): Models.Channel {
@@ -98,18 +94,20 @@ namespace KGS {
9894
}
9995
}
10096

101-
public _createGameTree(digest: KGS.DataDigest, channelId: number): Models.GameTree {
102-
let gameTree = this.games[channelId];
103-
if (!gameTree) {
97+
public _createGameState(digest: KGS.DataDigest, channelId: number): Models.GameState {
98+
let gameState = this.games[channelId];
99+
if (!gameState) {
104100
let gameChannel = this._requireChannel(channelId, Models.ChannelType.Game) as Models.GameChannel;
105-
if (gameChannel.size) gameTree = new Models.GameTree(gameChannel.size);
106-
else gameTree = new Models.GameTree();
101+
if (gameChannel.size) gameState = new Models.GameState(gameChannel.size);
102+
else gameState = new Models.GameState(digest.perfstamp);
107103

108-
this.games[channelId] = gameTree;
104+
this.games[channelId] = gameState;
109105
digest.touchGameTree(channelId);
106+
digest.touchGameClocks(channelId);
107+
digest.touchGameActions(channelId);
110108
}
111109

112-
return gameTree;
110+
return gameState;
113111
}
114112

115113
public _unjoinChannel(digest: KGS.DataDigest, channelId: number, closeChannel: boolean) {
@@ -122,7 +120,6 @@ namespace KGS {
122120
}
123121

124122
delete this.games[channelId];
125-
delete this.gameActions[channelId];
126123
}
127124
}
128125

@@ -181,6 +178,12 @@ namespace KGS {
181178
if (channel.mergeUsers(message.users)) digest.touchChannelUsers(message.channelId);
182179

183180
this.GAME_UPDATE(digest, message);
181+
182+
if (message.clocks) {
183+
let gameState = this._database._createGameState(digest, message.channelId);
184+
gameState.mergeClockStates(digest.perfstamp, message.clocks.white, message.clocks.black);
185+
digest.touchGameClocks(message.channelId);
186+
}
184187
}
185188

186189
public CHALLENGE_JOIN = (digest: KGS.DataDigest, message: KGS.Downstream.CHALLENGE_JOIN) => {
@@ -267,34 +270,30 @@ namespace KGS {
267270
}
268271

269272
public GAME_STATE = (digest: KGS.DataDigest, message: KGS.Downstream.GAME_STATE) => {
270-
let gameActions: Models.GameActions = 0;
273+
let gameChannel = this._database._requireChannel(message.channelId, Models.ChannelType.Game) as Models.GameChannel;
274+
if (gameChannel.mergeFlags(message)) digest.touchChannel(message.channelId);
275+
276+
if (message.clocks) {
277+
let gameState = this._database._createGameState(digest, message.channelId);
278+
gameState.mergeClockStates(digest.perfstamp, message.clocks.white, message.clocks.black);
279+
digest.touchGameClocks(message.channelId);
280+
}
281+
282+
let previousGameActions: number = gameChannel.actions;
283+
gameChannel.clearActions();
271284
for (let i = 0; i < message.actions.length; ++i) {
272285
let user = this._database._updateUser(digest, message.actions[i].user);
273286
if (user.name == this._database.username) {
274-
switch (message.actions[i].action) {
275-
case "MOVE": gameActions |= Models.GameActions.Move; break;
276-
case "EDIT": gameActions |= Models.GameActions.Edit; break;
277-
case "SCORE": gameActions |= Models.GameActions.Score; break;
278-
case "CHALLENGE_CREATE": gameActions |= Models.GameActions.ChallengeCreate; break;
279-
case "CHALLENGE_SETUP": gameActions |= Models.GameActions.ChallengeSetup; break;
280-
case "CHALLENGE_WAIT": gameActions |= Models.GameActions.ChallengeWait; break;
281-
case "CHALLENGE_ACCEPT": gameActions |= Models.GameActions.ChallengeAccept; break;
282-
case "CHALLENGE_SUBMITTED": gameActions |= Models.GameActions.ChallengeSubmitted; break;
283-
case "EDIT_DELAY": gameActions |= Models.GameActions.EditDelay; break;
284-
}
287+
gameChannel.enableAction(message.actions[i].action);
285288
}
286289
}
287-
288-
if (this._database.gameActions[message.channelId] != gameActions) {
289-
this._database.gameActions[message.channelId] = gameActions;
290-
digest.touchGameActions(message.channelId);
291-
}
290+
if (previousGameActions != gameChannel.actions) digest.touchGameActions(message.channelId);
292291
}
293292

294293
public GAME_UPDATE = (digest: KGS.DataDigest, message: KGS.Downstream.GAME_UPDATE) => {
295294
if ((message.sgfEvents) && (message.sgfEvents.length > 0)) {
296-
let gameTree = this._database._createGameTree(digest, message.channelId);
297-
gameTree.processEvents(...message.sgfEvents);
295+
let gameState = this._database._createGameState(digest, message.channelId);
296+
gameState.processSGFEvents(digest.perfstamp, ...message.sgfEvents);
298297
digest.touchGameTree(message.channelId);
299298
}
300299
}

kgs/protocol/Downstream.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,20 @@ namespace KGS {
153153
gameId: number
154154
}
155155

156+
export interface ClockState {
157+
paused?: boolean; // If present, the clock has been paused, e.g. because the player has left the game.
158+
running?: boolean; // If present, the clock is running. A clock is only running when it is the turn of the player who owns this clock.
159+
time: number; // The seconds left in the current period of the clock.
160+
periodsLeft?: number; // Only present for Japanese byo-yomi clocks. The number of periods left on the clock.
161+
stonesLeft?: number; // Only present for Canadian clocks. The number of stones left in the current period.
162+
}
163+
156164
export const _GAME_STATE: string = "GAME_STATE";
157165
export interface GAME_STATE extends ChannelMessage, GameFlags {
166+
clocks?: {
167+
black?: ClockState,
168+
white?: ClockState
169+
},
158170
actions: {
159171
user: KGS.User,
160172
action: "MOVE" | "EDIT" | "SCORE" | "CHALLENGE_CREATE" | "CHALLENGE_SETUP" | "CHALLENGE_WAIT" | "CHALLENGE_ACCEPT" | "CHALLENGE_SUBMITTED" | "EDIT_DELAY"

models/GameChannel.ts

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ namespace Models {
1717
playerBlack: string;
1818
challengeCreator: string;
1919

20+
actions: Models.GameActions;
21+
2022
constructor(channelId: number) {
2123
super(channelId, ChannelType.Game);
2224
}
@@ -47,17 +49,7 @@ namespace Models {
4749
if (this.mergeProposal(c.initialProposal)) touch = true;
4850
}
4951

50-
let pvt: boolean = (game.private)? true : false;
51-
if (this.restrictedPrivate != pvt) { this.restrictedPrivate = pvt; touch = true; }
52-
53-
let plus: boolean = (game.subscribers)? true : false;
54-
if (this.restrictedPlus != plus) { this.restrictedPlus = plus; touch = true; }
55-
56-
let phase: GamePhase = GamePhase.Active;
57-
if (game.paused) phase = GamePhase.Paused;
58-
if (game.adjourned) phase = GamePhase.Adjourned;
59-
if (game.over) phase = GamePhase.Concluded;
60-
if (this.phase != phase) { this.phase = phase; touch = true; }
52+
if (this.mergeFlags(game)) touch = true;
6153

6254
let playerWhite: string = ((game.players) && (game.players.white))? game.players.white.name : null;
6355
if (this.playerWhite != playerWhite) { this.playerWhite = playerWhite; touch = true; }
@@ -84,6 +76,24 @@ namespace Models {
8476
return touch;
8577
}
8678

79+
public mergeFlags(flags: KGS.GameFlags): boolean {
80+
let touch: boolean = false;
81+
82+
let pvt: boolean = (flags.private)? true : false;
83+
if (this.restrictedPrivate != pvt) { this.restrictedPrivate = pvt; touch = true; }
84+
85+
let plus: boolean = (flags.subscribers)? true : false;
86+
if (this.restrictedPlus != plus) { this.restrictedPlus = plus; touch = true; }
87+
88+
let phase: GamePhase = GamePhase.Active;
89+
if (flags.paused) phase = GamePhase.Paused;
90+
if (flags.adjourned) phase = GamePhase.Adjourned;
91+
if (flags.over) phase = GamePhase.Concluded;
92+
if (this.phase != phase) { this.phase = phase; touch = true; }
93+
94+
return touch;
95+
}
96+
8797
public mergeProposal(proposal: KGS.DownstreamProposal): boolean {
8898
if (this.gameType != GameType.Challenge) throw "Game Type is not challenge";
8999
if ((null == this.proposal) || (!Utils.valueEquals(this.proposal, proposal, Utils.ComparisonFlags.ArraysAsSets))) {
@@ -145,5 +155,55 @@ namespace Models {
145155
let sz: string = this.size.toString();
146156
return sz + "×" + sz;
147157
}
158+
159+
public clearActions(): boolean {
160+
let touch: boolean = (this.actions)? true : false;
161+
this.actions = 0;
162+
return touch;
163+
}
164+
165+
public enableAction(action: string | Models.GameActions) {
166+
if (!action) return false;
167+
168+
let a: Models.GameActions = 0;
169+
if (Utils.isString(action)) {
170+
switch (<string>action) {
171+
case "MOVE": a = Models.GameActions.Move; break;
172+
case "EDIT": a = Models.GameActions.Edit; break;
173+
case "SCORE": a = Models.GameActions.Score; break;
174+
case "CHALLENGE_CREATE": a = Models.GameActions.ChallengeCreate; break;
175+
case "CHALLENGE_SETUP": a = Models.GameActions.ChallengeSetup; break;
176+
case "CHALLENGE_WAIT": a = Models.GameActions.ChallengeWait; break;
177+
case "CHALLENGE_ACCEPT": a = Models.GameActions.ChallengeAccept; break;
178+
case "CHALLENGE_SUBMITTED": a = Models.GameActions.ChallengeSubmitted; break;
179+
case "EDIT_DELAY": a = Models.GameActions.EditDelay; break;
180+
default: throw "Unknown or Unsupported Game Action: '" + action + "'";
181+
}
182+
}
183+
else {
184+
a = <Models.GameActions>action;
185+
}
186+
187+
let union = ((this.actions)? this.actions : 0) | a;
188+
if (union != this.actions) {
189+
this.actions = union;
190+
return true;
191+
}
192+
else return false;
193+
}
194+
195+
public disableAction(action: Models.GameActions) {
196+
if (!action) return false;
197+
let removed = ((this.actions)? this.actions : 0) & ~action;
198+
if (removed != this.actions) {
199+
this.actions = removed;
200+
return true;
201+
}
202+
else return false;
203+
}
204+
205+
public hasAction(action: Models.GameActions): boolean {
206+
return (this.actions)? ((this.actions & action) == action) : false;
207+
}
148208
}
149209
}

0 commit comments

Comments
 (0)