role="dialog"
data-checkbox="modalChat"
)
- #chat.card
+ .card
label.modal-close(for="modalChat")
#participants
span {{ Object.keys(people).length + " " + st.tr["participant(s):"] }}
span.anonymous(v-if="Object.values(people).some(p => !p.name && p.id === 0)")
| + @nonymous
Chat(
+ ref="chatcomp"
:players="game.players"
:pastChats="game.chats"
:newChat="newChat"
@mychat="processChat"
@chatcleared="clearChat"
)
+ input#modalConfirm.modal(type="checkbox")
+ div#confirmDiv(role="dialog")
+ .card
+ .diagram(v-html="curDiag")
+ .button-group#buttonsConfirm
+ // onClick for acceptBtn: set dynamically
+ button.acceptBtn
+ span {{ st.tr["Validate"] }}
+ button.refuseBtn(@click="cancelMove()")
+ span {{ st.tr["Cancel"] }}
.row
#aboveBoard.col-sm-12.col-md-9.col-md-offset-3.col-lg-10.col-lg-offset-2
span.variant-cadence {{ game.cadence }}
span.variant-name {{ game.vname }}
- button#chatBtn(onClick="window.doClick('modalChat')") Chat
+ span#nextGame(
+ v-if="nextIds.length > 0"
+ @click="showNextGame()"
+ )
+ | {{ st.tr["Next_g"] }}
+ button#chatBtn.tooltip(
+ onClick="window.doClick('modalChat')"
+ aria-label="Chat"
+ )
+ img(src="/images/icons/chat.svg")
#actions(v-if="game.score=='*'")
- button(
+ button.tooltip(
@click="clickDraw()"
:class="{['draw-' + drawOffer]: true}"
+ :aria-label="st.tr['Draw']"
)
- | {{ st.tr["Draw"] }}
- button(
+ img(src="/images/icons/draw.svg")
+ button.tooltip(
v-if="!!game.mycolor"
@click="abortGame()"
+ :aria-label="st.tr['Abort']"
)
- | {{ st.tr["Abort"] }}
- button(
+ img(src="/images/icons/abort.svg")
+ button.tooltip(
v-if="!!game.mycolor"
@click="resign()"
+ :aria-label="st.tr['Resign']"
)
- | {{ st.tr["Resign"] }}
+ img(src="/images/icons/resign.svg")
+ button.tooltip(
+ v-else-if="!!game.mycolor"
+ @click="rematch()"
+ :aria-label="st.tr['Rematch']"
+ )
+ img(src="/images/icons/rematch.svg")
#playersInfo
p
span.name(:class="{connected: isConnected(0)}")
)
span.time-left {{ virtualClocks[0][0] }}
span.time-separator(v-if="!!virtualClocks[0][1]") :
- span.time-right(v-if="!!virtualClocks[0][1]") {{ virtualClocks[0][1] }}
+ span.time-right(v-if="!!virtualClocks[0][1]")
+ | {{ virtualClocks[0][1] }}
span.split-names -
span.name(:class="{connected: isConnected(1)}")
| {{ game.players[1].name || "@nonymous" }}
)
span.time-left {{ virtualClocks[1][0] }}
span.time-separator(v-if="!!virtualClocks[1][1]") :
- span.time-right(v-if="!!virtualClocks[1][1]") {{ virtualClocks[1][1] }}
+ span.time-right(v-if="!!virtualClocks[1][1]")
+ | {{ virtualClocks[1][1] }}
BaseGame(
ref="basegame"
:game="game"
import { ajax } from "@/utils/ajax";
import { extractTime } from "@/utils/timeControl";
import { getRandString } from "@/utils/alea";
+import { getDiagram } from "@/utils/printDiagram";
import { processModalClick } from "@/utils/modalClick";
-import { getFullNotation } from "@/utils/notation";
import { playMove, getFilteredMove } from "@/utils/playUndo";
import { getScoreMessage } from "@/utils/scoring";
import { ArrayFun } from "@/utils/array";
BaseGame,
Chat
},
- // gameRef: to find the game in (potentially remote) storage
data: function() {
return {
st: store.state,
id: "",
rid: ""
},
- game: {
- // Passed to BaseGame
- players: [{ name: "" }, { name: "" }],
- chats: [],
- rendered: false
- },
- virtualClocks: [[0,0], [0,0]], //initialized with true game.clocks
+ nextIds: [],
+ game: {}, //passed to BaseGame
+ // virtualClocks will be initialized from true game.clocks
+ virtualClocks: [],
vr: null, //"variant rules" object initialized from FEN
drawOffer: "",
people: {}, //players + observers
onMygames: [], //opponents (or me) on "MyGames" page
lastate: undefined, //used if opponent send lastate before game is ready
repeat: {}, //detect position repetition
+ curDiag: "", //for corr moves confirmation
newChat: "",
conn: null,
roomInitialized: false,
// If newmove got no pingback, send again:
opponentGotMove: false,
connexionString: "",
+ // Intervals from setInterval():
+ // TODO: limit them to e.g. 3 retries ?!
+ askIfPeerConnected: null,
+ askLastate: null,
+ retrySendmove: null,
+ clockUpdate: null,
// Related to (killing of) self multi-connects:
newConnect: {},
killed: {}
};
},
watch: {
- $route: function(to) {
- this.gameRef.id = to.params["id"];
- this.gameRef.rid = to.query["rid"];
- this.loadGame();
+ $route: function(to, from) {
+ if (from.params["id"] != to.params["id"]) {
+ // Change everything:
+ this.cleanBeforeDestroy();
+ this.atCreation();
+ } else {
+ // Same game ID
+ this.gameRef.id = to.params["id"];
+ this.gameRef.rid = to.query["rid"];
+ this.nextIds = JSON.parse(this.$route.query["next"] || "[]");
+ this.loadGame();
+ }
}
},
// NOTE: some redundant code with Hall.vue (mostly related to people array)
created: function() {
- // Always add myself to players' list
- const my = this.st.user;
- this.$set(this.people, my.sid, { id: my.id, name: my.name });
- this.gameRef.id = this.$route.params["id"];
- this.gameRef.rid = this.$route.query["rid"]; //may be undefined
- // Initialize connection
- this.connexionString =
- params.socketUrl +
- "/?sid=" +
- this.st.user.sid +
- "&tmpId=" +
- getRandString() +
- "&page=" +
- encodeURIComponent(this.$route.path);
- this.conn = new WebSocket(this.connexionString);
- this.conn.onmessage = this.socketMessageListener;
- this.conn.onclose = this.socketCloseListener;
- // Socket init required before loading remote game:
- const socketInit = callback => {
- if (!!this.conn && this.conn.readyState == 1)
- // 1 == OPEN state
- callback();
- else
- // Socket not ready yet (initial loading)
- // NOTE: it's important to call callback without arguments,
- // otherwise first arg is Websocket object and loadGame fails.
- this.conn.onopen = () => callback();
- };
- if (!this.gameRef.rid)
- // Game stored locally or on server
- this.loadGame(null, () => socketInit(this.roomInit));
- else
- // Game stored remotely: need socket to retrieve it
- // NOTE: the callback "roomInit" will be lost, so we don't provide it.
- // --> It will be given when receiving "fullgame" socket event.
- socketInit(this.loadGame);
+ this.atCreation();
},
mounted: function() {
document
.getElementById("chatWrap")
.addEventListener("click", processModalClick);
+ if ("ontouchstart" in window) {
+ // Disable tooltips on smartphones:
+ document.getElementsByClassName("tooltip").forEach(elt => {
+ elt.classList.remove("tooltip");
+ });
+ }
},
beforeDestroy: function() {
- this.send("disconnect");
+ this.cleanBeforeDestroy();
},
methods: {
+ atCreation: function() {
+ // 0] (Re)Set variables
+ this.gameRef.id = this.$route.params["id"];
+ // rid = remote ID to find an observed live game,
+ // next = next corr games IDs to navigate faster
+ // (Both might be undefined)
+ this.gameRef.rid = this.$route.query["rid"];
+ this.nextIds = JSON.parse(this.$route.query["next"] || "[]");
+ // Always add myself to players' list
+ const my = this.st.user;
+ this.$set(this.people, my.sid, { id: my.id, name: my.name });
+ this.game = {
+ players: [{ name: "" }, { name: "" }],
+ chats: [],
+ rendered: false
+ };
+ let chatComp = this.$refs["chatcomp"];
+ if (!!chatComp) chatComp.chats = [];
+ this.virtualClocks = [[0,0], [0,0]];
+ this.vr = null;
+ this.drawOffer = "";
+ this.onMygames = [];
+ this.lastate = undefined;
+ this.newChat = "";
+ this.roomInitialized = false;
+ this.askGameTime = 0;
+ this.gameIsLoading = false;
+ this.gotLastate = false;
+ this.gotMoveIdx = -1;
+ this.opponentGotMove = false;
+ this.askIfPeerConnected = null;
+ this.askLastate = null;
+ this.retrySendmove = null;
+ this.clockUpdate = null;
+ this.newConnect = {};
+ this.killed = {};
+ // 1] Initialize connection
+ this.connexionString =
+ params.socketUrl +
+ "/?sid=" +
+ this.st.user.sid +
+ "&tmpId=" +
+ getRandString() +
+ "&page=" +
+ // Discard potential "/?next=[...]" for page indication:
+ encodeURIComponent(this.$route.path.match(/\/game\/[a-zA-Z0-9]+/)[0]);
+ this.conn = new WebSocket(this.connexionString);
+ this.conn.onmessage = this.socketMessageListener;
+ this.conn.onclose = this.socketCloseListener;
+ // Socket init required before loading remote game:
+ const socketInit = callback => {
+ if (!!this.conn && this.conn.readyState == 1)
+ // 1 == OPEN state
+ callback();
+ else
+ // Socket not ready yet (initial loading)
+ // NOTE: it's important to call callback without arguments,
+ // otherwise first arg is Websocket object and loadGame fails.
+ this.conn.onopen = () => callback();
+ };
+ if (!this.gameRef.rid)
+ // Game stored locally or on server
+ this.loadGame(null, () => socketInit(this.roomInit));
+ else
+ // Game stored remotely: need socket to retrieve it
+ // NOTE: the callback "roomInit" will be lost, so we don't provide it.
+ // --> It will be given when receiving "fullgame" socket event.
+ socketInit(this.loadGame);
+ },
+ cleanBeforeDestroy: function() {
+ if (!!this.askIfPeerConnected)
+ clearInterval(this.askIfPeerConnected);
+ if (!!this.askLastate)
+ clearInterval(this.askLastate);
+ if (!!this.retrySendmove)
+ clearInterval(this.retrySendmove);
+ if (!!this.clockUpdate)
+ clearInterval(this.clockUpdate);
+ this.send("disconnect");
+ },
roomInit: function() {
if (!this.roomInitialized) {
// Notify the room only now that I connected, because
const player = this.game.players[index];
// Is it me ?
if (this.st.user.sid == player.sid || this.st.user.id == player.uid)
- return true;
+ // Still have to check for name (because of potential multi-accounts
+ // on same browser, although this should be rare...)
+ return (!this.st.user.name || this.st.user.name == player.name);
// Try to find a match in people:
return (
(
this.send("newchat", { data: chat });
// NOTE: anonymous chats in corr games are not stored on server (TODO?)
if (this.game.type == "corr" && this.st.user.id > 0)
- GameStorage.update(this.gameRef.id, { chat: chat });
+ this.updateCorrGame({ chat: chat });
},
clearChat: function() {
// Nothing more to do if game is live (chats not recorded)
(color == "b" && movesCount % 2 == 1);
this.send("turnchange", { target: sid, yourTurn: yourTurn });
},
+ showNextGame: function() {
+ if (this.nextIds.length == 0) return;
+ // Did I play in current game? If not, add it to nextIds list
+ if (this.game.score == "*" && this.vr.turn == this.game.mycolor)
+ this.nextIds.unshift(this.game.id);
+ const nextGid = this.nextIds.pop();
+ this.$router.push(
+ "/game/" + nextGid + "/?next=" + JSON.stringify(this.nextIds));
+ },
+ rematch: function() {
+ alert("Unimplemented yet (soon :) )");
+ // TODO: same logic as for draw, but re-click remove rematch offer (toggle)
+ },
askGameAgain: function() {
this.gameIsLoading = true;
+ const currentUrl = document.location.href;
const doAskGame = () => {
+ if (currentUrl != document.location.href) return; //page change
if (!this.gameRef.rid)
// This is my game: just reload.
this.loadGame();
else {
// Just ask fullgame again (once!), this is much simpler.
// If this fails, the user could just reload page :/
- let self = this;
- (function askIfPeerConnected() {
- if (!!self.people[self.gameRef.rid])
- self.send("askfullgame", { target: self.gameRef.rid });
- else setTimeout(askIfPeerConnected, 1000);
- })();
+ this.send("askfullgame", { target: this.gameRef.rid });
+ this.askIfPeerConnected = setInterval(
+ () => {
+ if (
+ !!this.people[this.gameRef.rid] &&
+ currentUrl != document.location.href
+ ) {
+ this.send("askfullgame", { target: this.gameRef.rid });
+ clearInterval(this.askIfPeerConnected);
+ }
+ },
+ 1000
+ );
}
};
// Delay of at least 2s between two game requests
this.game.score == "*" &&
this.game.players.some(p => p.sid == user.sid)
) {
- let self = this;
- (function askLastate() {
- self.send("asklastate", { target: user.sid });
- setTimeout(
- () => {
- // Ask until we got a reply (or opponent disconnect):
- if (!self.gotLastate && !!self.people[user.sid])
- askLastate();
- },
- 1000
- );
- })();
+ this.send("asklastate", { target: user.sid });
+ this.askLastate = setInterval(
+ () => {
+ // Ask until we got a reply (or opponent disconnect):
+ if (!this.gotLastate && !!this.people[user.sid])
+ this.send("asklastate", { target: user.sid });
+ else
+ clearInterval(this.askLastate);
+ },
+ 1000
+ );
}
}
break;
this.conn.addEventListener("message", this.socketMessageListener);
this.conn.addEventListener("close", this.socketCloseListener);
},
+ updateCorrGame: function(obj) {
+ ajax(
+ "/games",
+ "PUT",
+ {
+ gid: this.gameRef.id,
+ newObj: obj
+ }
+ );
+ },
// lastate was received, but maybe game wasn't ready yet:
processLastate: function() {
const data = this.lastate;
if (!confirm(this.st.tr["Offer draw?"])) return;
this.drawOffer = "sent";
this.send("drawoffer");
- GameStorage.update(this.gameRef.id, { drawOffer: this.game.mycolor });
+ if (this.game.type == "live") {
+ GameStorage.update(
+ this.gameRef.id,
+ { drawOffer: this.game.mycolor }
+ );
+ } else this.updateCorrGame({ drawOffer: this.game.mycolor });
}
},
abortGame: function() {
}
// NOTE: clocks in seconds, initime in milliseconds
game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of
+ game.clocks = [tc.mainTime, tc.mainTime];
const L = game.moves.length;
if (game.score == "*") {
// Set clocks + initime
- game.clocks = [tc.mainTime, tc.mainTime];
game.initime = [0, 0];
if (L >= 1) {
const gameLastupdate = game.moves[L-1].played;
// Re-load game because we missed some moves:
// artificially reset BaseGame (required if moves arrived in wrong order)
this.$refs["basegame"].re_setVariables();
+ else
+ // Initial loading:
+ this.gotMoveIdx = game.moves.length - 1;
this.re_setClocks();
this.$nextTick(() => {
this.game.rendered = true;
// Remote live game: forgetting about callback func... (TODO: design)
this.send("askfullgame", { target: this.gameRef.rid });
} else {
- // Local or corr game
+ // Local or corr game on server.
// NOTE: afterRetrieval() is never called if game not found
- GameStorage.get(this.gameRef.id, afterRetrieval);
+ const gid = this.gameRef.id;
+ if (Number.isInteger(gid) || !isNaN(parseInt(gid))) {
+ // corr games identifiers are integers
+ ajax("/games", "GET", { gid: gid }, res => {
+ let g = res.game;
+ g.moves.forEach(m => {
+ m.squares = JSON.parse(m.squares);
+ });
+ afterRetrieval(g);
+ });
+ }
+ else
+ // Local game
+ GameStorage.get(this.gameRef.id, afterRetrieval);
}
},
re_setClocks: function() {
i == colorIdx ? (Date.now() - this.game.initime[colorIdx]) / 1000 : 0;
return ppt(this.game.clocks[i] - removeTime).split(':');
});
- let clockUpdate = setInterval(() => {
+ this.clockUpdate = setInterval(() => {
if (
countdown < 0 ||
this.game.moves.length > currentMovesCount ||
this.game.score != "*"
) {
- clearInterval(clockUpdate);
+ clearInterval(this.clockUpdate);
if (countdown < 0)
this.gameOver(
currentTurn == "w" ? "0-1" : "1-0",
else this.game.clocks[colorIdx] = extractTime(this.game.cadence).mainTime;
// data.initime is set only when I receive a "lastate" move from opponent
this.game.initime[nextIdx] = data.initime || Date.now();
- this.re_setClocks();
// If repetition detected, consider that a draw offer was received:
const fenObj = this.vr.getFenForRepeat();
this.repeat[fenObj] = this.repeat[fenObj] ? this.repeat[fenObj] + 1 : 1;
if (this.repeat[fenObj] >= 3) this.drawOffer = "threerep";
else if (this.drawOffer == "threerep") this.drawOffer = "";
- // Since corr games are stored at only one location, update should be
- // done only by one player for each move:
if (!!this.game.mycolor && !data.receiveMyMove) {
// NOTE: 'var' to see that variable outside this block
var filtered_move = getFilteredMove(move);
}
+ // Since corr games are stored at only one location, update should be
+ // done only by one player for each move:
if (
!!this.game.mycolor &&
!data.receiveMyMove &&
break;
}
if (this.game.type == "corr") {
- GameStorage.update(this.gameRef.id, {
+ // corr: only move, fen and score
+ this.updateCorrGame({
fen: this.game.fen,
move: {
squares: filtered_move,
this.opponentGotMove = false;
this.send("newmove", {data: sendMove});
// If the opponent doesn't reply gotmove soon enough, re-send move:
- let retrySendmove = setInterval(
+ this.retrySendmove = setInterval(
() => {
if (this.opponentGotMove) {
- clearInterval(retrySendmove);
+ clearInterval(this.retrySendmove);
return;
}
let oppsid = this.game.players[nextIdx].sid;
}
if (!oppsid || !this.people[oppsid])
// Opponent is disconnected: he'll ask last state
- clearInterval(retrySendmove);
+ clearInterval(this.retrySendmove);
else this.send("newmove", {data: sendMove, target: oppsid});
},
1000
moveCol == this.game.mycolor &&
!data.receiveMyMove
) {
- setTimeout(() => {
- // TODO: remplacer cette confirm box par qqch de plus discret
- // (et de même pour challenge accepté / refusé)
- if (
- !confirm(
- this.st.tr["Move played:"] +
- " " +
- getFullNotation(move) +
- "\n" +
- this.st.tr["Are you sure?"]
- )
- ) {
- this.$refs["basegame"].cancelLastMove();
- return;
+ let el = document.querySelector("#buttonsConfirm > .acceptBtn");
+ // We may play several moves in a row: in case of, remove listener:
+ let elClone = el.cloneNode(true);
+ el.parentNode.replaceChild(elClone, el);
+ elClone.addEventListener(
+ "click",
+ () => {
+ document.getElementById("modalConfirm").checked = false;
+ doProcessMove();
+ if (this.st.settings.gotonext) this.showNextGame();
+ else this.re_setClocks();
}
- doProcessMove();
- // Let small time to finish drawing current move attempt:
- }, 500);
+ );
+ // PlayOnBoard is enough, and more appropriate for Synchrone Chess
+ V.PlayOnBoard(this.vr.board, move);
+ const position = this.vr.getBaseFen();
+ V.UndoOnBoard(this.vr.board, move);
+ this.curDiag = getDiagram({
+ position: position,
+ orientation: V.CanFlip ? this.game.mycolor : "w"
+ });
+ document.getElementById("modalConfirm").checked = true;
}
- else doProcessMove();
+ else {
+ doProcessMove();
+ this.re_setClocks();
+ }
+ },
+ cancelMove: function() {
+ document.getElementById("modalConfirm").checked = false;
+ this.$refs["basegame"].cancelLastMove();
},
gameOver: function(score, scoreMsg) {
this.game.score = score;
});
if (myIdx >= 0) {
// OK, I play in this game
- GameStorage.update(this.gameRef.id, {
+ const scoreObj = {
score: score,
scoreMsg: scoreMsg
- });
+ };
+ if (this.Game.type == "live")
+ GameStorage.update(this.gameRef.id, scoreObj);
+ else this.updateCorrGame(scoreObj);
// Notify the score to main Hall. TODO: only one player (currently double send)
this.send("result", { gid: this.game.id, score: score });
}
#actions
display: inline-block
margin: 0
- button
- display: inline-block
- margin: 0
+
+button
+ display: inline-block
+ margin: 0
+ display: inline-flex
+ img
+ height: 24px
+ display: flex
+ @media screen and (max-width: 767px)
+ height: 18px
@media screen and (max-width: 767px)
#aboveBoard
font-weight: bold
padding-right: 10px
+span#nextGame
+ background-color: #edda99
+ cursor: pointer
+ display: inline-block
+ margin-right: 10px
+
span.name
font-size: 1.5rem
padding: 0 3px
display: inline-block
margin: 0 15px
-#chat
+#chatWrap > .card
padding-top: 20px
max-width: 767px
- border: none;
+ border: none
-#chatBtn
- margin: 0 10px 0 0
+#confirmDiv > .card
+ max-width: 767px
+ max-height: 100%
.draw-sent, .draw-sent:hover
background-color: lightyellow
.somethingnew
background-color: #c5fefe
+
+.diagram
+ margin: 0 auto
+ max-width: 400px
+ // width: 100% required for Firefox
+ width: 100%
+
+#buttonsConfirm
+ margin: 0
+ & > button > span
+ width: 100%
+ text-align: center
+
+button.acceptBtn
+ background-color: lightgreen
+button.refuseBtn
+ background-color: red
</style>