</template>
<script>
-// See https://stackoverflow.com/a/35417159
import ContactForm from "@/components/ContactForm.vue";
import Language from "@/components/Language.vue";
import Settings from "@/components/Settings.vue";
import UpsertUser from "@/components/UpsertUser.vue";
import { store } from "./store.js";
+import { processModalClick } from "./utils/modalClick.js";
export default {
components: {
ContactForm,
return `/images/flags/${this.st.lang}.svg`;
},
},
-// mounted: function() {
-// feather.replace();
-// },
+ mounted: function() {
+ let dialogs = document.querySelectorAll("div[role='dialog']");
+ dialogs.forEach(d => {
+ d.addEventListener("click", processModalClick);
+ });
+ },
methods: {
hideDrawer: function(e) {
if (e.target.innerText == "Forum")
</script>
<style lang="sass">
+//html, *
+// font-family: "Open Sans", Arial, sans-serif
+// --back-color: #f2f2f2
+// --a-link-color: black
+// --a-visited-color: black
+
+body
+ padding: 0
+ min-width: 320px
+
#app
font-family: "Avenir", Helvetica, Arial, sans-serif
-webkit-font-smoothing: antialiased
-moz-osx-font-smoothing: grayscale
.container
+ overflow: hidden
@media screen and (max-width: 767px)
padding: 0
.clickable
cursor: pointer
+.text-center
+ text-align: center
+
+.smallpad
+ padding: 5px
+
+.emphasis
+ font-style: italic
+
+.clearer
+ clear: both
+
+.smallfont
+ font-size: 0.8em
+
+.bigfont
+ font-size: 1.2em
+
+.bold
+ font-weight: bold
+
nav
width: 100%
margin: 0
label.drawer-close
top: 50px
+@media screen and (max-width: 767px)
+ .button-group
+ flex-direction: row
+ button:not(:first-child)
+ border-left: 1px solid var(--button-group-border-color)
+ border-top: 0
+
footer
border: 1px solid #ddd
//background-color: #000033
@media screen and (max-width: 767px)
footer
border: none
+
+//#settings, #contactForm
+// max-width: 767px
+// @media screen and (max-width: 767px)
+// max-width: 100vw
+//[type="checkbox"].modal+div .card
+// max-width: 767px
+// max-height: 100vh
+//[type="checkbox"].modal+div .card.small-modal
+// max-width: 320px
+//[type="checkbox"].modal+div .card.big-modal
+// max-width: 90vw
</style>
<template lang="pug">
-div#baseGame(tabindex=-1 @click="() => focusBg()" @keydown="handleKeys")
+div#baseGame(tabindex=-1 @click="() => focusBg()"
+ @keydown="handleKeys" @wheel="handleScroll")
input#modalEog.modal(type="checkbox")
- div(role="dialog" aria-labelledby="eogMessage")
+ div(role="dialog" data-checkbox="modalEog" aria-labelledby="eogMessage")
.card.smallpad.small-modal.text-center
label.modal-close(for="modalEog")
h3#eogMessage.section {{ endgameMessage }}
break;
}
},
+ handleScroll: function(e) {
+ if (e.deltaY < 0)
+ this.undo();
+ else if (e.deltaY > 0)
+ this.play();
+ },
re_setVariables: function() {
this.endgameMessage = "";
this.orientation = this.game.mycolor || "w"; //default orientation for observed games
padding: 0
td
text-align: left
-.clearer
- clear: both
</style>
const data = JSON.parse(msg.data);
if (data.code == "newchat") //only event at this level
{
- this.chats.unshift({msg:data.msg,
- name:data.name || "@nonymous", sid:data.from});
+ this.chats.unshift({msg:data.msg, name:data.name || "@nonymous"});
this.$emit("newchat-received"); //data not required here
}
};
methods: {
classObject: function(chat) {
return {
- "my-chatmsg": chat.sid == this.st.user.sid,
+ "my-chatmsg": chat.name == this.st.user.name,
"opp-chatmsg": this.players.some(
- p => p.sid == chat.sid && p.sid != this.st.user.sid)
+ p => p.name == chat.name && p.name != this.st.user.name)
};
},
sendChat: function() {
let chatInput = document.getElementById("inputChat");
const chatTxt = chatInput.value;
chatInput.value = "";
- const chat = {msg:chatTxt, name: this.st.user.name || "@nonymous",
- sid:this.st.user.sid};
+ const chat = {msg:chatTxt, name: this.st.user.name || "@nonymous"};
this.$emit("newchat-sent", chat); //useful for corr games
this.chats.unshift(chat);
this.st.conn.send(JSON.stringify({
<template lang="pug">
div
input#modalContact.modal(type="checkbox")
- div(role="dialog" aria-labelledby="contactTitle")
+ div(role="dialog" data-checkbox="modalContact"
+ aria-labelledby="contactTitle")
form.card.smallpad
label.modal-close(for="modalContact")
h3#contactTitle.section {{ st.tr["Contact form"] }}
<style lang="sass" scoped>
#emailSent
+ color: blue
display: none
</style>
"fr": "Français",
};
input#modalLang.modal(type="checkbox")
- div(role="dialog")
+ div(role="dialog" data-checkbox="modalLang")
#language.card
label.modal-close(for="modalLang")
form(@change="setLanguage")
<template lang="pug">
div
input#modalSettings.modal(type="checkbox")
- div(role="dialog" aria-labelledby="settingsTitle")
+ div(role="dialog" data-checkbox="modalSettings"
+ aria-labelledby="settingsTitle")
.card.smallpad(@change="updateSettings")
label.modal-close(for="modalSettings")
h3#settingsTitle.section {{ st.tr["Preferences"] }}
fieldset
- label(for="setSqSize") {{ st.tr["Square size (in pixels). 0 for 'adaptative'"] }}
+ label(for="setSqSize")
+ | {{ st.tr["Square size (in pixels). 0 for 'adaptative'"] }}
input#setSqSize(type="number" v-model="st.settings.sqSize")
fieldset
label(for="selectHints") {{ st.tr["Show move hints?"] }}
option(value="1") {{ st.tr["Moves from a square"] }}
option(value="2") {{ st.tr["Pieces which can move"] }}
fieldset
- label(for="setHighlight") {{ st.tr["Highlight squares? (Last move & checks)"] }}
+ label(for="setHighlight")
+ | {{ st.tr["Highlight squares? (Last move & checks)"] }}
input#setHighlight(type="checkbox" v-model="st.settings.highlight")
fieldset
label(for="setCoords") {{ st.tr["Show board coordinates?"] }}
<template lang="pug">
div
input#modalUser.modal(type="checkbox" @change="trySetEnterTime")
- div(role="dialog")
+ div(role="dialog" data-checkbox="modalUser")
.card
label.modal-close(for="modalUser")
h3 {{ stage }}
+++ /dev/null
-html, *
- font-family: "Open Sans", Arial, sans-serif
- --back-color: #f2f2f2
- --a-link-color: blue
- --a-visited-color: blue
-
-body
- padding: 0
- min-width: 320px
-
-.container
- padding: 0
- overflow: hidden
-
-div
- padding: 0
-
-.section-content
- *
- margin-left: auto
- margin-right: auto
- max-width: 767px
- figure.diagram-container
- max-width: 1000px
- @media screen and (max-width: 767px)
- max-width: 100%
- padding: 0 5px
-
-@media screen and (max-width: 767px)
- .button-group
- flex-direction: row
- button:not(:first-child)
- border-left: 1px solid var(--button-group-border-color)
- border-top: 0
-
-.right-menu
- float: right
- @media screen and (max-width: 767px)
- .info-container
- p
- margin-right: 5px
-
-a.right-menu
- &:link, &:visited, &:hover
- color: black
-
-#settings, #contactForm
- max-width: 767px
- @media screen and (max-width: 767px)
- max-width: 100vw
-
-#emailSent
- color: blue
- display: none
-
-a
- text-decoration: underline
-
-.text-center
- text-align: center
-
-.smallpad
- padding: 5px
-
-.emphasis
- font-style: italic
-
-.clickable
- cursor: pointer
-
-.clearer
- clear: both
-
-.red
- color: #cc3300
-
-.purple
- color: purple
-
-.smallfont
- font-size: 0.8em
-
-.bigfont
- font-size: 1.2em
-
-.bold
- font-weight: bold
-
-[type="checkbox"].modal+div .card
- max-width: 767px
- max-height: 100vh
-[type="checkbox"].modal+div .card.small-modal
- max-width: 320px
-[type="checkbox"].modal+div .card.big-modal
- max-width: 90vw
ul
li Translations: see client/src/translations/ folder
li.
- Styling: client/src/stylesheets/ and <style> part of .vue
- files in client/src/{components,views}
+ Styling: see <style> parts of .vue files
+ in client/src/{components,views}
li.
Back-end and front-end code: a lot can be improved!
Feel free to send pull requests :)
},
// TODO: also option to takeback a move ?
- update: function(gameId, obj) //chat, move, fen, clocks, score, initime, ...
+ // obj: chat, move, fen, clocks, score[Msg], initime, ...
+ update: function(gameId, obj)
{
if (Number.isInteger(gameId) || !isNaN(parseInt(gameId)))
{
move: obj.move, //may be undefined...
fen: obj.fen,
score: obj.score,
+ scoreMsg: obj.scoreMsg,
drawOffer: obj.drawOffer,
}
}
--- /dev/null
+export function processModalClick(e)
+{
+ // Close a modal when click on it but outside focused element
+ const data = e.target.dataset;
+ if (!!data.checkbox)
+ document.getElementById(data.checkbox).checked = false;
+}
<template lang="pug">
main
input#modalChat.modal(type="checkbox" @change="toggleChat")
- div(role="dialog" aria-labelledby="inputChat")
+ div#chatWrap(role="dialog" data-checkbox="modalChat"
+ aria-labelledby="inputChat")
#chat.card
label.modal-close(for="modalChat")
Chat(:players="game.players" :pastChats="game.chats"
#aboveBoard.col-sm-12.col-md-9.col-md-offset-3.col-lg-10.col-lg-offset-2
button#chatBtn(onClick="doClick('modalChat')") Chat
#actions(v-if="game.mode!='analyze' && game.score=='*'")
- button(@click="offerDraw") Draw
+ button(@click="clickDraw" :class="{['draw-' + drawOffer]: true}") Draw
button(@click="abortGame") Abort
button(@click="resign") Resign
#playersInfo
import { ppt } from "@/utils/datetime";
import { extractTime } from "@/utils/timeControl";
import { ArrayFun } from "@/utils/array";
+import { processModalClick } from "@/utils/modalClick";
export default {
name: 'my-game',
game: {players:[{name:""},{name:""}]}, //passed to BaseGame
virtualClocks: [0, 0], //initialized with true game.clocks
vr: null, //"variant rules" object initialized from FEN
- drawOffer: "", //TODO: use for button style
- people: [], //players + observers
+ drawOffer: "",
+ people: {}, //players + observers
lastate: undefined, //used if opponent send lastate before game is ready
repeat: {}, //detect position repetition
};
}, 1000);
},
},
- // TODO: redundant code with Hall.vue (related to people array)
+ // NOTE: some redundant code with Hall.vue (related to people array)
created: function() {
// Always add myself to players' list
const my = this.st.user;
- this.people.push({sid:my.sid, id:my.id, name:my.name});
+ 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
// Define socket .onmessage() and .onclose() events:
socketInit(this.loadGame);
}
},
+ mounted: function() {
+ document.getElementById("chatWrap").addEventListener(
+ "click", processModalClick);
+ },
methods: {
// O.1] Ask server for room composition:
roomInit: function() {
const name = this.game.players[index].name;
if (this.st.user.name == name)
return true;
- return this.people.some(p => p.name == name);
+ return Object.values(this.people).some(p => p.name == name);
},
socketMessageListener: function(msg) {
const data = JSON.parse(msg.data);
case "pollclients":
{
data.sockIds.forEach(sid => {
- this.people.push({sid:sid, id:0, name:""});
+ // TODO: understand clearly what happens here, problems when a
+ // game is quit, and then launch a new game from hall.
+ if (!!this.people[sid])
+ return;
+ this.$set(this.people, sid, {id:0, name:""});
// Ask only identity
this.st.conn.send(JSON.stringify({code:"askidentity", target:sid}));
});
// Request for identification: reply if I'm not anonymous
if (this.st.user.id > 0)
{
- this.st.conn.send(JSON.stringify(
- // people[0] instead of st.user to avoid sending email
- {code:"identity", user:this.people[0], target:data.from}));
+ this.st.conn.send(JSON.stringify({code:"identity",
+ user: {
+ // NOTE: decompose to avoid revealing email
+ name: this.st.user.name,
+ sid: this.st.user.sid,
+ id: this.st.user.id,
+ },
+ target:data.from}));
}
break;
}
case "identity":
{
- let player = this.people.find(p => p.sid == data.user.sid);
+ let player = this.people[data.user.sid];
// NOTE: sometimes player.id fails because player is undefined...
// Probably because the event was meant for Hall?
if (!player)
player.id = data.user.id;
player.name = data.user.name;
// Sending last state only for live games: corr games are complete
- if (this.game.type == "live" && this.game.oppsid == player.sid)
+ if (this.game.type == "live" && this.game.oppsid == data.user.sid)
{
// Send our "last state" informations to opponent
const L = this.game.moves.length;
lastMove.draw = true;
this.st.conn.send(JSON.stringify({
code: "lastate",
- target: player.sid,
+ target: data.user.sid,
state:
{
lastMove: lastMove,
this.gameOver("?", "Abort");
break;
case "draw":
- this.gameOver("1/2", "Mutual agreement");
+ this.gameOver("1/2", data.message);
break;
case "drawoffer":
this.drawOffer = "received"; //TODO: observers don't know who offered draw
break;
case "connect":
{
- this.people.push({name:"", id:0, sid:data.from});
- this.st.conn.send(JSON.stringify({code:"askidentity", target:data.from}));
+ // TODO: next condition is probably not required. See note line 150
+ if (!this.people[data.from])
+ {
+ this.$set(this.people, data.from, {name:"", id:0});
+ this.st.conn.send(JSON.stringify({code:"askidentity", target:data.from}));
+ }
break;
}
case "disconnect":
- ArrayFun.remove(this.people, p => p.sid == data.from);
+ this.$delete(this.people, data.from);
break;
}
},
this.drawOffer = "received";
}
},
- offerDraw: function() {
+ clickDraw: function() {
if (["received","threerep"].includes(this.drawOffer))
{
if (!confirm("Accept draw?"))
return;
- this.people.forEach(p => {
- if (p.sid != this.st.user.sid)
- this.st.conn.send(JSON.stringify({code:"draw", target:p.sid}));
- });
const message = (this.drawOffer == "received"
? "Mutual agreement"
: "Three repetitions");
+ Object.keys(this.people).forEach(sid => {
+ if (sid != this.st.user.sid)
+ {
+ this.st.conn.send(JSON.stringify({code:"draw",
+ message:message, target:sid}));
+ }
+ });
this.gameOver("1/2", message);
}
else if (this.drawOffer == "sent")
if (!confirm("Offer draw?"))
return;
this.drawOffer = "sent";
- this.people.forEach(p => {
- if (p.sid != this.st.user.sid)
- this.st.conn.send(JSON.stringify({code:"drawoffer", target:p.sid}));
+ Object.keys(this.people).forEach(sid => {
+ if (sid != this.st.user.sid)
+ this.st.conn.send(JSON.stringify({code:"drawoffer", target:sid}));
});
if (this.game.type == "corr")
GameStorage.update(this.gameRef.id, {drawOffer: true});
if (!confirm(this.st.tr["Terminate game?"]))
return;
this.gameOver("?", "Abort");
- this.people.forEach(p => {
- if (p.sid != this.st.user.sid)
+ Object.keys(this.people).forEach(sid => {
+ if (sid != this.st.user.sid)
{
this.st.conn.send(JSON.stringify({
code: "abort",
- target: p.sid,
+ target: sid,
}));
}
});
resign: function(e) {
if (!confirm("Resign the game?"))
return;
- this.people.forEach(p => {
- if (p.sid != this.st.user.sid)
+ Object.keys(this.people).forEach(sid => {
+ if (sid != this.st.user.sid)
{
this.st.conn.send(JSON.stringify({code:"resign",
- side:this.game.mycolor, target:p.sid}));
+ side:this.game.mycolor, target:sid}));
}
});
this.gameOver(this.game.mycolor=="w" ? "0-1" : "1-0", "Resign");
addTime = this.game.increment - elapsed/1000;
}
let sendMove = Object.assign({}, filtered_move, {addTime: addTime});
- this.people.forEach(p => {
- if (p.sid != this.st.user.sid)
+ Object.keys(this.people).forEach(sid => {
+ if (sid != this.st.user.sid)
{
this.st.conn.send(JSON.stringify({
code: "newmove",
- target: p.sid,
+ target: sid,
move: sendMove,
}));
}
return p.sid == this.st.user.sid || p.uid == this.st.user.id;
});
if (myIdx >= 0) //OK, I play in this game
- GameStorage.update(this.gameRef.id, { score: score });
+ {
+ GameStorage.update(this.gameRef.id,
+ {score: score, scoreMsg: scoreMsg});
+ }
},
},
};
#chatBtn
margin: 0 10px 0 0
+
+.draw-sent, .draw-sent:hover
+ background-color: lightyellow
+
+.draw-received, .draw-received:hover
+ background-color: lightgreen
+
+.draw-threerep, .draw-threerep:hover
+ background-color: #e4d1fc
</style>
h3#infoMessage.section
p(v-html="infoMessage")
input#modalNewgame.modal(type="checkbox")
- div(role="dialog" aria-labelledby="titleFenedit")
+ div(role="dialog" data-checkbox="modalNewgame"
+ aria-labelledby="titleFenedit")
.card.smallpad(@keyup.enter="newChallenge")
label#closeNewgame.modal-close(for="modalNewgame")
fieldset
button(@click="pdisplay='players'") Players
button(@click="pdisplay='chat'") Chat
#players(v-show="pdisplay=='players'")
- h3 Online players
- .player(v-for="p in uniquePlayers" @click="tryChallenge(p)"
- :class="{anonymous: !!p.count}"
- )
- | {{ p.name + (!!p.count ? " ("+p.count+")" : "") }}
+ p.text-center(v-for="p in uniquePlayers")
+ span(:class="{anonymous: !!p.count}")
+ | {{ (p.name || '@nonymous') + (!!p.count ? " ("+p.count+")" : "") }}
+ button.player-action(v-if="!p.count" @click="challOrWatch(p,$event)")
+ | {{ whatPlayerDoes(p) }}
#chat(v-show="pdisplay=='chat'")
Chat(:players="[]")
input#gameSection(type="radio" aria-hidden="true" name="accordion")
gdisplay: "live",
games: [],
challenges: [],
- people: [], //people in main hall
+ people: {}, //people in main hall
infoMessage: "",
newchallenge: {
fen: "",
computed: {
uniquePlayers: function() {
// Show e.g. "@nonymous (5)", and do nothing on click on anonymous
- let anonymous = {name:"@nonymous", count:0};
+ let anonymous = {name:"", count:0};
let playerList = {};
- this.people.forEach(p => {
+ Object.values(this.people).forEach(p => {
if (p.id > 0)
{
// We don't count registered users connections: either they are here or not.
if (!playerList[p.id])
- playerList[p.id] = {name: p.name, count: 0};
+ playerList[p.id] = {name: p.name};
}
else
anonymous.count++;
created: function() {
// Always add myself to players' list
const my = this.st.user;
- this.people.push({sid:my.sid, id:my.id, name:my.name});
+ this.$set(this.people, my.sid, {id:my.id, name:my.name});
// Retrieve live challenge (not older than 30 minute) if any:
const chall = JSON.parse(localStorage.getItem("challenge") || "false");
if (!!chall)
// this.st.variants might be uninitialized (variant == null)
return (!!variant ? variant.name : "");
},
- getSid: function(pname) {
- const pIdx = this.people.findIndex(pl => pl.name == pname);
- return (pIdx === -1 ? null : this.people[pIdx].sid);
- },
- getPname: function(sid) {
- const pIdx = this.people.findIndex(pl => pl.sid == sid);
- return (pIdx === -1 ? null : this.people[pIdx].name);
+ whatPlayerDoes: function(p) {
+ if (this.games.some(g => g.players.some(pl => pl.sid == p.sid)))
+ return "Playing";
+ return "Challenge"; //player is available
},
sendSomethingTo: function(to, code, obj, warnDisconnected) {
const doSend = (code, obj, sid) => {
if (!!to)
{
// Challenge with targeted players
- const targetSid = this.getSid(to);
+ const targetSid =
+ Object.keys(this.people).find(sid => this.people[sid].name == to);
if (!targetSid)
{
if (!!warnDisconnected)
else
{
// Open challenge: send to all connected players (except us)
- this.people.forEach(p => {
- if (p.sid != this.st.user.sid) //only sid is always set
- doSend(code, obj, p.sid);
+ Object.keys(this.people).forEach(sid => {
+ if (sid != this.st.user.sid)
+ doSend(code, obj, sid);
});
}
},
case "pollclients":
{
data.sockIds.forEach(sid => {
- this.people.push({sid:sid, id:0, name:""});
+ this.$set(this.people, sid, {id:0, name:""});
// Ask identity, challenges and game(s)
this.st.conn.send(JSON.stringify({code:"askidentity", target:sid}));
this.st.conn.send(JSON.stringify({code:"askchallenge", target:sid}));
// Request for identification: reply if I'm not anonymous
if (this.st.user.id > 0)
{
- this.st.conn.send(JSON.stringify(
- // people[0] instead of st.user to avoid sending email
- {code:"identity", user:this.people[0], target:data.from}));
+ this.st.conn.send(JSON.stringify({code:"identity",
+ user: {
+ // NOTE: decompose to avoid revealing email
+ name: this.st.user.name,
+ sid: this.st.user.sid,
+ id: this.st.user.id,
+ },
+ target:data.from}));
}
break;
}
+ case "identity":
+ {
+ this.$set(this.people, data.user.sid,
+ {id: data.user.id, name: data.user.name});
+ break;
+ }
case "askchallenge":
{
// Send my current live challenge (if any)
}
break;
}
- case "identity":
- {
- const pIdx = this.people.findIndex(p => p.sid == data.user.sid);
- this.people[pIdx].id = data.user.id;
- this.people[pIdx].name = data.user.name;
- break;
- }
case "challenge":
{
// Receive challenge from some player (+sid)
let newChall = data.chall;
newChall.type = this.classifyObject(data.chall);
- const pIdx = this.people.findIndex(p => p.sid == data.from);
- newChall.from = this.people[pIdx]; //may be anonymous
+ newChall.from =
+ Object.assign({sid:data.from}, this.people[data.from]);
newChall.added = Date.now(); //TODO: this is reception timestamp, not creation
newChall.vname = this.getVname(newChall.vid);
this.challenges.push(newChall);
}
case "refusechallenge":
{
- alert(this.getPname(data.from) + " declined your challenge");
+ alert(this.people[data.from].name + " declined your challenge");
ArrayFun.remove(this.challenges, c => c.id == data.cid);
break;
}
}
case "connect":
{
- this.people.push({name:"", id:0, sid:data.from});
+ this.$set(this.people, data.from, {name:"", id:0});
this.st.conn.send(JSON.stringify({code:"askidentity", target:data.from}));
this.st.conn.send(JSON.stringify({code:"askchallenge", target:data.from}));
this.st.conn.send(JSON.stringify({code:"askgame", target:data.from}));
}
case "disconnect":
{
- ArrayFun.remove(this.people, p => p.sid == data.from);
+ this.$delete(this.people, data.from);
// Also remove all challenges sent by this player:
ArrayFun.remove(this.challenges, c => c.from.sid == data.from);
// And all live games where he plays and no other opponent is online
ArrayFun.remove(this.games, g =>
g.type == "live" && (g.players.every(p => p.sid == data.from
- || !this.people.some(pl => pl.sid == p.sid))), "all");
+ || !this.people[p.sid])), "all");
break;
}
}
this.newchallenge.to = player.name;
doClick("modalNewgame");
},
+ challOrWatch: function(p, e) {
+ switch (e.target.innerHTML)
+ {
+ case "Challenge":
+ this.tryChallenge(p);
+ break;
+ case "Playing":
+ // NOTE: this search for game was already done for rendering
+ this.showGame(this.games.find(
+ g => g.players.some(pl => pl.sid == p.sid)));
+ break;
+ };
+ },
newChallenge: async function() {
const vname = this.getVname(this.newchallenge.vid);
const vModule = await import("@/variants/" + vname + ".js");
window.V = vModule.VariantRules;
+ if (!!this.newchallenge.timeControl.match(/^[0-9]+$/))
+ this.newchallenge.timeControl += "+0"; //assume minutes, no increment
const error = checkChallenge(this.newchallenge);
if (!!error)
return alert(error);
// NOTE: vname and type are redundant (can be deduced from timeControl + vid)
chall.type = ctype;
chall.vname = vname;
- chall.from = this.people[0]; //avoid sending email
+ chall.from = { //decompose to avoid revealing email
+ sid: this.st.user.sid,
+ id: this.st.user.id,
+ name: this.st.user.name,
+ };
this.challenges.push(chall);
if (ctype == "live")
localStorage.setItem("challenge", JSON.stringify(chall));
}
if (c.accepted)
{
- c.seat = this.people[0]; //== this.st.user, avoid revealing email
+ c.seat = { //again, avoid c.seat = st.user to not reveal email
+ sid: this.st.user.sid,
+ id: this.st.user.id,
+ name: this.st.user.name,
+ };
this.launchGame(c);
}
else
let target = c.from.sid; //may not be defined if corr + offline opp
if (!target)
{
- const opponent = this.people.find(p => p.id == c.from.id);
- if (!!opponent)
- target = opponent.sid
+ target = Object.keys(this.people).find(sid =>
+ this.people[sid].id == c.from.id);
}
const tryNotifyOpponent = () => {
if (!!target) //opponent is online
max-width: 100%
margin: 0;
border: none;
+.anonymous
+ font-style: italic
+button.player-action
+ margin-left: 20px
</style>
button(v-show="!gameInProgress" @click="() => startGame('auto')")
| Sample game
button(v-show="!gameInProgress" @click="() => startGame('versus')")
- | Practice!
+ | Practice
button(v-show="gameInProgress" @click="() => stopGame()")
| Stop game
button(@click="gotoAnalyze") Analyze
</script>
<style lang="sass">
+//.section-content
+// *
+// margin-left: auto
+// margin-right: auto
+// max-width: 767px
+// figure.diagram-container
+// max-width: 1000px
+// @media screen and (max-width: 767px)
+// max-width: 100%
+// padding: 0 5px
+
.warn
padding: 3px
color: red
fenStart varchar, --initial state
fen varchar, --current state
score varchar,
+ scoreMsg varchar,
timeControl varchar,
created datetime, --used only for DB cleaning
drawOffer boolean,
create table Chats (
gid integer,
name varchar,
- sid varchar,
msg varchar,
added datetime
);
* fen: varchar (current position)
* timeControl: string
* score: varchar (result)
+ * scoreMsg: varchar ("Time", "Mutual agreement"...)
* created: datetime
* drawOffer: boolean
*
* gid: game id (int)
* msg: varchar
* name: varchar
- * sid: varchar (socket ID when sending message)
* added: datetime
*/
db.serialize(function() {
// TODO: optimize queries?
let query =
+ // NOTE: g.scoreMsg can be NULL
+ // (in this case score = "*" and no reason to look at it)
"SELECT g.id, g.vid, g.fen, g.fenStart, g.timeControl, g.score, " +
- "v.name AS vname " +
+ "g.scoreMsg, v.name AS vname " +
"FROM Games g " +
"JOIN Variants v " +
" ON g.vid = v.id " +
if (!!err3)
return cb(err3);
query =
- "SELECT msg, name, sid, added " +
+ "SELECT msg, name, added " +
"FROM Chats " +
"WHERE gid = " + id;
db.all(query, (err4,chats) => {
return "Wrong FEN string";
if (!!obj.score && !obj.score.match(/^[012?*\/-]+$/))
return "Wrong characters in score";
+ if (!!obj.scoreMsg && !obj.scoreMsg.match(/^[a-zA-Z ]+$/))
+ return "Wrong characters in score message";
if (!!obj.chat)
- {
- if (!obj.chat.sid.match(/^[a-zA-Z0-9]+$/))
- return "Wrong user SID";
return UserModel.checkNameEmail({name: obj.chat.name});
- }
return "";
},
modifs += "fen = '" + obj.fen + "',";
if (!!obj.score)
modifs += "score = '" + obj.score + "',";
+ if (!!obj.scoreMsg)
+ modifs += "scoreMsg = '" + obj.scoreMsg + "',";
modifs = modifs.slice(0,-1); //remove last comma
if (modifs.length > 0)
{
if (!!obj.chat)
{
query =
- "INSERT INTO Chats (gid, msg, name, sid, added) VALUES " +
- "(" + id + ",?,'" + obj.chat.name + "','"
- + obj.chat.sid + "'," + Date.now() + ")";
+ "INSERT INTO Chats (gid, msg, name, added) VALUES ("
+ + id + ",?,'" + obj.chat.name + "'," + "," + Date.now() + ")";
db.run(query, obj.chat.msg);
}
});
break;
case "draw":
clients[obj.target].sock.send(JSON.stringify(
- {code:"draw"}));
+ {code:"draw", message:obj.message}));
break;
}
});