From 418867e1edf18b2f54ab00f18fd6b74ea621e828 Mon Sep 17 00:00:00 2001 From: andyzhangim <48558940+Andision@users.noreply.github.com> Date: Sat, 21 Feb 2026 00:32:39 +0800 Subject: [PATCH] Add per-turn countdown UX and timeout alert audio --- client/dist/main.js | 4 +- client/dist/main.js.map | 2 +- client/global.css | 77 +++++++++++++++++++++++++-- client/index.html | 1 + client/mp3/double.mp3 | Bin 10482 -> 16557 bytes client/mp3/timeout.mp3 | Bin 0 -> 7725 bytes client/src/index.tsx | 106 ++++++++++++++++++++++++++++++++++---- docker-compose.yml | 6 +++ server/player_and_game.py | 24 ++++++++- server/splendor.py | 22 ++++++-- 10 files changed, 220 insertions(+), 22 deletions(-) create mode 100644 client/mp3/timeout.mp3 diff --git a/client/dist/main.js b/client/dist/main.js index 395a17c..dbe9340 100644 --- a/client/dist/main.js +++ b/client/dist/main.js @@ -1,8 +1,8 @@ -!function(e){var t={};function a(s){if(t[s])return t[s].exports;var i=t[s]={i:s,l:!1,exports:{}};return e[s].call(i.exports,i,i.exports,a),i.l=!0,i.exports}a.m=e,a.c=t,a.d=function(e,t,s){a.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,t){if(1&t&&(e=a(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(a.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)a.d(s,i,function(t){return e[t]}.bind(null,i));return s},a.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(t,"a",t),t},a.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},a.p="",a(a.s=0)}([function(e,t,a){"use strict";var s=this&&this.__awaiter||function(e,t,a,s){return new(a||(a=Promise))((function(i,n){function r(e){try{l(s.next(e))}catch(e){n(e)}}function o(e){try{l(s.throw(e))}catch(e){n(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof a?t:new a((function(e){e(t)}))).then(r,o)}l((s=s.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0});const i=a(1),n=a(2);a(3);let r=e=>!1;!function(){let e=0;const t=["b","u","w","g","r"],a=t.concat(["*"]),o=["level1","level2","level3"];function l(e,t,s,n,r){return a.map(a=>{var o=a+"chip";return"*"===a&&(o="schip"),i.createElement("div",{className:"gem "+o,key:a+"_colors_"+r},i.createElement("div",{className:"bubble"},e[a]),i.createElement("div",{className:"underlay",onClick:s.bind(t,a)},n))})}function c(e,t){return e.map(e=>i.createElement(m,{key:e.uuid,noble:e,game:t}))}class d extends i.PureComponent{render(){const e=this.props.card,a=this.props.game;var s=a.buy.bind(a,e.uuid);const n=t=>{t.preventDefault(),a.reserve.bind(a)(e.uuid)};return e.color?i.createElement("div",{className:"card card-"+e.color+" card-"+e.level,id:e.uuid},i.createElement("div",{className:"reserve",onClick:n},i.createElement("img",{className:"floppy",src:"client/img/floppy.png"})),i.createElement("div",{className:"overlay",onClick:s}),i.createElement("div",{className:"underlay"},i.createElement("div",{className:"header"},i.createElement("div",{className:"color "+e.color+"gem"}),i.createElement("div",{className:"points"},e.points>0&&e.points)),i.createElement("div",{className:"costs"},t.map(t=>{if(e.cost[t]>0)return i.createElement("div",{key:e.uuid+"_cost_"+t,className:"cost "+t},e.cost[t])})))):i.createElement("div",{className:"deck "+e.level})}}class m extends i.PureComponent{render(){const e=this.props.noble,a=this.props.game,s=a.noble.bind(a,e.uuid);return i.createElement("div",{className:"noble",onClick:s,id:"noble"+e.id},i.createElement("div",{className:"side-bar"},i.createElement("div",{className:"points"},e.points>0&&e.points),i.createElement("div",{className:"requirement"},t.map(t=>{if(e.requirement[t]>0)return i.createElement("div",{key:e.uuid+"_req_"+t,className:"requires "+t},e.requirement[t])}))))}}class h extends i.PureComponent{constructor(){super(...arguments),this.state={editingName:null},this.editName=e=>{this.setState({editingName:e.target.value})},this.focusName=e=>{e.target.select()},this.submitName=()=>{this.props.game.rename(this.state.editingName),this.setState({editingName:null})},this.keypress=e=>{"Enter"===e.key&&this.submitName()}}render(){const e=this.props.game,s=this.props.pid,n=e.selectPlayer.bind(e,s),r={};a.map(e=>{r[e]={cards:0,gems:this.props.gems[e]}});const o=t.map(t=>{var a=this.props.cards[t].map(a=>(r[t].cards+=1,i.createElement("div",{key:s+"_card_"+a.uuid,className:"colorSetInner"},i.createElement(d,{key:a.uuid,card:a,game:e}))));return i.createElement("div",{key:s+"_set_"+t,className:"colorSet"},a,i.createElement("div",{className:a.length>0?"endcap":"spacer"}))}),m=a.map(e=>i.createElement("div",{className:"statSet",key:"stat"+e},i.createElement("div",{className:`stat stat${"*"===e?"y":e}`},r[e].gems+("*"==e?"":" / "+r[e].cards)),"*"===e?i.createElement(i.Fragment,null):i.createElement("div",null,i.createElement("img",{className:"labelImg",src:"client/img/labels.png"})))),h=e.props.pid===s?" you selected":"",u=e.props.pid===s?" (you)":"",p=l(this.props.gems,e,e.discard,"X",s),g=this.props.reserved?this.props.reserved.map(t=>i.createElement(d,{key:t.uuid+"_inner",card:t,game:e})):[],y=this.props.reserved?g.length:this.props.nreserved,f=c(this.props.nobles,e);return i.createElement("div",{className:"player"+h},i.createElement("div",{className:"playerHeader"},i.createElement("div",{className:"playerPoints"},this.props.points),null==this.state.editingName?i.createElement(i.Fragment,null,i.createElement("div",{className:"playerName",onClick:n},this.props.name),e.props.pid===s&&null==this.state.editingName?i.createElement("div",{className:"pencil",onClick:()=>this.setState({editingName:this.props.name})},"✏️"):i.createElement(i.Fragment,null)):i.createElement("div",{className:"playerName"},i.createElement("input",{className:"nameInput",type:"text",value:this.state.editingName,autoFocus:!0,onKeyPress:this.keypress,onFocus:this.focusName,onBlur:this.submitName,onChange:this.editName})),i.createElement("div",{className:"playerName2"},u),e.state.turn===s&&i.createElement("div",{className:"turnIndicator"},"←")),e.state.selectedPlayer===s?i.createElement("div",{className:"floater"},i.createElement("div",{className:"cards"},o),i.createElement("div",{className:"nobles"},f),i.createElement("div",{className:"gems"},p),i.createElement("div",{className:"reserveArea"},y>0&&i.createElement("div",null,i.createElement("div",{className:"reserveText"},"reserved"),i.createElement("div",{className:"reserveCards"},g)))):i.createElement("div",{className:"stats"},i.createElement("div",{className:"gem-stats"},m),i.createElement("div",{className:"reservedStat"},g)))}}class u extends i.PureComponent{render(){return i.createElement("div",null,i.createElement("div",{className:"deck "+this.props.name},i.createElement("div",{className:"remaining"},this.props.remaining),i.createElement("div",{className:"overlay"}),i.createElement("div",{className:"reserve",onClick:this.props.game.reserve.bind(this.props.game,this.props.name)},i.createElement("img",{className:"floppy",src:"client/img/floppy.png"}))),i.createElement("div",{className:"c_"+this.props.name+" face-up-cards"},i.createElement("div",{className:"cards-inner"},this.props.cards&&this.props.cards.map(e=>i.createElement(d,{key:e.uuid,card:e,game:this.props.game})))))}}class p extends i.PureComponent{constructor(){super(...arguments),this.state={players:[],gems:{},cards:{},chat:[],decks:{},nobles:[],log:[],turn:-1,winner:null,mode:"normal",error:null,selectedPlayer:-1,phase:"pregame",showChat:!1,showLog:!1,chatNotify:!1},this.isMyTurn=e=>e==this.props.pid,this.updateState=e=>{if(e.state){if(this.isMyTurn(e.state.turn)?("waiting"==this.state.mode&&(f.badge("!"),document.getElementById("notify").play()),this.setState({mode:"normal"})):this.setState({mode:"waiting"}),-1==this.state.selectedPlayer&&this.props.pid<4&&this.setState({selectedPlayer:this.props.pid}),this.setState({log:e.state.log,cards:e.state.cards,decks:e.state.decks,players:e.state.players,gems:e.state.gems,nobles:e.state.nobles,turn:e.state.turn}),null!==e.state.winner&&"postgame"!=this.state.phase&&(alert(e.state.players[e.state.winner].name+" wins!"),this.setState({phase:"postgame"})),e.chat){var t=this.state.chat;if(t&&t[t.length-1]&&e.chat[e.chat.length-1]){var a=t[t.length-1],s=e.chat[e.chat.length-1];a.msg!=s.msg&&s.pid!=this.props.pid&&(f.badge("."),document.getElementById("notify").play(),this.state.showChat||this.setState({chatNotify:!0}))}this.setState({chat:e.chat})}for(const e of document.getElementsByClassName("scroller"))e.scrollTop=e.scrollHeight}},this.loginArgs=()=>"?pid="+this.props.pid+"&uuid="+this.props.uuid,this.take=e=>{this.act("take",e)},this.discard=e=>{confirm("Are you sure you want to discard a gem?")&&this.act("discard",e)},this.selectPlayer=e=>{this.setState({selectedPlayer:e})},this.buy=e=>{this.act("buy",e)},this.reserve=e=>{this.act("reserve",e)},this.noble=e=>{this.act("noble_visit",e)},this.rename=e=>s(this,void 0,void 0,(function*(){const t=yield fetch(`/rename/${this.props.gid}/${e}${this.loginArgs()}`,{method:"POST"}),a=yield t.json();r(a)})),this.act=(e,t)=>s(this,void 0,void 0,(function*(){const a=yield fetch("/game/"+this.props.gid+"/"+e+"/"+t+this.loginArgs(),{method:"POST"}),s=yield a.json();r(s)||this.updateState(s)})),this.nextTurn=()=>s(this,void 0,void 0,(function*(){const e=yield fetch("/game/"+this.props.gid+"/next"+this.loginArgs(),{method:"POST"}),t=yield e.json();r(t)||this.updateState(t)})),this.poll=()=>s(this,void 0,void 0,(function*(){const e=yield fetch("/poll/"+this.props.gid+this.loginArgs()),t=yield e.json();r(t)||(this.updateState(t),this.poll())})),this.stat=()=>s(this,void 0,void 0,(function*(){const e=yield fetch(`/stat/${this.props.gid}${this.loginArgs()}`),t=yield e.json();r(t)||this.updateState(t)})),this.chat=e=>s(this,void 0,void 0,(function*(){const t=document.getElementById("chat-inner");if(13==e.which){const e=yield fetch("/game/"+this.props.gid+"/chat"+this.loginArgs(),{method:"POST",body:JSON.stringify({msg:t.value})});t.value="";const a=yield e.json();r(a)||this.updateState(a)}}))}componentDidMount(){this.stat(),this.poll()}render(){var e=this.state.players.map(e=>i.createElement(h,{selectedPlayer:this.state.selectedPlayer,key:e.uuid,pid:e.id,name:e.name,points:e.score,game:this,cards:e.cards,nobles:e.nobles,gems:e.gems,reserved:e.reserved,nreserved:e.reserved.length})),t=l(this.state.gems,this,this.take,"","game"),a=c(this.state.nobles,this),s=this.state.log.map((e,t)=>i.createElement("div",{key:"log-line-"+t,className:"line"},i.createElement("span",{className:"pid"},"["+e.pid+"] "),i.createElement("span",{className:"msg"},e.msg))),n=this.state.chat.map((e,t)=>i.createElement("div",{key:"chat-line-"+t,className:"line"},i.createElement("span",{className:`name name${e.pid}`},e.name+": "),i.createElement("span",{className:"msg"},e.msg))),r=o.map(e=>i.createElement(u,{key:e,game:this,name:e,cards:this.state.cards[e],remaining:this.state.decks[e]}));return i.createElement("div",null,i.createElement("div",{id:"game-board"},i.createElement("div",{id:"common-area"},i.createElement("div",{id:"noble-area",className:"split"},a),i.createElement("div",{id:"level-area",className:"split"},r),i.createElement("div",{className:"reserve-info"},i.createElement("div",{className:"reserve-info-inner"},i.createElement("div",null,"Click on card to buy, click on "),i.createElement("div",null,i.createElement("img",{className:"floppy",src:"client/img/floppy.png"})),i.createElement("div",null," to reserve."))),i.createElement("div",{id:"gem-area",className:"you"},t)),i.createElement("div",{id:"player-area"},e)),i.createElement("div",{id:"log-box",style:{bottom:this.state.showLog?-4:-514}},i.createElement("div",{className:"title",onClick:()=>this.setState({showLog:!this.state.showLog})},"::Log"),i.createElement("div",{className:"scroller"},s)),i.createElement("div",{id:"chat-box",onClick:()=>this.setState({chatNotify:!1}),style:{bottom:this.state.showChat?-4:-314}},i.createElement("div",{className:`title${this.state.chatNotify?" blinking":""}`,onClick:()=>this.setState({showChat:!this.state.showChat})},"::Chat"),i.createElement("div",{className:"scroller"},n),i.createElement("div",{id:"chat"},i.createElement("span",{id:"prompt"},">"),i.createElement("input",{id:"chat-inner",type:"text",onKeyPress:this.chat}))),this.state.turn>=0&&this.props.pid>=0&&this.props.pid<4&&i.createElement("button",{id:"pass-turn",onClick:this.nextTurn,style:{opacity:this.isMyTurn(this.state.turn)?1:.3}},"Pass turn"))}}const g=e=>i.createElement("div",{className:"error-box",style:{opacity:e.opacity}},i.createElement("div",{className:"error-box-inner"},e.error));class y extends i.PureComponent{constructor(){super(...arguments),this.state={startKey:null,loading:!0,lobby:!1,joined:!1,pid:-1,uuid:"",gid:"",gameName:"",errorOpacity:0,error:null},this.creating=!1,this.join=(e,t)=>s(this,void 0,void 0,(function*(){const a=this.readSession();if(a[e]&&!a[e].loading&&(a[e].joined||"spectate"===t))return this.setState(a[e]),void this.save();const s=yield fetch(`/${t}/${e}`,{method:"POST"}),i=yield s.json();404!==i.status?this.showError(i)||(this.setState({joined:"join"===t,loading:!1,pid:i.id,uuid:i.uuid,gid:e}),this.save()):this.createGame()})),this.showError=t=>{let a=null;return t?!!(t.error||t.result&&t.result.error)&&(a=t.error||t.result.error,404===t.status?(this.clear(),this.setState({loading:!0,joined:!1,pid:-1,uuid:""}),this.join(this.state.gid,"spectate"),!0):(this.setState({error:a,errorOpacity:1}),clearTimeout(e),e=setTimeout(()=>{this.setState({errorOpacity:0})},4e3),!0)):(a="Request failed",!0)},this.createGame=()=>s(this,void 0,void 0,(function*(){if(this.creating)return;this.creating=!0;const e=""===this.state.gid?this.state.gameName:this.state.gid,t=yield fetch(`/create/${e}`,{method:"POST"}),a=yield t.json();this.showError(a)||(history.replaceState(null,"Splendor",`/${a.game}`),this.join(a.game,"join"),this.setState({startKey:a.start,loading:!0,lobby:!1}))})),this.readSession=()=>{var e=window.localStorage.getItem("splendor");return null===e?{}:JSON.parse(e)},this.save=()=>{setTimeout(()=>{this.saveRaw(this.state)},100)},this.clear=()=>{this.saveRaw(null)},this.saveRaw=e=>{const t=this.readSession();null===e?delete t[this.state.gid]:t[this.state.gid]=e,window.localStorage.splendor=JSON.stringify(t)},this.startGame=()=>s(this,void 0,void 0,(function*(){const e=yield fetch(`/start/${this.state.gid}/${this.state.startKey}`,{method:"POST"}),t=yield e.json();this.showError(t)||(this.setState({startKey:null}),this.save())})),this.nameChange=e=>{this.setState({gameName:e.target.value})},this.keyPress=e=>{"Enter"===e.key&&this.createGame()}}componentDidMount(){if(r=this.showError,"/"===window.location.pathname)return void fetch("/suggest").then(e=>s(this,void 0,void 0,(function*(){const t=yield e.json();this.setState({lobby:!0,gameName:t.result.game,loading:!1})})));const e=window.location.pathname.substring(1);var t=this.readSession();this.setState(Object.assign(Object.assign({},t[e]),{gid:e})),this.state.loading&&this.join(e,"spectate")}render(){return this.state.loading?i.createElement("div",{id:"game"},i.createElement(g,{error:this.state.error,opacity:this.state.errorOpacity})):this.state.lobby?i.createElement("div",{className:"lobby"},i.createElement("div",{className:"main-title"},"Splendor"),i.createElement("div",{className:"desc"},"Play Splendor online with others. Enter a game name or use the suggested game name to start a game."),i.createElement("div",{className:"name"},i.createElement("input",{className:"game-name",type:"text",onChange:this.nameChange,onKeyPress:this.keyPress,value:this.state.gameName}),i.createElement("button",{onClick:this.createGame,className:"create-game"},"Create Game")),i.createElement(g,{error:this.state.error,opacity:this.state.errorOpacity})):i.createElement("div",{id:"game"},i.createElement("div",{id:"game-title"},i.createElement("div",{className:"link"},"Share this link with friends to join in or watch: ",i.createElement("a",{href:"."},`${document.location.href}`)),i.createElement("div",{className:"buttons"},!this.state.joined&&i.createElement("button",{className:"start-game",onClick:()=>this.join(this.state.gid,"join")},"Join Game"),this.state.startKey&&0==this.state.pid&&i.createElement("button",{className:"start-game",onClick:this.startGame},"Start Game"))),this.state.pid>=0&&this.state.gid&&this.state.uuid&&i.createElement(p,{key:this.state.pid,gid:this.state.gid,pid:this.state.pid,uuid:this.state.uuid}),i.createElement(g,{error:this.state.error,opacity:this.state.errorOpacity}))}}const f=new Favico({position:"up"});document.onclick=()=>{f.badge("")},n.render(i.createElement("div",null,i.createElement(y,null)),document.getElementById("content"))}()},function(e,t){e.exports=React},function(e,t){e.exports=ReactDOM},function(e,t,a){var s; +!function(e){var t={};function s(a){if(t[a])return t[a].exports;var i=t[a]={i:a,l:!1,exports:{}};return e[a].call(i.exports,i,i.exports,s),i.l=!0,i.exports}s.m=e,s.c=t,s.d=function(e,t,a){s.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:a})},s.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},s.t=function(e,t){if(1&t&&(e=s(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var a=Object.create(null);if(s.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)s.d(a,i,function(t){return e[t]}.bind(null,i));return a},s.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return s.d(t,"a",t),t},s.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},s.p="",s(s.s=0)}([function(e,t,s){"use strict";var a=this&&this.__awaiter||function(e,t,s,a){return new(s||(s=Promise))((function(i,n){function r(e){try{l(a.next(e))}catch(e){n(e)}}function o(e){try{l(a.throw(e))}catch(e){n(e)}}function l(e){var t;e.done?i(e.value):(t=e.value,t instanceof s?t:new s((function(e){e(t)}))).then(r,o)}l((a=a.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0});const i=s(1),n=s(2);s(3);let r=e=>!1;!function(){let e=0;const t=["b","u","w","g","r"],s=t.concat(["*"]),o=["level1","level2","level3"];function l(e,t,a,n,r){return s.map(s=>{var o=s+"chip";"*"===s&&(o="schip");const l="*"!==s;return i.createElement("div",{className:"gem "+o+(l?" gem-clickable":" gem-disabled"),key:s+"_colors_"+r,onClick:l?a.bind(t,s):void 0},i.createElement("div",{className:"bubble"},e[s]),i.createElement("div",{className:"underlay"},l?n:""))})}function c(e,t){return e.map(e=>i.createElement(m,{key:e.uuid,noble:e,game:t}))}class d extends i.PureComponent{render(){const e=this.props.card,s=this.props.game;var a=s.buy.bind(s,e.uuid);const n=t=>{t.preventDefault(),s.reserve.bind(s)(e.uuid)};return e.color?i.createElement("div",{className:"card card-"+e.color+" card-"+e.level,id:e.uuid},i.createElement("div",{className:"reserve",onClick:n},i.createElement("img",{className:"floppy",src:"client/img/floppy.png"})),i.createElement("div",{className:"overlay",onClick:a}),i.createElement("div",{className:"underlay"},i.createElement("div",{className:"header"},i.createElement("div",{className:"color "+e.color+"gem"}),i.createElement("div",{className:"points"},e.points>0&&e.points)),i.createElement("div",{className:"costs"},t.map(t=>{if(e.cost[t]>0)return i.createElement("div",{key:e.uuid+"_cost_"+t,className:"cost "+t},e.cost[t])})))):i.createElement("div",{className:"deck "+e.level})}}class m extends i.PureComponent{render(){const e=this.props.noble,s=this.props.game,a=s.noble.bind(s,e.uuid);return i.createElement("div",{className:"noble",onClick:a,id:"noble"+e.id},i.createElement("div",{className:"side-bar"},i.createElement("div",{className:"points"},e.points>0&&e.points),i.createElement("div",{className:"requirement"},t.map(t=>{if(e.requirement[t]>0)return i.createElement("div",{key:e.uuid+"_req_"+t,className:"requires "+t},e.requirement[t])}))))}}class h extends i.PureComponent{constructor(){super(...arguments),this.state={editingName:null},this.editName=e=>{this.setState({editingName:e.target.value})},this.focusName=e=>{e.target.select()},this.submitName=()=>{this.props.game.rename(this.state.editingName),this.setState({editingName:null})},this.keypress=e=>{"Enter"===e.key&&this.submitName()}}render(){const e=this.props.game,a=this.props.pid,n=e.selectPlayer.bind(e,a),r={};s.map(e=>{r[e]={cards:0,gems:this.props.gems[e]}});const o=t.map(t=>{var s=this.props.cards[t].map(s=>(r[t].cards+=1,i.createElement("div",{key:a+"_card_"+s.uuid,className:"colorSetInner"},i.createElement(d,{key:s.uuid,card:s,game:e}))));return i.createElement("div",{key:a+"_set_"+t,className:"colorSet"},s,i.createElement("div",{className:s.length>0?"endcap":"spacer"}))}),m=s.map(e=>i.createElement("div",{className:"statSet",key:"stat"+e},i.createElement("div",{className:"stat stat"+("*"===e?"y":e)},r[e].gems+("*"==e?"":" / "+r[e].cards)),"*"===e?i.createElement(i.Fragment,null):i.createElement("div",null,i.createElement("img",{className:"labelImg",src:"client/img/labels.png"})))),h=e.props.pid===a?" you selected":"",u=e.props.pid===a?" (you)":"",p=l(this.props.gems,e,e.discard,"X",a),g=this.props.reserved?this.props.reserved.map(t=>i.createElement(d,{key:t.uuid+"_inner",card:t,game:e})):[],y=this.props.reserved?g.length:this.props.nreserved,f=c(this.props.nobles,e);return i.createElement("div",{className:"player"+h},i.createElement("div",{className:"playerHeader"},i.createElement("div",{className:"playerPoints"},this.props.points),null==this.state.editingName?i.createElement(i.Fragment,null,i.createElement("div",{className:"playerName",onClick:n},this.props.name),e.props.pid===a&&null==this.state.editingName?i.createElement("div",{className:"pencil",onClick:()=>this.setState({editingName:this.props.name})},"✏️"):i.createElement(i.Fragment,null)):i.createElement("div",{className:"playerName"},i.createElement("input",{className:"nameInput",type:"text",value:this.state.editingName,autoFocus:!0,onKeyPress:this.keypress,onFocus:this.focusName,onBlur:this.submitName,onChange:this.editName})),i.createElement("div",{className:"playerName2"},u),e.state.turn===a&&i.createElement("div",{className:"turnIndicator"},"←")),e.state.selectedPlayer===a?i.createElement("div",{className:"floater"},i.createElement("div",{className:"cards"},o),i.createElement("div",{className:"nobles"},f),i.createElement("div",{className:"gems"},p),i.createElement("div",{className:"reserveArea"},y>0&&i.createElement("div",null,i.createElement("div",{className:"reserveText"},"reserved"),i.createElement("div",{className:"reserveCards"},g)))):i.createElement("div",{className:"stats"},i.createElement("div",{className:"gem-stats"},m),i.createElement("div",{className:"reservedStat"},g)))}}class u extends i.PureComponent{render(){return i.createElement("div",null,i.createElement("div",{className:"deck "+this.props.name},i.createElement("div",{className:"remaining"},this.props.remaining),i.createElement("div",{className:"overlay"}),i.createElement("div",{className:"reserve",onClick:this.props.game.reserve.bind(this.props.game,this.props.name)},i.createElement("img",{className:"floppy",src:"client/img/floppy.png"}))),i.createElement("div",{className:"c_"+this.props.name+" face-up-cards"},i.createElement("div",{className:"cards-inner"},this.props.cards&&this.props.cards.map(e=>i.createElement(d,{key:e.uuid,card:e,game:this.props.game})))))}}class p extends i.PureComponent{constructor(){super(...arguments),this.ticker=null,this.timeoutAudio=null,this.state={players:[],gems:{},cards:{},chat:[],decks:{},nobles:[],log:[],turn:-1,winner:null,mode:"normal",error:null,selectedPlayer:-1,phase:"pregame",showChat:!1,showLog:!1,chatNotify:!1,turn_seconds:30,turn_deadline:null,nowMs:Date.now()},this.isMyTurn=e=>e==this.props.pid,this.updateState=e=>{if(e.state){if(this.isMyTurn(e.state.turn)?("waiting"==this.state.mode&&(f.badge("!"),document.getElementById("notify").play()),this.setState({mode:"normal"})):this.setState({mode:"waiting"}),-1==this.state.selectedPlayer&&this.props.pid<4&&this.setState({selectedPlayer:this.props.pid}),this.setState({log:e.state.log,cards:e.state.cards,decks:e.state.decks,players:e.state.players,gems:e.state.gems,nobles:e.state.nobles,turn:e.state.turn,turn_seconds:e.state.turn_seconds,turn_deadline:e.state.turn_deadline,nowMs:Date.now()}),null!==e.state.winner&&"postgame"!=this.state.phase&&(alert(e.state.players[e.state.winner].name+" wins!"),this.setState({phase:"postgame"})),e.chat){var t=this.state.chat;if(t&&t[t.length-1]&&e.chat[e.chat.length-1]){var s=t[t.length-1],a=e.chat[e.chat.length-1];s.msg!=a.msg&&a.pid!=this.props.pid&&(f.badge("."),document.getElementById("notify").play(),this.state.showChat||this.setState({chatNotify:!0}))}this.setState({chat:e.chat})}for(const e of document.getElementsByClassName("scroller"))e.scrollTop=e.scrollHeight}},this.loginArgs=()=>"?pid="+this.props.pid+"&uuid="+this.props.uuid,this.take=e=>{this.act("take",e)},this.discard=e=>{confirm("Are you sure you want to discard a gem?")&&this.act("discard",e)},this.selectPlayer=e=>{this.setState({selectedPlayer:e})},this.buy=e=>{this.act("buy",e)},this.reserve=e=>{this.act("reserve",e)},this.noble=e=>{this.act("noble_visit",e)},this.rename=e=>a(this,void 0,void 0,(function*(){const t=yield fetch(`/rename/${this.props.gid}/${e}${this.loginArgs()}`,{method:"POST"}),s=yield t.json();r(s)})),this.act=(e,t)=>a(this,void 0,void 0,(function*(){const s=yield fetch("/game/"+this.props.gid+"/"+e+"/"+t+this.loginArgs(),{method:"POST"}),a=yield s.json();r(a)||this.updateState(a)})),this.nextTurn=()=>a(this,void 0,void 0,(function*(){const e=yield fetch("/game/"+this.props.gid+"/next"+this.loginArgs(),{method:"POST"}),t=yield e.json();r(t)||this.updateState(t)})),this.poll=()=>a(this,void 0,void 0,(function*(){const e=yield fetch("/poll/"+this.props.gid+this.loginArgs()),t=yield e.json();r(t)||(this.updateState(t),this.poll())})),this.stat=()=>a(this,void 0,void 0,(function*(){const e=yield fetch(`/stat/${this.props.gid}${this.loginArgs()}`),t=yield e.json();r(t)||this.updateState(t)})),this.turnRemaining=()=>{if(this.state.turn<0||!this.state.turn_deadline)return null;const e=Math.ceil(this.state.turn_deadline-this.state.nowMs/1e3);return Math.max(0,e)},this.stopTimeoutAudio=()=>{this.timeoutAudio&&(this.timeoutAudio.pause(),this.timeoutAudio.currentTime=0)},this.syncTimeoutAudio=()=>{if(!this.timeoutAudio)return;const e=this.turnRemaining();null!==e&&e>0&&e<=10&&this.state.turn===this.props.pid?this.timeoutAudio.paused&&this.timeoutAudio.play().catch(()=>{}):this.stopTimeoutAudio()},this.chat=e=>a(this,void 0,void 0,(function*(){const t=document.getElementById("chat-inner");if(13==e.which){const e=yield fetch("/game/"+this.props.gid+"/chat"+this.loginArgs(),{method:"POST",body:JSON.stringify({msg:t.value})});t.value="";const s=yield e.json();r(s)||this.updateState(s)}}))}componentDidMount(){this.stat(),this.poll(),this.ticker=window.setInterval(()=>this.setState({nowMs:Date.now()}),1e3),this.timeoutAudio=document.getElementById("timeout"),this.timeoutAudio&&(this.timeoutAudio.loop=!0)}componentDidUpdate(){this.syncTimeoutAudio()}componentWillUnmount(){null!==this.ticker&&window.clearInterval(this.ticker),this.stopTimeoutAudio()}render(){var e=this.state.players.map(e=>i.createElement(h,{selectedPlayer:this.state.selectedPlayer,key:e.uuid,pid:e.id,name:e.name,points:e.score,game:this,cards:e.cards,nobles:e.nobles,gems:e.gems,reserved:e.reserved,nreserved:e.reserved.length})),t=l(this.state.gems,this,this.take,"","game"),s=c(this.state.nobles,this),a=this.state.log.map((e,t)=>i.createElement("div",{key:"log-line-"+t,className:"line"},i.createElement("span",{className:"pid"},"["+e.pid+"] "),i.createElement("span",{className:"msg"},e.msg))),n=this.state.chat.map((e,t)=>i.createElement("div",{key:"chat-line-"+t,className:"line"},i.createElement("span",{className:"name name"+e.pid},e.name+": "),i.createElement("span",{className:"msg"},e.msg))),r=o.map(e=>i.createElement(u,{key:e,game:this,name:e,cards:this.state.cards[e],remaining:this.state.decks[e]}));const d=this.turnRemaining();return i.createElement("div",null,i.createElement("div",{id:"game-board"},i.createElement("div",{id:"common-area"},i.createElement("div",{id:"noble-area",className:"split"},s),i.createElement("div",{id:"level-area",className:"split"},r),i.createElement("div",{className:"reserve-info"},i.createElement("div",{className:"reserve-info-inner"},i.createElement("div",null,"Click on card to buy, click on "),i.createElement("div",null,i.createElement("img",{className:"floppy",src:"client/img/floppy.png"})),i.createElement("div",null," to reserve."))),i.createElement("div",{id:"gem-area",className:"you"},t)),i.createElement("div",{id:"player-area"},e)),i.createElement("div",{id:"log-box",style:{bottom:this.state.showLog?-4:-514}},i.createElement("div",{className:"title",onClick:()=>this.setState({showLog:!this.state.showLog})},"::Log"),i.createElement("div",{className:"scroller"},a)),i.createElement("div",{id:"chat-box",onClick:()=>this.setState({chatNotify:!1}),style:{bottom:this.state.showChat?-4:-314}},i.createElement("div",{className:"title"+(this.state.chatNotify?" blinking":""),onClick:()=>this.setState({showChat:!this.state.showChat})},"::Chat"),i.createElement("div",{className:"scroller"},n),i.createElement("div",{id:"chat"},i.createElement("span",{id:"prompt"},">"),i.createElement("input",{id:"chat-inner",type:"text",onKeyPress:this.chat}))),this.state.turn>=0&&this.props.pid>=0&&this.props.pid<4&&i.createElement("div",{id:"turn-controls"},null!==d&&i.createElement("div",{id:"turn-timer"},this.state.turn===this.props.pid?"Your turn":"Turn",": ",d,"s"),i.createElement("button",{id:"pass-turn",onClick:this.nextTurn,style:{opacity:this.isMyTurn(this.state.turn)?1:.3}},"Pass turn")))}}const g=e=>i.createElement("div",{className:"error-box",style:{opacity:e.opacity}},i.createElement("div",{className:"error-box-inner"},e.error));class y extends i.PureComponent{constructor(){super(...arguments),this.state={startKey:null,loading:!0,lobby:!1,joined:!1,pid:-1,uuid:"",gid:"",gameName:"",turnSecondsInput:"30",errorOpacity:0,error:null},this.creating=!1,this.join=(e,t)=>a(this,void 0,void 0,(function*(){const s=this.readSession();if(s[e]&&!s[e].loading&&(s[e].joined||"spectate"===t))return this.setState(s[e]),void this.save();const a=yield fetch(`/${t}/${e}`,{method:"POST"}),i=yield a.json();404!==i.status?this.showError(i)||(this.setState({joined:"join"===t,loading:!1,pid:i.id,uuid:i.uuid,gid:e}),this.save()):this.createGame()})),this.showError=t=>{let s=null;return t?!!(t.error||t.result&&t.result.error)&&(s=t.error||t.result.error,404===t.status?(this.clear(),this.setState({loading:!0,joined:!1,pid:-1,uuid:""}),this.join(this.state.gid,"spectate"),!0):(this.setState({error:s,errorOpacity:1}),clearTimeout(e),e=setTimeout(()=>{this.setState({errorOpacity:0})},4e3),!0)):(s="Request failed",!0)},this.createGame=()=>a(this,void 0,void 0,(function*(){if(this.creating)return;this.creating=!0;const e=""===this.state.gid?this.state.gameName:this.state.gid,t=parseInt((this.state.turnSecondsInput||"").trim(),10),s=Number.isFinite(t)&&t>0?t:30,a=yield fetch("/create/"+e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({turnSeconds:s})}),i=yield a.json();this.showError(i)||(history.replaceState(null,"Splendor","/"+i.game),this.join(i.game,"join"),this.setState({startKey:i.start,loading:!0,lobby:!1}))})),this.readSession=()=>{var e=window.localStorage.getItem("splendor");return null===e?{}:JSON.parse(e)},this.save=()=>{setTimeout(()=>{this.saveRaw(this.state)},100)},this.clear=()=>{this.saveRaw(null)},this.saveRaw=e=>{const t=this.readSession();null===e?delete t[this.state.gid]:t[this.state.gid]=e,window.localStorage.splendor=JSON.stringify(t)},this.startGame=()=>a(this,void 0,void 0,(function*(){const e=yield fetch(`/start/${this.state.gid}/${this.state.startKey}`,{method:"POST"}),t=yield e.json();this.showError(t)||(this.setState({startKey:null}),this.save())})),this.nameChange=e=>{this.setState({gameName:e.target.value})},this.turnSecondsChange=e=>{this.setState({turnSecondsInput:e.target.value})},this.keyPress=e=>{"Enter"===e.key&&this.createGame()}}componentDidMount(){if(r=this.showError,"/"===window.location.pathname)return void fetch("/suggest").then(e=>a(this,void 0,void 0,(function*(){const t=yield e.json();this.setState({lobby:!0,gameName:t.result.game,loading:!1})})));const e=window.location.pathname.substring(1);var t=this.readSession();this.setState(Object.assign(Object.assign({},t[e]),{gid:e})),this.state.loading&&this.join(e,"spectate")}render(){return this.state.loading?i.createElement("div",{id:"game"},i.createElement(g,{error:this.state.error,opacity:this.state.errorOpacity})):this.state.lobby?i.createElement("div",{className:"lobby"},i.createElement("div",{className:"main-title"},"Splendor"),i.createElement("div",{className:"desc"},"Play Splendor online with others. Enter a game name or use the suggested game name to start a game."),i.createElement("div",{className:"lobby-form"},i.createElement("div",{className:"input-row"},i.createElement("span",{className:"input-label"},"Game Name:"),i.createElement("input",{className:"game-name lobby-input",type:"text",onChange:this.nameChange,onKeyPress:this.keyPress,value:this.state.gameName})),i.createElement("div",{className:"input-row"},i.createElement("span",{className:"input-label"},"Turn Time:"),i.createElement("input",{className:"game-name lobby-input lobby-seconds",type:"number",min:1,onChange:this.turnSecondsChange,onKeyPress:this.keyPress,value:this.state.turnSecondsInput}),i.createElement("span",{className:"input-suffix"},"seconds / turn")),i.createElement("div",{className:"input-row input-row-actions"},i.createElement("button",{onClick:this.createGame,className:"create-game"},"Create Game"))),i.createElement(g,{error:this.state.error,opacity:this.state.errorOpacity})):i.createElement("div",{id:"game"},i.createElement("div",{id:"game-title"},i.createElement("div",{className:"link"},"Share this link with friends to join in or watch: ",i.createElement("a",{href:"."},""+document.location.href)),i.createElement("div",{className:"buttons"},!this.state.joined&&i.createElement("button",{className:"start-game",onClick:()=>this.join(this.state.gid,"join")},"Join Game"),this.state.startKey&&0==this.state.pid&&i.createElement("button",{className:"start-game",onClick:this.startGame},"Start Game"))),this.state.pid>=0&&this.state.gid&&this.state.uuid&&i.createElement(p,{key:this.state.pid,gid:this.state.gid,pid:this.state.pid,uuid:this.state.uuid}),i.createElement(g,{error:this.state.error,opacity:this.state.errorOpacity}))}}const f=new Favico({position:"up"});document.onclick=()=>{f.badge("")},n.render(i.createElement("div",null,i.createElement(y,null)),document.getElementById("content"))}()},function(e,t){e.exports=React},function(e,t){e.exports=ReactDOM},function(e,t,s){var a; /** * @license MIT * @fileOverview Favico animations * @author Miroslav Magda, http://blog.ejci.net * @version 0.3.10 - */!function(){var a=function(e){"use strict";e=e||{};var t,a,s,i,n,r,o,l,c,d,m,h,u,p,g,y,f={bgColor:"#d00",textColor:"#fff",fontFamily:"sans-serif",fontStyle:"bold",type:"circle",position:"down",animation:"slide",elementId:!1,dataUrl:!1,win:window};(u={}).ff="undefined"!=typeof InstallTrigger,u.chrome=!!window.chrome,u.opera=!!window.opera||navigator.userAgent.indexOf("Opera")>=0,u.ie=/*@cc_on!@*/!1,u.safari=Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor")>0,u.supported=u.chrome||u.ff||u.opera;var v=[];m=function(){},l=h=!1;var w={ready:function(){l=!0,w.reset(),m()},reset:function(){l&&(v=[],c=!1,d=!1,r.clearRect(0,0,i,s),r.drawImage(o,0,0,i,s),N.setIcon(n),window.clearTimeout(p),window.clearTimeout(g))}};w.start=function(){if(l&&!d){if(v.length>0){d=!0;var e=function(){["type","animation","bgColor","textColor","fontFamily","fontStyle"].forEach((function(e){e in v[0].options&&(t[e]=v[0].options[e])})),k.run(v[0].options,(function(){c=v[0],d=!1,v.length>0&&(v.shift(),w.start())}),!1)};c?k.run(c.options,(function(){e()}),!0):e()}}};var E={},b=function(e){return e.n="number"==typeof e.n?Math.abs(0|e.n):e.n,e.x=i*e.x,e.y=s*e.y,e.w=i*e.w,e.h=s*e.h,e.len=(""+e.n).length,e};E.circle=function(e){var a=!1;2===(e=b(e)).len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,a=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,a=!0),r.clearRect(0,0,i,s),r.drawImage(o,0,0,i,s),r.beginPath(),r.font=t.fontStyle+" "+Math.floor(e.h*(e.n>99?.85:1))+"px "+t.fontFamily,r.textAlign="center",a?(r.moveTo(e.x+e.w/2,e.y),r.lineTo(e.x+e.w-e.h/2,e.y),r.quadraticCurveTo(e.x+e.w,e.y,e.x+e.w,e.y+e.h/2),r.lineTo(e.x+e.w,e.y+e.h-e.h/2),r.quadraticCurveTo(e.x+e.w,e.y+e.h,e.x+e.w-e.h/2,e.y+e.h),r.lineTo(e.x+e.h/2,e.y+e.h),r.quadraticCurveTo(e.x,e.y+e.h,e.x,e.y+e.h-e.h/2),r.lineTo(e.x,e.y+e.h/2),r.quadraticCurveTo(e.x,e.y,e.x+e.h/2,e.y)):r.arc(e.x+e.w/2,e.y+e.h/2,e.h/2,0,2*Math.PI),r.fillStyle="rgba("+t.bgColor.r+","+t.bgColor.g+","+t.bgColor.b+","+e.o+")",r.fill(),r.closePath(),r.beginPath(),r.stroke(),r.fillStyle="rgba("+t.textColor.r+","+t.textColor.g+","+t.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?r.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):r.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),r.closePath()},E.rectangle=function(e){2===(e=b(e)).len?(e.x=e.x-.4*e.w,e.w=1.4*e.w):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w),r.clearRect(0,0,i,s),r.drawImage(o,0,0,i,s),r.beginPath(),r.font=t.fontStyle+" "+Math.floor(e.h*(e.n>99?.9:1))+"px "+t.fontFamily,r.textAlign="center",r.fillStyle="rgba("+t.bgColor.r+","+t.bgColor.g+","+t.bgColor.b+","+e.o+")",r.fillRect(e.x,e.y,e.w,e.h),r.fillStyle="rgba("+t.textColor.r+","+t.textColor.g+","+t.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?r.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):r.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),r.closePath()};function x(e){if(e.paused||e.ended||h)return!1;try{r.clearRect(0,0,i,s),r.drawImage(e,0,0,i,s)}catch(e){}g=setTimeout((function(){x(e)}),k.duration),N.setIcon(n)}var N={};function S(e){e=e.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i,(function(e,t,a,s){return t+t+a+a+s+s}));var t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return!!t&&{r:parseInt(t[1],16),g:parseInt(t[2],16),b:parseInt(t[3],16)}}function C(e,t){var a,s={};for(a in e)s[a]=e[a];for(a in t)s[a]=t[a];return s}N.getIcon=function(){var e=!1;return t.element?e=t.element:t.elementId?(e=y.getElementById(t.elementId)).setAttribute("href",e.getAttribute("src")):!1===(e=function(){for(var e=y.getElementsByTagName("head")[0].getElementsByTagName("link"),t=e.length-1;t>=0;t--)if(/(^|\s)icon(\s|$)/i.test(e[t].getAttribute("rel")))return e[t];return!1}())&&((e=y.createElement("link")).setAttribute("rel","icon"),y.getElementsByTagName("head")[0].appendChild(e)),e.setAttribute("type","image/png"),e},N.setIcon=function(e){var s=e.toDataURL("image/png");if(t.dataUrl&&t.dataUrl(s),t.element)t.element.setAttribute("href",s),t.element.setAttribute("src",s);else if(t.elementId){var i=y.getElementById(t.elementId);i.setAttribute("href",s),i.setAttribute("src",s)}else if(u.ff||u.opera){var n=a;a=y.createElement("link"),u.opera&&a.setAttribute("rel","icon"),a.setAttribute("rel","icon"),a.setAttribute("type","image/png"),y.getElementsByTagName("head")[0].appendChild(a),a.setAttribute("href",s),n.parentNode&&n.parentNode.removeChild(n)}else a.setAttribute("href",s)};var k={duration:40,types:{}};return k.types.fade=[{x:.4,y:.4,w:.6,h:.6,o:0},{x:.4,y:.4,w:.6,h:.6,o:.1},{x:.4,y:.4,w:.6,h:.6,o:.2},{x:.4,y:.4,w:.6,h:.6,o:.3},{x:.4,y:.4,w:.6,h:.6,o:.4},{x:.4,y:.4,w:.6,h:.6,o:.5},{x:.4,y:.4,w:.6,h:.6,o:.6},{x:.4,y:.4,w:.6,h:.6,o:.7},{x:.4,y:.4,w:.6,h:.6,o:.8},{x:.4,y:.4,w:.6,h:.6,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.none=[{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.pop=[{x:1,y:1,w:0,h:0,o:1},{x:.9,y:.9,w:.1,h:.1,o:1},{x:.8,y:.8,w:.2,h:.2,o:1},{x:.7,y:.7,w:.3,h:.3,o:1},{x:.6,y:.6,w:.4,h:.4,o:1},{x:.5,y:.5,w:.5,h:.5,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.popFade=[{x:.75,y:.75,w:0,h:0,o:0},{x:.65,y:.65,w:.1,h:.1,o:.2},{x:.6,y:.6,w:.2,h:.2,o:.4},{x:.55,y:.55,w:.3,h:.3,o:.6},{x:.5,y:.5,w:.4,h:.4,o:.8},{x:.45,y:.45,w:.5,h:.5,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.slide=[{x:.4,y:1,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.8,w:.6,h:.6,o:1},{x:.4,y:.7,w:.6,h:.6,o:1},{x:.4,y:.6,w:.6,h:.6,o:1},{x:.4,y:.5,w:.6,h:.6,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],k.run=function(e,a,s,i){var r=k.types[y.hidden||y.msHidden||y.webkitHidden||y.mozHidden?"none":t.animation];i=!0===s?void 0!==i?i:r.length-1:void 0!==i?i:0,a=a||function(){},i=0?(E[t.type](C(e,r[i])),p=setTimeout((function(){s?i-=1:i+=1,k.run(e,a,s,i)}),k.duration),N.setIcon(n)):a()},function(){(t=C(f,e)).bgColor=S(t.bgColor),t.textColor=S(t.textColor),t.position=t.position.toLowerCase(),t.animation=k.types[""+t.animation]?t.animation:f.animation,y=t.win.document;var l=t.position.indexOf("up")>-1,c=t.position.indexOf("left")>-1;if(l||c)for(var d=0;d0?o.height:32,i=o.width>0?o.width:32,n.height=s,n.width=i,r=n.getContext("2d"),w.ready()},o.setAttribute("src",a.getAttribute("href"))):(o.onload=function(){s=32,i=32,o.height=s,o.width=i,n.height=s,n.width=i,r=n.getContext("2d"),w.ready()},o.setAttribute("src",""))}(),{badge:function(e,t){t=("string"==typeof t?{animation:t}:t)||{},m=function(){try{if("number"==typeof e?e>0:""!==e){var a={type:"badge",options:{n:e}};if("animation"in t&&k.types[""+t.animation]&&(a.options.animation=""+t.animation),"type"in t&&E[""+t.type]&&(a.options.type=""+t.type),["bgColor","textColor"].forEach((function(e){e in t&&(a.options[e]=S(t[e]))})),["fontStyle","fontFamily"].forEach((function(e){e in t&&(a.options[e]=t[e])})),v.push(a),v.length>100)throw new Error("Too many badges requests in queue.");w.start()}else w.reset()}catch(e){throw new Error("Error setting badge. Message: "+e.message)}},l&&m()},video:function(e){m=function(){try{if("stop"===e)return h=!0,w.reset(),void(h=!1);e.addEventListener("play",(function(){x(this)}),!1)}catch(e){throw new Error("Error setting video. Message: "+e.message)}},l&&m()},image:function(e){m=function(){try{var t=e.width,a=e.height,o=document.createElement("img"),l=t/i=0,u.ie=/*@cc_on!@*/!1,u.safari=Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor")>0,u.supported=u.chrome||u.ff||u.opera;var v=[];m=function(){},l=h=!1;var w={ready:function(){l=!0,w.reset(),m()},reset:function(){l&&(v=[],c=!1,d=!1,r.clearRect(0,0,i,a),r.drawImage(o,0,0,i,a),x.setIcon(n),window.clearTimeout(p),window.clearTimeout(g))}};w.start=function(){if(l&&!d){if(v.length>0){d=!0;var e=function(){["type","animation","bgColor","textColor","fontFamily","fontStyle"].forEach((function(e){e in v[0].options&&(t[e]=v[0].options[e])})),k.run(v[0].options,(function(){c=v[0],d=!1,v.length>0&&(v.shift(),w.start())}),!1)};c?k.run(c.options,(function(){e()}),!0):e()}}};var E={},b=function(e){return e.n="number"==typeof e.n?Math.abs(0|e.n):e.n,e.x=i*e.x,e.y=a*e.y,e.w=i*e.w,e.h=a*e.h,e.len=(""+e.n).length,e};E.circle=function(e){var s=!1;2===(e=b(e)).len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,s=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,s=!0),r.clearRect(0,0,i,a),r.drawImage(o,0,0,i,a),r.beginPath(),r.font=t.fontStyle+" "+Math.floor(e.h*(e.n>99?.85:1))+"px "+t.fontFamily,r.textAlign="center",s?(r.moveTo(e.x+e.w/2,e.y),r.lineTo(e.x+e.w-e.h/2,e.y),r.quadraticCurveTo(e.x+e.w,e.y,e.x+e.w,e.y+e.h/2),r.lineTo(e.x+e.w,e.y+e.h-e.h/2),r.quadraticCurveTo(e.x+e.w,e.y+e.h,e.x+e.w-e.h/2,e.y+e.h),r.lineTo(e.x+e.h/2,e.y+e.h),r.quadraticCurveTo(e.x,e.y+e.h,e.x,e.y+e.h-e.h/2),r.lineTo(e.x,e.y+e.h/2),r.quadraticCurveTo(e.x,e.y,e.x+e.h/2,e.y)):r.arc(e.x+e.w/2,e.y+e.h/2,e.h/2,0,2*Math.PI),r.fillStyle="rgba("+t.bgColor.r+","+t.bgColor.g+","+t.bgColor.b+","+e.o+")",r.fill(),r.closePath(),r.beginPath(),r.stroke(),r.fillStyle="rgba("+t.textColor.r+","+t.textColor.g+","+t.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?r.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):r.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),r.closePath()},E.rectangle=function(e){2===(e=b(e)).len?(e.x=e.x-.4*e.w,e.w=1.4*e.w):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w),r.clearRect(0,0,i,a),r.drawImage(o,0,0,i,a),r.beginPath(),r.font=t.fontStyle+" "+Math.floor(e.h*(e.n>99?.9:1))+"px "+t.fontFamily,r.textAlign="center",r.fillStyle="rgba("+t.bgColor.r+","+t.bgColor.g+","+t.bgColor.b+","+e.o+")",r.fillRect(e.x,e.y,e.w,e.h),r.fillStyle="rgba("+t.textColor.r+","+t.textColor.g+","+t.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?r.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):r.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),r.closePath()};function N(e){if(e.paused||e.ended||h)return!1;try{r.clearRect(0,0,i,a),r.drawImage(e,0,0,i,a)}catch(e){}g=setTimeout((function(){N(e)}),k.duration),x.setIcon(n)}var x={};function S(e){e=e.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i,(function(e,t,s,a){return t+t+s+s+a+a}));var t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return!!t&&{r:parseInt(t[1],16),g:parseInt(t[2],16),b:parseInt(t[3],16)}}function C(e,t){var s,a={};for(s in e)a[s]=e[s];for(s in t)a[s]=t[s];return a}x.getIcon=function(){var e=!1;return t.element?e=t.element:t.elementId?(e=y.getElementById(t.elementId)).setAttribute("href",e.getAttribute("src")):!1===(e=function(){for(var e=y.getElementsByTagName("head")[0].getElementsByTagName("link"),t=e.length-1;t>=0;t--)if(/(^|\s)icon(\s|$)/i.test(e[t].getAttribute("rel")))return e[t];return!1}())&&((e=y.createElement("link")).setAttribute("rel","icon"),y.getElementsByTagName("head")[0].appendChild(e)),e.setAttribute("type","image/png"),e},x.setIcon=function(e){var a=e.toDataURL("image/png");if(t.dataUrl&&t.dataUrl(a),t.element)t.element.setAttribute("href",a),t.element.setAttribute("src",a);else if(t.elementId){var i=y.getElementById(t.elementId);i.setAttribute("href",a),i.setAttribute("src",a)}else if(u.ff||u.opera){var n=s;s=y.createElement("link"),u.opera&&s.setAttribute("rel","icon"),s.setAttribute("rel","icon"),s.setAttribute("type","image/png"),y.getElementsByTagName("head")[0].appendChild(s),s.setAttribute("href",a),n.parentNode&&n.parentNode.removeChild(n)}else s.setAttribute("href",a)};var k={duration:40,types:{}};return k.types.fade=[{x:.4,y:.4,w:.6,h:.6,o:0},{x:.4,y:.4,w:.6,h:.6,o:.1},{x:.4,y:.4,w:.6,h:.6,o:.2},{x:.4,y:.4,w:.6,h:.6,o:.3},{x:.4,y:.4,w:.6,h:.6,o:.4},{x:.4,y:.4,w:.6,h:.6,o:.5},{x:.4,y:.4,w:.6,h:.6,o:.6},{x:.4,y:.4,w:.6,h:.6,o:.7},{x:.4,y:.4,w:.6,h:.6,o:.8},{x:.4,y:.4,w:.6,h:.6,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.none=[{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.pop=[{x:1,y:1,w:0,h:0,o:1},{x:.9,y:.9,w:.1,h:.1,o:1},{x:.8,y:.8,w:.2,h:.2,o:1},{x:.7,y:.7,w:.3,h:.3,o:1},{x:.6,y:.6,w:.4,h:.4,o:1},{x:.5,y:.5,w:.5,h:.5,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.popFade=[{x:.75,y:.75,w:0,h:0,o:0},{x:.65,y:.65,w:.1,h:.1,o:.2},{x:.6,y:.6,w:.2,h:.2,o:.4},{x:.55,y:.55,w:.3,h:.3,o:.6},{x:.5,y:.5,w:.4,h:.4,o:.8},{x:.45,y:.45,w:.5,h:.5,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.slide=[{x:.4,y:1,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.8,w:.6,h:.6,o:1},{x:.4,y:.7,w:.6,h:.6,o:1},{x:.4,y:.6,w:.6,h:.6,o:1},{x:.4,y:.5,w:.6,h:.6,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],k.run=function(e,s,a,i){var r=k.types[y.hidden||y.msHidden||y.webkitHidden||y.mozHidden?"none":t.animation];i=!0===a?void 0!==i?i:r.length-1:void 0!==i?i:0,s=s||function(){},i=0?(E[t.type](C(e,r[i])),p=setTimeout((function(){a?i-=1:i+=1,k.run(e,s,a,i)}),k.duration),x.setIcon(n)):s()},function(){(t=C(f,e)).bgColor=S(t.bgColor),t.textColor=S(t.textColor),t.position=t.position.toLowerCase(),t.animation=k.types[""+t.animation]?t.animation:f.animation,y=t.win.document;var l=t.position.indexOf("up")>-1,c=t.position.indexOf("left")>-1;if(l||c)for(var d=0;d0?o.height:32,i=o.width>0?o.width:32,n.height=a,n.width=i,r=n.getContext("2d"),w.ready()},o.setAttribute("src",s.getAttribute("href"))):(o.onload=function(){a=32,i=32,o.height=a,o.width=i,n.height=a,n.width=i,r=n.getContext("2d"),w.ready()},o.setAttribute("src",""))}(),{badge:function(e,t){t=("string"==typeof t?{animation:t}:t)||{},m=function(){try{if("number"==typeof e?e>0:""!==e){var s={type:"badge",options:{n:e}};if("animation"in t&&k.types[""+t.animation]&&(s.options.animation=""+t.animation),"type"in t&&E[""+t.type]&&(s.options.type=""+t.type),["bgColor","textColor"].forEach((function(e){e in t&&(s.options[e]=S(t[e]))})),["fontStyle","fontFamily"].forEach((function(e){e in t&&(s.options[e]=t[e])})),v.push(s),v.length>100)throw new Error("Too many badges requests in queue.");w.start()}else w.reset()}catch(e){throw new Error("Error setting badge. Message: "+e.message)}},l&&m()},video:function(e){m=function(){try{if("stop"===e)return h=!0,w.reset(),void(h=!1);e.addEventListener("play",(function(){N(this)}),!1)}catch(e){throw new Error("Error setting video. Message: "+e.message)}},l&&m()},image:function(e){m=function(){try{var t=e.width,s=e.height,o=document.createElement("img"),l=t/i { return false }\n\n(function () {\n let errorTimeout = 0\n const colors = ['b', 'u', 'w', 'g', 'r']\n const gemColors = colors.concat(['*'])\n const levelNames = ['level1', 'level2', 'level3']\n\n function mapColors(gems: GemsT, game: Game, callback: (color: GemT) => void, symbol: string, uuid: string | number) {\n return gemColors.map((color: GemT) => {\n var cName = color + \"chip\"\n if (color === '*') cName = \"schip\"\n return (\n
\n
{gems[color]}
\n
{symbol}
\n
\n );\n });\n }\n\n function mapNobles(nobles: NobleT[], game: Game) {\n return nobles.map((noble) => {\n return (\n \n );\n });\n }\n\n class Card extends React.PureComponent<{ card: CardT, game: Game }, {}> {\n render() {\n const card = this.props.card\n const game = this.props.game\n var buyer = game.buy.bind(game, card.uuid)\n const reserver = (e: React.MouseEvent) => {\n e.preventDefault();\n game.reserve.bind(game)(card.uuid)\n }\n\n if (card.color) {\n return (\n \n
\n \n
\n
\n
\n
\n
\n
\n
\n {card.points > 0 && card.points}\n
\n
\n
\n {colors.map((color: ColorT) => {\n if(card.cost[color] > 0) {\n return (\n \n {card.cost[color]}\n
\n )\n }\n })}\n
\n \n \n );\n } else {\n return (\n
\n )\n }\n }\n }\n\n class Noble extends React.PureComponent<{ noble: NobleT, game: Game }, {}> {\n render() {\n const noble = this.props.noble\n const game = this.props.game\n const visit = game.noble.bind(game, noble.uuid)\n\n return (\n
\n
\n
\n {noble.points > 0 && noble.points}\n
\n
\n {colors.map((color: ColorT) => {\n if(noble.requirement[color] > 0) {\n return (\n \n {noble.requirement[color]}\n
\n )\n }\n })}\n
\n
\n \n );\n }\n }\n\n interface PlayerProps {\n game: Game\n pid: number\n cards: CardsT\n gems: GemsT\n name: string\n points: number\n nobles: NobleT[]\n reserved: CardT[]\n nreserved: number\n selectedPlayer: number\n }\n\n class Player extends React.PureComponent {\n state = { editingName: null as string | null }\n\n editName = (e: React.ChangeEvent) => {\n this.setState({ editingName: e.target.value })\n }\n\n focusName = (e: React.FocusEvent) => {\n e.target.select()\n }\n\n submitName = () => {\n this.props.game.rename(this.state.editingName);\n this.setState({ editingName: null })\n }\n\n keypress = (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\") {\n this.submitName()\n }\n }\n\n render() {\n const game = this.props.game\n const pid = this.props.pid\n const playerSelector = game.selectPlayer.bind(game, pid)\n const collection: { [color: string]: { cards: number, gems: number } } = {}\n\n gemColors.map((color: ColorT) => {\n collection[color] = {'cards': 0, 'gems': this.props.gems[color]};\n })\n\n const set = colors.map((color: ColorT) => {\n var cards = this.props.cards[color].map((card: CardT) => {\n collection[color]['cards'] += 1;\n return (\n \n \n \n );\n })\n return (\n
\n {cards}\n
0 ? \"endcap\" : \"spacer\"}>
\n
\n )\n })\n\n const stats = gemColors.map((color) => {\n return (\n
\n
{collection[color]['gems'] + (color == '*' ? '' : ' / ' + collection[color]['cards'])}
\n {color === '*' ? :
}\n
\n )\n })\n\n const you = (game.props.pid === pid ? \" you selected\" : \"\")\n const youName = (game.props.pid === pid ? \" (you)\" : \"\")\n const gems = mapColors(this.props.gems, game, game.discard, 'X', pid)\n const reserved = this.props.reserved ? this.props.reserved.map((card) => {\n return (\n \n );\n }) : []\n const reservedCount = this.props.reserved ? reserved.length : this.props.nreserved\n const nobles = mapNobles(this.props.nobles, game)\n\n return (\n
\n
\n
{this.props.points}
\n {this.state.editingName == null ?\n <>\n
{this.props.name}
\n {game.props.pid === pid && this.state.editingName == null ?\n
this.setState({ editingName: this.props.name })}>✏️
: \n }\n :\n
\n \n
\n }\n
{youName}
\n {game.state.turn === pid &&\n
\n }\n
\n {game.state.selectedPlayer === pid ?\n
\n
\n {set}\n
\n
\n {nobles}\n
\n
\n {gems}\n
\n
\n { reservedCount > 0 &&\n
\n
\n reserved\n
\n
\n {reserved}\n
\n
\n }\n
\n
:\n
\n
{stats}
\n
\n {reserved}\n
\n
\n }\n
\n );\n }\n }\n\n class Level extends React.PureComponent<{ name: string, remaining: number, game: Game, cards: CardT[] }, {}> {\n render() {\n return (\n
\n
\n
\n {this.props.remaining}\n
\n
\n
\n \n
\n
\n
\n
\n {this.props.cards && this.props.cards.map((card) =>\n \n )}\n
\n
\n
\n );\n }\n }\n\n class Game extends React.PureComponent<{ gid: string, pid: number, uuid: string }, GameState> {\n state = {\n players: [],\n gems: {},\n cards: {},\n chat: [],\n decks: {},\n nobles: [],\n log: [],\n turn: -1,\n winner: null,\n mode: \"normal\",\n error: null,\n selectedPlayer: -1,\n phase: \"pregame\",\n showChat: false,\n showLog: false,\n chatNotify: false,\n } as GameState\n\n isMyTurn = (turn: number) => {\n return (turn == this.props.pid);\n }\n\n updateState = (r: ServerResponse) => {\n if (r.state) {\n var myTurn = this.isMyTurn(r.state.turn);\n if (!myTurn) this.setState({mode: \"waiting\"});\n else {\n if (this.state.mode == \"waiting\") {\n favicon.badge('!' as any);\n (document.getElementById(\"notify\") as HTMLAudioElement).play();\n }\n this.setState({mode: \"normal\"});\n }\n\n if (this.state.selectedPlayer == -1 && this.props.pid < 4) {\n this.setState({selectedPlayer: this.props.pid});\n }\n\n this.setState({\n log: r.state.log,\n cards: r.state.cards,\n decks: r.state.decks,\n players: r.state.players,\n gems: r.state.gems,\n nobles: r.state.nobles,\n turn: r.state.turn,\n });\n\n if (r.state.winner !== null && this.state.phase != \"postgame\") {\n alert(r.state.players[r.state.winner].name + \" wins!\");\n this.setState({phase: \"postgame\"});\n }\n\n if (r.chat) {\n var chat = this.state.chat;\n if (chat && chat[chat.length - 1] && r.chat[r.chat.length -1]) {\n var lastLocalChat = chat[chat.length - 1];\n var lastRemoteChat = r.chat[r.chat.length - 1];\n if (lastLocalChat.msg != lastRemoteChat.msg && lastRemoteChat.pid != this.props.pid) {\n favicon.badge('.' as any);\n (document.getElementById(\"notify\") as HTMLAudioElement).play();\n if (!this.state.showChat) this.setState({ chatNotify: true })\n }\n }\n this.setState({ chat: r.chat });\n }\n\n for (const scroller of document.getElementsByClassName(\"scroller\")) {\n scroller.scrollTop = scroller.scrollHeight\n }\n }\n }\n\n loginArgs = () => {\n return '?pid=' + this.props.pid + '&uuid=' + this.props.uuid\n }\n\n take = (color: string) => {\n this.act('take', color)\n }\n\n discard = (color: string) => {\n if (confirm(\"Are you sure you want to discard a gem?\")) {\n this.act('discard', color)\n }\n }\n\n selectPlayer = (player: number) => {\n this.setState({ selectedPlayer: player })\n }\n\n buy = (uuid: string) => {\n this.act('buy', uuid)\n }\n\n reserve = (uuid: string) => {\n this.act('reserve', uuid)\n }\n\n noble = (uuid: string) => {\n this.act('noble_visit', uuid);\n }\n\n rename = async (name: string) => {\n const resp = await fetch(`/rename/${this.props.gid}/${name}${this.loginArgs()}`, { method: 'POST' })\n const json = await resp.json()\n globalShowError(json)\n }\n\n act = async (action: string, target: string) => {\n const resp = await fetch('/game/' + this.props.gid + '/' + action + '/' + target + this.loginArgs(), { method: 'POST' })\n const json = await resp.json()\n if (!globalShowError(json)) this.updateState(json)\n }\n\n nextTurn = async () => {\n const resp = await fetch('/game/' + this.props.gid + '/next' + this.loginArgs(), { method: 'POST' })\n const json = await resp.json()\n if (!globalShowError(json)) this.updateState(json)\n }\n\n poll = async () => {\n const resp = await fetch('/poll/' + this.props.gid + this.loginArgs())\n const json = await resp.json()\n if (!globalShowError(json)) {\n this.updateState(json)\n this.poll()\n }\n }\n\n stat = async () => {\n const resp = await fetch(`/stat/${this.props.gid}${this.loginArgs()}`)\n const json = await resp.json()\n if (!globalShowError(json)) this.updateState(json)\n }\n\n componentDidMount() {\n this.stat()\n this.poll()\n }\n\n chat = async (e: React.KeyboardEvent) => {\n const chatBox = document.getElementById(\"chat-inner\") as HTMLInputElement\n if (e.which == 13) {\n const resp = await fetch('/game/' + this.props.gid + '/chat' + this.loginArgs(), {\n method: 'POST',\n body: JSON.stringify({ msg: chatBox.value }),\n })\n chatBox.value = \"\"\n const json = await resp.json()\n if (!globalShowError(json)) this.updateState(json)\n }\n }\n\n render() {\n var players = this.state.players.map((player) => {\n return (\n \n );\n });\n var gems = mapColors(this.state.gems, this, this.take, '', 'game');\n var nobles = mapNobles(this.state.nobles, this);\n var log = this.state.log.map((logLine, i) => {\n return (\n
\n {\"[\" + logLine.pid + \"] \"}\n {logLine.msg}\n
\n );\n });\n var chat = this.state.chat.map((chatLine, i) => {\n return (\n
\n {chatLine.name + \": \"}\n {chatLine.msg}\n
\n );\n });\n var levels = levelNames.map((level) => {\n return (\n \n )\n });\n return (\n
\n
\n
\n
\n {nobles}\n
\n
\n {levels}\n
\n
\n
\n
Click on card to buy, click on
to reserve.
\n
\n
\n
\n {gems}\n
\n
\n
\n {players}\n
\n
\n
\n
this.setState({ showLog: !this.state.showLog })}>::Log
\n
\n {log}\n
\n
\n
this.setState({ chatNotify: false })} style={{ bottom: this.state.showChat ? -4 : -314 }}>\n
this.setState({ showChat: !this.state.showChat })}>::Chat
\n
\n {chat}\n
\n
\n >\n \n
\n
\n {this.state.turn >= 0 && this.props.pid >= 0 && this.props.pid < 4 &&\n \n }\n
\n );\n }\n }\n\n const ErrorMsg = (props: { error: string | null, opacity: number }) => {\n return
\n
{props.error}
\n
\n }\n\n interface GameCreatorState {\n startKey: string | null\n loading: boolean\n lobby: boolean\n gameName: string\n joined: boolean\n pid: number\n uuid: string\n gid: string\n error: string | null\n errorOpacity: number\n }\n\n class GameCreator extends React.PureComponent<{}, GameCreatorState> {\n state = {\n startKey: null,\n loading: true,\n lobby: false,\n joined: false,\n pid: -1,\n uuid: '',\n gid: '',\n gameName: '',\n errorOpacity: 0,\n error: null,\n } as GameCreatorState\n\n creating = false\n\n join = async (game: string, act: string) => {\n const session = this.readSession()\n if (session[game] && !session[game].loading && (session[game].joined || act === 'spectate')) {\n this.setState(session[game]);\n this.save();\n return;\n }\n\n const resp = await fetch(`/${act}/${game}`, { method: 'POST' })\n const json = await resp.json()\n if (json.status === 404) {\n this.createGame()\n return\n }\n\n if (this.showError(json)) return\n\n this.setState({\n joined: (act === 'join'),\n loading: false,\n pid: json.id,\n uuid: json.uuid,\n gid: game,\n })\n this.save()\n }\n\n showError = (resp: ServerResponse): boolean => {\n let msg: string | null = null\n if (!resp) {\n msg = \"Request failed\";\n return true\n }\n else if (!resp.error && (!resp.result || !resp.result.error)) return false;\n else msg = resp.error || resp.result.error;\n\n if (resp.status === 404) {\n this.clear()\n this.setState({ loading: true, joined: false, pid: -1, uuid: '' })\n this.join(this.state.gid, 'spectate')\n return true\n }\n\n this.setState({ error: msg, errorOpacity: 1 })\n\n clearTimeout(errorTimeout);\n errorTimeout = setTimeout(() => {\n this.setState({ errorOpacity: 0 })\n }, 4000)\n\n return true\n }\n\n createGame = async () => {\n if (this.creating) return\n this.creating = true\n const gameName = this.state.gid === '' ? this.state.gameName : this.state.gid\n const resp = await fetch(`/create/${gameName}`, { method: \"POST\" })\n const json = await resp.json()\n\n if (this.showError(json)) return\n\n history.replaceState(null, \"Splendor\", `/${json.game}`)\n this.join(json.game, 'join')\n this.setState({ startKey: json.start, loading: true, lobby: false })\n }\n\n readSession = () => {\n var session = window.localStorage.getItem('splendor');\n if (session === null) return {};\n return JSON.parse(session);\n }\n\n componentDidMount() {\n globalShowError = this.showError\n\n if (window.location.pathname === \"/\") {\n fetch(`/suggest`).then(async (resp) => {\n const json = await resp.json()\n this.setState({\n lobby: true,\n gameName: json.result.game,\n loading: false,\n })\n })\n return\n }\n\n const gid = window.location.pathname.substring(1)\n var session = this.readSession()\n this.setState({\n ...session[gid],\n gid,\n })\n if (this.state.loading) {\n this.join(gid, 'spectate')\n }\n }\n\n save = () => {\n setTimeout(() => {\n this.saveRaw(this.state)\n }, 100)\n }\n\n clear = () => {\n this.saveRaw(null)\n }\n\n saveRaw = (state: any) => {\n const session = this.readSession();\n if (state === null) delete session[this.state.gid]\n else session[this.state.gid] = state\n window.localStorage['splendor'] = JSON.stringify(session);\n }\n\n startGame = async () => {\n const resp = await fetch(`/start/${this.state.gid}/${this.state.startKey}`, { method: \"POST\" })\n const json = await resp.json()\n\n if (!this.showError(json)) {\n this.setState({ startKey: null });\n this.save();\n }\n }\n\n nameChange = (e: React.ChangeEvent) => {\n this.setState({ gameName: e.target.value })\n }\n\n keyPress = (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\") this.createGame()\n }\n\n render() {\n if (this.state.loading) {\n return
\n \n
\n }\n\n if (this.state.lobby) {\n return
\n
Splendor
\n
Play Splendor online with others. Enter a game name or use the suggested game name to start a game.
\n
\n \n \n
\n \n
\n }\n\n return (\n
\n )\n }\n }\n\n const favicon=new Favico({\n position : 'up'\n })\n\n document.onclick = () => {\n favicon.badge('' as any)\n }\n\n ReactDOM.render(\n
\n \n
,\n document.getElementById('content')\n )\n})()\n","module.exports = React;","module.exports = ReactDOM;","/**\n * @license MIT\n * @fileOverview Favico animations\n * @author Miroslav Magda, http://blog.ejci.net\n * @version 0.3.10\n */\n\n/**\n * Create new favico instance\n * @param {Object} Options\n * @return {Object} Favico object\n * @example\n * var favico = new Favico({\n * bgColor : '#d00',\n * textColor : '#fff',\n * fontFamily : 'sans-serif',\n * fontStyle : 'bold',\n * position : 'down',\n * type : 'circle',\n * animation : 'slide',\n * dataUrl: function(url){},\n * win: top\n * });\n */\n(function () {\n\n\tvar Favico = (function (opt) {\n\t\t'use strict';\n\t\topt = (opt) ? opt : {};\n\t\tvar _def = {\n\t\t\tbgColor: '#d00',\n\t\t\ttextColor: '#fff',\n\t\t\tfontFamily: 'sans-serif', //Arial,Verdana,Times New Roman,serif,sans-serif,...\n\t\t\tfontStyle: 'bold', //normal,italic,oblique,bold,bolder,lighter,100,200,300,400,500,600,700,800,900\n\t\t\ttype: 'circle',\n\t\t\tposition: 'down', // down, up, left, leftup (upleft)\n\t\t\tanimation: 'slide',\n\t\t\telementId: false,\n\t\t\tdataUrl: false,\n\t\t\twin: window\n\t\t};\n\t\tvar _opt, _orig, _h, _w, _canvas, _context, _img, _ready, _lastBadge, _running, _readyCb, _stop, _browser, _animTimeout, _drawTimeout, _doc;\n\n\t\t_browser = {};\n\t\t_browser.ff = typeof InstallTrigger != 'undefined';\n\t\t_browser.chrome = !!window.chrome;\n\t\t_browser.opera = !!window.opera || navigator.userAgent.indexOf('Opera') >= 0;\n\t\t_browser.ie = /*@cc_on!@*/false;\n\t\t_browser.safari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;\n\t\t_browser.supported = (_browser.chrome || _browser.ff || _browser.opera);\n\n\t\tvar _queue = [];\n\t\t_readyCb = function () {\n\t\t};\n\t\t_ready = _stop = false;\n\t\t/**\n\t\t * Initialize favico\n\t\t */\n\t\tvar init = function () {\n\t\t\t//merge initial options\n\t\t\t_opt = merge(_def, opt);\n\t\t\t_opt.bgColor = hexToRgb(_opt.bgColor);\n\t\t\t_opt.textColor = hexToRgb(_opt.textColor);\n\t\t\t_opt.position = _opt.position.toLowerCase();\n\t\t\t_opt.animation = (animation.types['' + _opt.animation]) ? _opt.animation : _def.animation;\n\n\t\t\t_doc = _opt.win.document;\n\n\t\t\tvar isUp = _opt.position.indexOf('up') > -1;\n\t\t\tvar isLeft = _opt.position.indexOf('left') > -1;\n\n\t\t\t//transform animation\n\t\t\tif (isUp || isLeft) {\n\t\t\t\tfor (var i = 0; i < animation.types['' + _opt.animation].length; i++) {\n\t\t\t\t\tvar step = animation.types['' + _opt.animation][i];\n\n\t\t\t\t\tif (isUp) {\n\t\t\t\t\t\tif (step.y < 0.6) {\n\t\t\t\t\t\t\tstep.y = step.y - 0.4;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tstep.y = step.y - 2 * step.y + (1 - step.w);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (isLeft) {\n\t\t\t\t\t\tif (step.x < 0.6) {\n\t\t\t\t\t\t\tstep.x = step.x - 0.4;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tstep.x = step.x - 2 * step.x + (1 - step.h);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tanimation.types['' + _opt.animation][i] = step;\n\t\t\t\t}\n\t\t\t}\n\t\t\t_opt.type = (type['' + _opt.type]) ? _opt.type : _def.type;\n\n\t\t\t_orig = link.getIcon();\n\t\t\t//create temp canvas\n\t\t\t_canvas = document.createElement('canvas');\n\t\t\t//create temp image\n\t\t\t_img = document.createElement('img');\n\t\t\tif (_orig.hasAttribute('href')) {\n\t\t\t\t_img.setAttribute('crossOrigin', 'anonymous');\n\t\t\t\t//get width/height\n\t\t\t\t_img.onload = function () {\n\t\t\t\t\t_h = (_img.height > 0) ? _img.height : 32;\n\t\t\t\t\t_w = (_img.width > 0) ? _img.width : 32;\n\t\t\t\t\t_canvas.height = _h;\n\t\t\t\t\t_canvas.width = _w;\n\t\t\t\t\t_context = _canvas.getContext('2d');\n\t\t\t\t\ticon.ready();\n\t\t\t\t};\n\t\t\t\t_img.setAttribute('src', _orig.getAttribute('href'));\n\t\t\t} else {\n\t\t\t\t_img.onload = function () {\n\t\t\t\t\t_h = 32;\n\t\t\t\t\t_w = 32;\n\t\t\t\t\t_img.height = _h;\n\t\t\t\t\t_img.width = _w;\n\t\t\t\t\t_canvas.height = _h;\n\t\t\t\t\t_canvas.width = _w;\n\t\t\t\t\t_context = _canvas.getContext('2d');\n\t\t\t\t\ticon.ready();\n\t\t\t\t};\n\t\t\t\t_img.setAttribute('src', '');\n\t\t\t}\n\n\t\t};\n\t\t/**\n\t\t * Icon namespace\n\t\t */\n\t\tvar icon = {};\n\t\t/**\n\t\t * Icon is ready (reset icon) and start animation (if ther is any)\n\t\t */\n\t\ticon.ready = function () {\n\t\t\t_ready = true;\n\t\t\ticon.reset();\n\t\t\t_readyCb();\n\t\t};\n\t\t/**\n\t\t * Reset icon to default state\n\t\t */\n\t\ticon.reset = function () {\n\t\t\t//reset\n\t\t\tif (!_ready) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t_queue = [];\n\t\t\t_lastBadge = false;\n\t\t\t_running = false;\n\t\t\t_context.clearRect(0, 0, _w, _h);\n\t\t\t_context.drawImage(_img, 0, 0, _w, _h);\n\t\t\t//_stop=true;\n\t\t\tlink.setIcon(_canvas);\n\t\t\t//webcam('stop');\n\t\t\t//video('stop');\n\t\t\twindow.clearTimeout(_animTimeout);\n\t\t\twindow.clearTimeout(_drawTimeout);\n\t\t};\n\t\t/**\n\t\t * Start animation\n\t\t */\n\t\ticon.start = function () {\n\t\t\tif (!_ready || _running) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar finished = function () {\n\t\t\t\t_lastBadge = _queue[0];\n\t\t\t\t_running = false;\n\t\t\t\tif (_queue.length > 0) {\n\t\t\t\t\t_queue.shift();\n\t\t\t\t\ticon.start();\n\t\t\t\t} else {\n\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (_queue.length > 0) {\n\t\t\t\t_running = true;\n\t\t\t\tvar run = function () {\n\t\t\t\t\t// apply options for this animation\n\t\t\t\t\t['type', 'animation', 'bgColor', 'textColor', 'fontFamily', 'fontStyle'].forEach(function (a) {\n\t\t\t\t\t\tif (a in _queue[0].options) {\n\t\t\t\t\t\t\t_opt[a] = _queue[0].options[a];\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t\tanimation.run(_queue[0].options, function () {\n\t\t\t\t\t\tfinished();\n\t\t\t\t\t}, false);\n\t\t\t\t};\n\t\t\t\tif (_lastBadge) {\n\t\t\t\t\tanimation.run(_lastBadge.options, function () {\n\t\t\t\t\t\trun();\n\t\t\t\t\t}, true);\n\t\t\t\t} else {\n\t\t\t\t\trun();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\t/**\n\t\t * Badge types\n\t\t */\n\t\tvar type = {};\n\t\tvar options = function (opt) {\n\t\t\topt.n = ((typeof opt.n) === 'number') ? Math.abs(opt.n | 0) : opt.n;\n\t\t\topt.x = _w * opt.x;\n\t\t\topt.y = _h * opt.y;\n\t\t\topt.w = _w * opt.w;\n\t\t\topt.h = _h * opt.h;\n\t\t\topt.len = (\"\" + opt.n).length;\n\t\t\treturn opt;\n\t\t};\n\t\t/**\n\t\t * Generate circle\n\t\t * @param {Object} opt Badge options\n\t\t */\n\t\ttype.circle = function (opt) {\n\t\t\topt = options(opt);\n\t\t\tvar more = false;\n\t\t\tif (opt.len === 2) {\n\t\t\t\topt.x = opt.x - opt.w * 0.4;\n\t\t\t\topt.w = opt.w * 1.4;\n\t\t\t\tmore = true;\n\t\t\t} else if (opt.len >= 3) {\n\t\t\t\topt.x = opt.x - opt.w * 0.65;\n\t\t\t\topt.w = opt.w * 1.65;\n\t\t\t\tmore = true;\n\t\t\t}\n\t\t\t_context.clearRect(0, 0, _w, _h);\n\t\t\t_context.drawImage(_img, 0, 0, _w, _h);\n\t\t\t_context.beginPath();\n\t\t\t_context.font = _opt.fontStyle + \" \" + Math.floor(opt.h * (opt.n > 99 ? 0.85 : 1)) + \"px \" + _opt.fontFamily;\n\t\t\t_context.textAlign = 'center';\n\t\t\tif (more) {\n\t\t\t\t_context.moveTo(opt.x + opt.w / 2, opt.y);\n\t\t\t\t_context.lineTo(opt.x + opt.w - opt.h / 2, opt.y);\n\t\t\t\t_context.quadraticCurveTo(opt.x + opt.w, opt.y, opt.x + opt.w, opt.y + opt.h / 2);\n\t\t\t\t_context.lineTo(opt.x + opt.w, opt.y + opt.h - opt.h / 2);\n\t\t\t\t_context.quadraticCurveTo(opt.x + opt.w, opt.y + opt.h, opt.x + opt.w - opt.h / 2, opt.y + opt.h);\n\t\t\t\t_context.lineTo(opt.x + opt.h / 2, opt.y + opt.h);\n\t\t\t\t_context.quadraticCurveTo(opt.x, opt.y + opt.h, opt.x, opt.y + opt.h - opt.h / 2);\n\t\t\t\t_context.lineTo(opt.x, opt.y + opt.h / 2);\n\t\t\t\t_context.quadraticCurveTo(opt.x, opt.y, opt.x + opt.h / 2, opt.y);\n\t\t\t} else {\n\t\t\t\t_context.arc(opt.x + opt.w / 2, opt.y + opt.h / 2, opt.h / 2, 0, 2 * Math.PI);\n\t\t\t}\n\t\t\t_context.fillStyle = 'rgba(' + _opt.bgColor.r + ',' + _opt.bgColor.g + ',' + _opt.bgColor.b + ',' + opt.o + ')';\n\t\t\t_context.fill();\n\t\t\t_context.closePath();\n\t\t\t_context.beginPath();\n\t\t\t_context.stroke();\n\t\t\t_context.fillStyle = 'rgba(' + _opt.textColor.r + ',' + _opt.textColor.g + ',' + _opt.textColor.b + ',' + opt.o + ')';\n\t\t\t//_context.fillText((more) ? '9+' : opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15));\n\t\t\tif ((typeof opt.n) === 'number' && opt.n > 999) {\n\t\t\t\t_context.fillText(((opt.n > 9999) ? 9 : Math.floor(opt.n / 1000)) + 'k+', Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.2));\n\t\t\t} else {\n\t\t\t\t_context.fillText(opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15));\n\t\t\t}\n\t\t\t_context.closePath();\n\t\t};\n\t\t/**\n\t\t * Generate rectangle\n\t\t * @param {Object} opt Badge options\n\t\t */\n\t\ttype.rectangle = function (opt) {\n\t\t\topt = options(opt);\n\t\t\tvar more = false;\n\t\t\tif (opt.len === 2) {\n\t\t\t\topt.x = opt.x - opt.w * 0.4;\n\t\t\t\topt.w = opt.w * 1.4;\n\t\t\t\tmore = true;\n\t\t\t} else if (opt.len >= 3) {\n\t\t\t\topt.x = opt.x - opt.w * 0.65;\n\t\t\t\topt.w = opt.w * 1.65;\n\t\t\t\tmore = true;\n\t\t\t}\n\t\t\t_context.clearRect(0, 0, _w, _h);\n\t\t\t_context.drawImage(_img, 0, 0, _w, _h);\n\t\t\t_context.beginPath();\n\t\t\t_context.font = _opt.fontStyle + \" \" + Math.floor(opt.h * (opt.n > 99 ? 0.9 : 1)) + \"px \" + _opt.fontFamily;\n\t\t\t_context.textAlign = 'center';\n\t\t\t_context.fillStyle = 'rgba(' + _opt.bgColor.r + ',' + _opt.bgColor.g + ',' + _opt.bgColor.b + ',' + opt.o + ')';\n\t\t\t_context.fillRect(opt.x, opt.y, opt.w, opt.h);\n\t\t\t_context.fillStyle = 'rgba(' + _opt.textColor.r + ',' + _opt.textColor.g + ',' + _opt.textColor.b + ',' + opt.o + ')';\n\t\t\t//_context.fillText((more) ? '9+' : opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15));\n\t\t\tif ((typeof opt.n) === 'number' && opt.n > 999) {\n\t\t\t\t_context.fillText(((opt.n > 9999) ? 9 : Math.floor(opt.n / 1000)) + 'k+', Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.2));\n\t\t\t} else {\n\t\t\t\t_context.fillText(opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15));\n\t\t\t}\n\t\t\t_context.closePath();\n\t\t};\n\n\t\t/**\n\t\t * Set badge\n\t\t */\n\t\tvar badge = function (number, opts) {\n\t\t\topts = ((typeof opts) === 'string' ? {\n\t\t\t\tanimation: opts\n\t\t\t} : opts) || {};\n\t\t\t_readyCb = function () {\n\t\t\t\ttry {\n\t\t\t\t\tif (typeof (number) === 'number' ? (number > 0) : (number !== '')) {\n\t\t\t\t\t\tvar q = {\n\t\t\t\t\t\t\ttype: 'badge',\n\t\t\t\t\t\t\toptions: {\n\t\t\t\t\t\t\t\tn: number\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t};\n\t\t\t\t\t\tif ('animation' in opts && animation.types['' + opts.animation]) {\n\t\t\t\t\t\t\tq.options.animation = '' + opts.animation;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif ('type' in opts && type['' + opts.type]) {\n\t\t\t\t\t\t\tq.options.type = '' + opts.type;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t['bgColor', 'textColor'].forEach(function (o) {\n\t\t\t\t\t\t\tif (o in opts) {\n\t\t\t\t\t\t\t\tq.options[o] = hexToRgb(opts[o]);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t\t['fontStyle', 'fontFamily'].forEach(function (o) {\n\t\t\t\t\t\t\tif (o in opts) {\n\t\t\t\t\t\t\t\tq.options[o] = opts[o];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t\t_queue.push(q);\n\t\t\t\t\t\tif (_queue.length > 100) {\n\t\t\t\t\t\t\tthrow new Error('Too many badges requests in queue.');\n\t\t\t\t\t\t}\n\t\t\t\t\t\ticon.start();\n\t\t\t\t\t} else {\n\t\t\t\t\t\ticon.reset();\n\t\t\t\t\t}\n\t\t\t\t} catch (e) {\n\t\t\t\t\tthrow new Error('Error setting badge. Message: ' + e.message);\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (_ready) {\n\t\t\t\t_readyCb();\n\t\t\t}\n\t\t};\n\n\t\t/**\n\t\t * Set image as icon\n\t\t */\n\t\tvar image = function (imageElement) {\n\t\t\t_readyCb = function () {\n\t\t\t\ttry {\n\t\t\t\t\tvar w = imageElement.width;\n\t\t\t\t\tvar h = imageElement.height;\n\t\t\t\t\tvar newImg = document.createElement('img');\n\t\t\t\t\tvar ratio = (w / _w < h / _h) ? (w / _w) : (h / _h);\n\t\t\t\t\tnewImg.setAttribute('crossOrigin', 'anonymous');\n\t\t\t\t\tnewImg.onload=function(){\n\t\t\t\t\t\t_context.clearRect(0, 0, _w, _h);\n\t\t\t\t\t\t_context.drawImage(newImg, 0, 0, _w, _h);\n\t\t\t\t\t\tlink.setIcon(_canvas);\n\t\t\t\t\t};\n\t\t\t\t\tnewImg.setAttribute('src', imageElement.getAttribute('src'));\n\t\t\t\t\tnewImg.height = (h / ratio);\n\t\t\t\t\tnewImg.width = (w / ratio);\n\t\t\t\t} catch (e) {\n\t\t\t\t\tthrow new Error('Error setting image. Message: ' + e.message);\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (_ready) {\n\t\t\t\t_readyCb();\n\t\t\t}\n\t\t};\n\t\t/**\n\t\t * Set video as icon\n\t\t */\n\t\tvar video = function (videoElement) {\n\t\t\t_readyCb = function () {\n\t\t\t\ttry {\n\t\t\t\t\tif (videoElement === 'stop') {\n\t\t\t\t\t\t_stop = true;\n\t\t\t\t\t\ticon.reset();\n\t\t\t\t\t\t_stop = false;\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t//var w = videoElement.width;\n\t\t\t\t\t//var h = videoElement.height;\n\t\t\t\t\t//var ratio = (w / _w < h / _h) ? (w / _w) : (h / _h);\n\t\t\t\t\tvideoElement.addEventListener('play', function () {\n\t\t\t\t\t\tdrawVideo(this);\n\t\t\t\t\t}, false);\n\n\t\t\t\t} catch (e) {\n\t\t\t\t\tthrow new Error('Error setting video. Message: ' + e.message);\n\t\t\t\t}\n\t\t\t};\n\t\t\tif (_ready) {\n\t\t\t\t_readyCb();\n\t\t\t}\n\t\t};\n\t\t/**\n\t\t * Set video as icon\n\t\t */\n\t\tvar webcam = function (action) {\n\t\t\t//UR\n\t\t\tif (!window.URL || !window.URL.createObjectURL) {\n\t\t\t\twindow.URL = window.URL || {};\n\t\t\t\twindow.URL.createObjectURL = function (obj) {\n\t\t\t\t\treturn obj;\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (_browser.supported) {\n\t\t\t\tvar newVideo = false;\n\t\t\t\tnavigator.getUserMedia = navigator.getUserMedia || navigator.oGetUserMedia || navigator.msGetUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia;\n\t\t\t\t_readyCb = function () {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (action === 'stop') {\n\t\t\t\t\t\t\t_stop = true;\n\t\t\t\t\t\t\ticon.reset();\n\t\t\t\t\t\t\t_stop = false;\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tnewVideo = document.createElement('video');\n\t\t\t\t\t\tnewVideo.width = _w;\n\t\t\t\t\t\tnewVideo.height = _h;\n\t\t\t\t\t\tnavigator.getUserMedia({\n\t\t\t\t\t\t\tvideo: true,\n\t\t\t\t\t\t\taudio: false\n\t\t\t\t\t\t}, function (stream) {\n\t\t\t\t\t\t\tnewVideo.src = URL.createObjectURL(stream);\n\t\t\t\t\t\t\tnewVideo.play();\n\t\t\t\t\t\t\tdrawVideo(newVideo);\n\t\t\t\t\t\t}, function () {\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\tthrow new Error('Error setting webcam. Message: ' + e.message);\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\tif (_ready) {\n\t\t\t\t\t_readyCb();\n\t\t\t\t}\n\t\t\t}\n\n\t\t};\n\n\t\t/**\n\t\t * Draw video to context and repeat :)\n\t\t */\n\t\tfunction drawVideo(video) {\n\t\t\tif (video.paused || video.ended || _stop) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t//nasty hack for FF webcam (Thanks to Julian Ćwirko, kontakt@redsunmedia.pl)\n\t\t\ttry {\n\t\t\t\t_context.clearRect(0, 0, _w, _h);\n\t\t\t\t_context.drawImage(video, 0, 0, _w, _h);\n\t\t\t} catch (e) {\n\n\t\t\t}\n\t\t\t_drawTimeout = setTimeout(function () {\n\t\t\t\tdrawVideo(video);\n\t\t\t}, animation.duration);\n\t\t\tlink.setIcon(_canvas);\n\t\t}\n\n\t\tvar link = {};\n\t\t/**\n\t\t * Get icon from HEAD tag or create a new element\n\t\t */\n\t\tlink.getIcon = function () {\n\t\t\tvar elm = false;\n\t\t\t//get link element\n\t\t\tvar getLink = function () {\n\t\t\t\tvar link = _doc.getElementsByTagName('head')[0].getElementsByTagName('link');\n\t\t\t\tfor (var l = link.length, i = (l - 1); i >= 0; i--) {\n\t\t\t\t\tif ((/(^|\\s)icon(\\s|$)/i).test(link[i].getAttribute('rel'))) {\n\t\t\t\t\t\treturn link[i];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t};\n\t\t\tif (_opt.element) {\n\t\t\t\telm = _opt.element;\n\t\t\t} else if (_opt.elementId) {\n\t\t\t\t//if img element identified by elementId\n\t\t\t\telm = _doc.getElementById(_opt.elementId);\n\t\t\t\telm.setAttribute('href', elm.getAttribute('src'));\n\t\t\t} else {\n\t\t\t\t//if link element\n\t\t\t\telm = getLink();\n\t\t\t\tif (elm === false) {\n\t\t\t\t\telm = _doc.createElement('link');\n\t\t\t\t\telm.setAttribute('rel', 'icon');\n\t\t\t\t\t_doc.getElementsByTagName('head')[0].appendChild(elm);\n\t\t\t\t}\n\t\t\t}\n\t\t\telm.setAttribute('type', 'image/png');\n\t\t\treturn elm;\n\t\t};\n\t\tlink.setIcon = function (canvas) {\n\t\t\tvar url = canvas.toDataURL('image/png');\n\t\t\tif (_opt.dataUrl) {\n\t\t\t\t//if using custom exporter\n\t\t\t\t_opt.dataUrl(url);\n\t\t\t}\n\t\t\tif (_opt.element) {\n\t\t\t\t_opt.element.setAttribute('href', url);\n\t\t\t\t_opt.element.setAttribute('src', url);\n\t\t\t} else if (_opt.elementId) {\n\t\t\t\t//if is attached to element (image)\n\t\t\t\tvar elm = _doc.getElementById(_opt.elementId);\n\t\t\t\telm.setAttribute('href', url);\n\t\t\t\telm.setAttribute('src', url);\n\t\t\t} else {\n\t\t\t\t//if is attached to fav icon\n\t\t\t\tif (_browser.ff || _browser.opera) {\n\t\t\t\t\t//for FF we need to \"recreate\" element, atach to dom and remove old \n\t\t\t\t\t//var originalType = _orig.getAttribute('rel');\n\t\t\t\t\tvar old = _orig;\n\t\t\t\t\t_orig = _doc.createElement('link');\n\t\t\t\t\t//_orig.setAttribute('rel', originalType);\n\t\t\t\t\tif (_browser.opera) {\n\t\t\t\t\t\t_orig.setAttribute('rel', 'icon');\n\t\t\t\t\t}\n\t\t\t\t\t_orig.setAttribute('rel', 'icon');\n\t\t\t\t\t_orig.setAttribute('type', 'image/png');\n\t\t\t\t\t_doc.getElementsByTagName('head')[0].appendChild(_orig);\n\t\t\t\t\t_orig.setAttribute('href', url);\n\t\t\t\t\tif (old.parentNode) {\n\t\t\t\t\t\told.parentNode.removeChild(old);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t_orig.setAttribute('href', url);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\t//http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb#answer-5624139\n\t\t//HEX to RGB convertor\n\t\tfunction hexToRgb(hex) {\n\t\t\tvar shorthandRegex = /^#?([a-f\\d])([a-f\\d])([a-f\\d])$/i;\n\t\t\thex = hex.replace(shorthandRegex, function (m, r, g, b) {\n\t\t\t\treturn r + r + g + g + b + b;\n\t\t\t});\n\t\t\tvar result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n\t\t\treturn result ? {\n\t\t\t\tr: parseInt(result[1], 16),\n\t\t\t\tg: parseInt(result[2], 16),\n\t\t\t\tb: parseInt(result[3], 16)\n\t\t\t} : false;\n\t\t}\n\n\t\t/**\n\t\t * Merge options\n\t\t */\n\t\tfunction merge(def, opt) {\n\t\t\tvar mergedOpt = {};\n\t\t\tvar attrname;\n\t\t\tfor (attrname in def) {\n\t\t\t\tmergedOpt[attrname] = def[attrname];\n\t\t\t}\n\t\t\tfor (attrname in opt) {\n\t\t\t\tmergedOpt[attrname] = opt[attrname];\n\t\t\t}\n\t\t\treturn mergedOpt;\n\t\t}\n\n\t\t/**\n\t\t * Cross-browser page visibility shim\n\t\t * http://stackoverflow.com/questions/12536562/detect-whether-a-window-is-visible\n\t\t */\n\t\tfunction isPageHidden() {\n\t\t\treturn _doc.hidden || _doc.msHidden || _doc.webkitHidden || _doc.mozHidden;\n\t\t}\n\n\t\t/**\n\t\t * @namespace animation\n\t\t */\n\t\tvar animation = {};\n\t\t/**\n\t\t * Animation \"frame\" duration\n\t\t */\n\t\tanimation.duration = 40;\n\t\t/**\n\t\t * Animation types (none,fade,pop,slide)\n\t\t */\n\t\tanimation.types = {};\n\t\tanimation.types.fade = [{\n\t\t\tx: 0.4,\n\t\t\ty: 0.4,\n\t\t\tw: 0.6,\n\t\t\th: 0.6,\n\t\t\to: 0.0\n\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.4,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 0.1\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.4,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 0.2\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.4,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 0.3\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.4,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 0.4\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.4,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 0.5\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.4,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 0.6\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.4,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 0.7\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.4,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 0.8\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.4,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 0.9\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.4,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 1.0\n\t\t\t}];\n\t\tanimation.types.none = [{\n\t\t\tx: 0.4,\n\t\t\ty: 0.4,\n\t\t\tw: 0.6,\n\t\t\th: 0.6,\n\t\t\to: 1\n\t\t}];\n\t\tanimation.types.pop = [{\n\t\t\tx: 1,\n\t\t\ty: 1,\n\t\t\tw: 0,\n\t\t\th: 0,\n\t\t\to: 1\n\t\t}, {\n\t\t\t\tx: 0.9,\n\t\t\t\ty: 0.9,\n\t\t\t\tw: 0.1,\n\t\t\t\th: 0.1,\n\t\t\t\to: 1\n\t\t\t}, {\n\t\t\t\tx: 0.8,\n\t\t\t\ty: 0.8,\n\t\t\t\tw: 0.2,\n\t\t\t\th: 0.2,\n\t\t\t\to: 1\n\t\t\t}, {\n\t\t\t\tx: 0.7,\n\t\t\t\ty: 0.7,\n\t\t\t\tw: 0.3,\n\t\t\t\th: 0.3,\n\t\t\t\to: 1\n\t\t\t}, {\n\t\t\t\tx: 0.6,\n\t\t\t\ty: 0.6,\n\t\t\t\tw: 0.4,\n\t\t\t\th: 0.4,\n\t\t\t\to: 1\n\t\t\t}, {\n\t\t\t\tx: 0.5,\n\t\t\t\ty: 0.5,\n\t\t\t\tw: 0.5,\n\t\t\t\th: 0.5,\n\t\t\t\to: 1\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.4,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 1\n\t\t\t}];\n\t\tanimation.types.popFade = [{\n\t\t\tx: 0.75,\n\t\t\ty: 0.75,\n\t\t\tw: 0,\n\t\t\th: 0,\n\t\t\to: 0\n\t\t}, {\n\t\t\t\tx: 0.65,\n\t\t\t\ty: 0.65,\n\t\t\t\tw: 0.1,\n\t\t\t\th: 0.1,\n\t\t\t\to: 0.2\n\t\t\t}, {\n\t\t\t\tx: 0.6,\n\t\t\t\ty: 0.6,\n\t\t\t\tw: 0.2,\n\t\t\t\th: 0.2,\n\t\t\t\to: 0.4\n\t\t\t}, {\n\t\t\t\tx: 0.55,\n\t\t\t\ty: 0.55,\n\t\t\t\tw: 0.3,\n\t\t\t\th: 0.3,\n\t\t\t\to: 0.6\n\t\t\t}, {\n\t\t\t\tx: 0.50,\n\t\t\t\ty: 0.50,\n\t\t\t\tw: 0.4,\n\t\t\t\th: 0.4,\n\t\t\t\to: 0.8\n\t\t\t}, {\n\t\t\t\tx: 0.45,\n\t\t\t\ty: 0.45,\n\t\t\t\tw: 0.5,\n\t\t\t\th: 0.5,\n\t\t\t\to: 0.9\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.4,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 1\n\t\t\t}];\n\t\tanimation.types.slide = [{\n\t\t\tx: 0.4,\n\t\t\ty: 1,\n\t\t\tw: 0.6,\n\t\t\th: 0.6,\n\t\t\to: 1\n\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.9,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 1\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.9,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 1\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.8,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 1\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.7,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 1\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.6,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 1\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.5,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 1\n\t\t\t}, {\n\t\t\t\tx: 0.4,\n\t\t\t\ty: 0.4,\n\t\t\t\tw: 0.6,\n\t\t\t\th: 0.6,\n\t\t\t\to: 1\n\t\t\t}];\n\t\t/**\n\t\t * Run animation\n\t\t * @param {Object} opt Animation options\n\t\t * @param {Object} cb Callabak after all steps are done\n\t\t * @param {Object} revert Reverse order? true|false\n\t\t * @param {Object} step Optional step number (frame bumber)\n\t\t */\n\t\tanimation.run = function (opt, cb, revert, step) {\n\t\t\tvar animationType = animation.types[isPageHidden() ? 'none' : _opt.animation];\n\t\t\tif (revert === true) {\n\t\t\t\tstep = (typeof step !== 'undefined') ? step : animationType.length - 1;\n\t\t\t} else {\n\t\t\t\tstep = (typeof step !== 'undefined') ? step : 0;\n\t\t\t}\n\t\t\tcb = (cb) ? cb : function () {\n\t\t\t};\n\t\t\tif ((step < animationType.length) && (step >= 0)) {\n\t\t\t\ttype[_opt.type](merge(opt, animationType[step]));\n\t\t\t\t_animTimeout = setTimeout(function () {\n\t\t\t\t\tif (revert) {\n\t\t\t\t\t\tstep = step - 1;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tstep = step + 1;\n\t\t\t\t\t}\n\t\t\t\t\tanimation.run(opt, cb, revert, step);\n\t\t\t\t}, animation.duration);\n\n\t\t\t\tlink.setIcon(_canvas);\n\t\t\t} else {\n\t\t\t\tcb();\n\t\t\t\treturn;\n\t\t\t}\n\t\t};\n\t\t//auto init\n\t\tinit();\n\t\treturn {\n\t\t\tbadge: badge,\n\t\t\tvideo: video,\n\t\t\timage: image,\n\t\t\twebcam: webcam,\n\t\t\treset: icon.reset,\n\t\t\tbrowser: {\n\t\t\t\tsupported: _browser.supported\n\t\t\t}\n\t\t};\n\t});\n\n\t// AMD / RequireJS\n\tif (typeof define !== 'undefined' && define.amd) {\n\t\tdefine([], function () {\n\t\t\treturn Favico;\n\t\t});\n\t}\n\t// CommonJS\n\telse if (typeof module !== 'undefined' && module.exports) {\n\t\tmodule.exports = Favico;\n\t}\n\t// included directly via diff --git a/client/mp3/double.mp3 b/client/mp3/double.mp3 index 8b0d12e32b1e6da56f085ca672ba87ff4df3f879..7867e148ab237d8d78ee67da8ac2025d21f0b5bf 100644 GIT binary patch literal 16557 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�GOt58BP@ofLkjG@v5gthMIy%1j2zfp2kVw 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 Y@oAm#2Jqke@BUBx&j0(4|Czx52Q#RvCIA2c literal 10482 zcmeI%S5Q-57$ES38X_fu00~7y4?RHW#n5~2^-u3eRRKXm??@-1N$((C0i|~V=|xaL ziWKQKRAhBu_GM>wpZ00LmwV5dd*{se{pQS^Z;qyl7zyBCFq#+}tNwjb0RY504i8+# zMTI0VLZX11Uui=C0PT*>1^p|)EkS+LG)R}4x9&Cp|!q;Q5lATdssVrE>#Nu2g1xL?*#CPULw zOB~PHsxoEE&~ooOCq|sTY&wykG2>FhW5n+&BJn`cd%}ev6#B;LRtv8n->)vsMV)c? z5+gp-qJ2Tl8Vz2I%`BN|y4(Vc&;q(G;xzp!c9D})OW5652WPt%9&&CLK=L<}s?YU| z$#SPR*GQVc2A{x0v=lH3)Qq%JVuPVs0Nd35cSL&>#Edw~(B(jm;r8FlmudPb<;z-g)-h)jYXa>w~HfyRhhqI{T~F!q^Of5mc;v;9|6k zpXm}k=bmV-^l86^kau6Xz)?DOBWEjy0peUSR1x5;xd5J4X ze)AQq3;7<+!%^GuXV1~qso#UjD_7$2((kvHNoK9u#YG!gx2ql+_FDa+{SY~G5hDD! z8B~5J3b`9r$(H+7mGD$Q(Kr8LrqSyDaqZ}fryU)-l}By;Pdyb-^&vUZ9U8u0g1BA~ zE*HCc-~P?vam(f0pRD~oF}J@1olkfZF<6Wwni#-_rp1|~u@OmVZon}b8|jR`){GW= z_)mz6co}$3#`*&Q9_cDp{7Kb0LwVO#&mMCUuZzA`kw9bBxzQdmUg(*DdNe6k0L|Lx zj;6yJprKgGzq^m>DQN`6qv5{py*6VnvujR+AVO$HjrZP>?V{0qakpT0mwC;kSCCzM zg-d1a;y1Gy!1Yc`!K!Ja>PWk*cO3d4=VWccdn-Z1+MLv-Ew+ z_jIQ4np@I~E}Z;dqf883P0Dwx>Vi5X3Huiph&>(D=|@vVYeT$NKM$exTek6{Xr0#n z^(CzMQrS2zBj^D8r`h7DB|p;4YAtB8tZS~PLe5crJ!f)wt+$i*Z`G`B0eb9NBS7{v z5#06^L~wiB=sbIW=uFs`?HzkiLbU@PAnF9Zg>^8AJ+bcquoDrFkr3}cNn}s?Cv-b1 zZwd69W^?MzUF@Bh--u4{MsU{Klf^?j^F6bQ;A=tayv|pRCvO#w#a}gzzC8N8(AoX+ z{hn+CpGxY%WJd~Lnl5`|vgN(ZciumrwpKmi(Isnn>SVBHJ+EEzpc9=eL$MlDoX)>~ z62_|v0Y_;^b9@+arccgG?r0$&ofe3=|ByCn$2*|2{Y8RmT2(k_`0Rw3-$3Pt$aD8M zT2;LTE9J@Ke;$sz2i8YY0*gt9vA5AII9W6?&J_}ofu=Hyz)OXUyEt6M)kA)u&`}Iw zl42k~dYqpy)}CD%Aj6QTEAf=%!ASE!gZTO{&lB-SDev71dmdcBUv3ZZvT9BJZqzO; zQvUY5;pzL%z~#Jerb0{^V)RVum{g5=h6Hv`{qaivv{Lq;De)h&{v>=!HRt=LW5g?? z0;U_+dNrT>(o(@U05Yenqqp~kN7 z_ihUL=ggLCdb&MfUQ$8rf{}I!01Fskr;il4*~dwe+}8_hnp4ig56+Xr`-6$2hxz^q zL8Df!z`to;Qw`q1ZbW=Tbdv4&^8jr~$j0C##eF17`xWYq?}`U9%ncrD7$B{M?L)7g z*){tLX!OXtB!F2$J3lZSQGGpNJ!|NkMNeqGhP5~;ep9GVKP7$oBB?EpcD>YtYfYr% z>nagTqhXbe`*teQuYtdzoJE|z)&LHT4t;6F&(sKnU4Y+fdxHk9K~n#M;pJDoIf zSQlpyzc62dJT?2w64O`ntaW%sp{=a@GY{tl`eHP2Mgu?wlmfT{+<~!xGg59M4QeuS zw7e@{RPY`mjdhO%!mftWfVeE6N&CP+0DB@Q6|=x90mxYx@&l!=0PWFp}({ zBtPeAdb=i}i@hE>>|k+O=S+gE-*(D#iZb<}7Aw}DuTRo;?$L;9b8nZBH^V0Q8;ylGI>|1z?f8W7W+sS}uM`gT zeE%}aJ2r!Vn(E0L3Eh!S3YO7-%P|Y7nA%cKEvHUl2>;FoEy$fp4rTIw)Cfk9(5Td# zE7DjDzJpt8>c!I<-P>Rnhkg8U6(@7(*z%z4bqYIEb<)cFA4ggFIMEsQ^1)aP5!j9s z2q?ZwN>-CuFM-bj&f=sL=mGAOWYy(Mj@T+K|Ga}P5_HXE_U@kI3#Nvt1yU^vSc7T) zIN3bn`J+#9d2Z^y{v~&MTRYl(K76w4VSD1fo=dfMa@1@|ekA#p-?d;$zd8SW%KSWo zAM$qZ%IEb(X6PXA^A6XL#QANZ?T>z*L0MbJN6fT8y4)t0!*g%uT?cOI8cE*-6 z7YFOc1%usNTtUJWl*1c=g};73ckM1jz1CIGewyhvShQlZtNu-U1l<{aB`~b$kNPJh zO1#KG^oIsMHIegrqyMi^etJ*yhW?!Q+fMz*l6OVku3Gx)J~vHyjwt9M_$9h@vxY6( zE!|03x#1hxII>FGP#s)C)wlRP^8U}0@NL=R?M;$6eilMvjM*!yj0J2G*~bH7RS%6E z@DyY~9Ib;DS5z_kxon_%yu8$TbF9L=<|6e)(eGakt<**|Ve{{zUrzE zgUYx4%Z84xe^`I4u@k@QNRlD#LhLrzIk`OJUw>4ql{WUA{t@P9Iby3e%QLlfHaC+q zns%c7sDJ&2^}Fzh9l6bs(|>2j?=mi8wpCrf4@-xho*=%z_n01{g#_QFN7KeWP&q_N z!2#%3+7ennM8CL+fEY39>Y_Fg+#I>ee{K<9a4l+fYw_*o0=Ty2ZCr*>91Y<`c>kwutzoqyZTI9#j!BTVm~+kI9Km22H1Uf zryfVb4mT%IKo{;$r18{<0UG$_RmjqIq^sB?8zjE+g2U*hK%|BU*?0h7Kct|Tjru2K zNCfLV`y*8BGm-atW9g+m(0CRkcO8Kxa`3!M4GbCRbMl;?eAOV?m9|$M<-&;@e&N6J z+FWv7HC@-pDaRX&xEjk~$~R&CoN6>(Jm;@Tuj`6wo1V}M_vQ-FsY(#0I+=;xHv8=^ zE#19Lta{1T^uk1MLF89vR=cT6_QP@IgU5Aur$i7bpI$c@=K3j~*zOp+ICEjHFFyp! zCp%MmOrL~5c>3t;6Nc-nlMU})-8Zf$Cnrz+8|G)=i2wk}-xK-dAeNXcHi{&chZ=vT z=h|^FE;aId^Nwq!i#wtG`G^63!5E5@Q5qsYDkc-%x4G+>HZRSTPZM;Q&iPEB>0XbA zZd*IgLLv|89wYQHs7lsE*XLkIb(*+KcKnSeWZGh@k@ncFBE&`L+Oot+)ZL-2FX&p-|T<<|j@M#m& zr8xo2Sgs6R`d~|^x##jjU}l+I@ua0NEKo1c)l|GE9ThEI&o(ha!euGOv_+OB4O{*v zARgm{c4&OQGePIE*eWu|M7Y;Y zX%@8eRbl?g&CNB1^?HTJy!$6X3TaD!PnCaSQM2(cP6&8s|5In&GxH|1 zK!7(mlhi;^8Q@H;Ok@vm0yf=pB1=Y!QS8$=F*MQ53(0>s)`uqyVrDrqZO$C&$W_21 za1t^=F^xS^IGLgtkxX<(Zjn4MMu%cAEoH%`lyjcA^L^;7<*@M^+c6+ugII1WP!`e` z(i8G%_oD<=o3Iz|+I@zcR0i(&lmxy3QVpG-j_hm(={(?JK|XE;8Dfw%Dx|aQkN{z1 zQv0Ia_v|UMncnNGC@D!~dv=A=(@-Q^e46JN!Ty|xd)?)hj_w0BL(S7_zW8Wto=@dK zLWuxhRR+?RCQ${osZrFVVd#_y;aks&K>eoZ{aYGz@S9v^PL10`awnm_f6vw(5sq{@ zhIH&ZP-S(hG8Y*lATHu>$JYk|OcE)b^-+>dQW63B!bo&TNvcP4peYh@Dt-TiXaR_O zcSu&ZaKxM{o4IoKz#m4GkE7rNV|A3q!1@su`UvcyLkmzN14-I)%RwbY@1K>|2 zRcsRHBPEWPWJSohe;l&~<$+MwV0)bEuww*hB*Y_Pge0hNA6I`*jGmem@tuu`J?cYx z>gU5TMS{il2niaAj!m7^0vV^*k>BRPBFX&tJc|9Z_?t{Xqn1>2n9eMAh&^b`Z?}y3 z$<0SD_)ECrr(CDrn?IolXJ{WaD!rKKZ+7-mcDF%mSc=z=F=sqA0 zhw3ZfNwJ)Sy^r3S6as)O$S4JsZ0{yhCj$C)q%B+a`1=WsQ=Kl|u;J|bwPxXWXFX4A z_{nX(s!-NzPTPV@uMWiNGvlrv+K8^oELh*k8U1b?cz^4CW=FA)^gQxjE4`OYt5MA} z9W|zq$__ieo({yya{IZ_xHLKCvu_<|YHsZ=VkNHQhK0Y>rWUjQbw5#`r0*T4%(mW$ z{PWBCTwb>%8{ju`AI%NaN1Li~qs9At(Ve&=G%a8c4ey_kK;$U>6QTw%JZ1(X&T-(! z)NJZKGXUwwnzJ(OR%xQ#Fn;N!?r@AFFdp3w_zof)8;qj}>SHE`4C{m4`?-Pl_n)>s zt{&0VTn*&ncs+qgJyEGbDagP!!taAeiblFmtlY}9)SrD$>$17z<{8)B{kv~bh7L2n z+lypvuMe_p>I=tdnp*GeYkeJmyp`{LwXpZ(=8B+T!Yaa| z&~Sz)!inLj`%riYmK4rYd8jf*enqcJKNLM7xa@FLs)fp=D@$$#DM z0%h;D&@_M)v@os&&4*(}lVcUoOt=cPfT9}uLCg_mKw?(pRfGc?5g8`3hh-9>k7Psh z_BoRQz+gZ;nTG$MeHM~i4Fk2BYp254GOcRs&Kv3K8`b_3;)^teekY+B@)__g-z>SJ z3eijaVs2CJ%PMW<*W$6}bY=2ps&%Qos_2RP`Oiw#Ae3id$oV@X@2#LWLQ9gF8GCIP zmFK(&pJ&ZKxF7~2RWHx|o@2iwEo^f*nt z%$7Gipw$l^UYv`ULcYVd_YL9oH%~|kXzT?^6xgywbJML~rv;(lD^nyj-7>IDoeS!5 z4r7G1Gf1yBP(h&Zr!;2<0j zPixNv7r-6hzxc%Bqx;Z!c<}%{X7l>*ToE3C1(V{4b&)J_+suxaMtePnT?8*{^r@Y9 z(_*N`l})oo_i5s8E*-`>?ay3nyXc{^gs5=UFiZaQZR18!faPZr_j zaI?7p-piFH@Bv8+oS}R-EAr)>{GHD?Dnf6}d4>N8=|#fcr;zB^`%Ji@i&uB3UY0)I z5u+L0MNW0eRG20s_tW^P01;R&ydZ!OSDnj>_H)4hz~DpUEVHmnc-^Q79EhYg^FDEO zIU<~MG^jLZ=ZI>oVU;^gB}Jgpv?6rVyC|FZ1v>WD(**l#g^mVroK#(u;JAb2*~~C> zclk#^Ervb%fOrVNKhgq$4Y^1kzxL*2)*}1ZsE(zoK6k__*pZZADw+a)%rH4Ic1@6$t|4r+_vdpsQ`2=MF3+PJ z9_3gw8iv2{hF)UQe{ZcW5lz&=_%kdN@k2nj2Hx{1+WgM54F=luL2aOS}3RZt}z zU2}*=JsS>9ga-l?altfr1W`%MerHiEbwon210EJz6LS`fM*wK0Thu>`Izy=44psjk z^0MT<7A=3-RczdS)yN;v=*6@$%>H9;+2fy3P^7W{6C#?w{ZLx_CjB;5=b&3xdR}H{ zD!&+)Kuz*U*`z5pkpbiA+3*qG63A6yEZQtIxqLZcztqZ0UFJf|s7^^|1rM;W-yVD3 z&U?GbZ7{|@Hc%u5%%88qEO@)W6K>!!iIa-q1~BUzm;kW=IeWfam6VZVK^@Am;)i2cokCzs&TjK29Zup>(E(UZE^hxJdS>9+liX#zj_8qb-hj%TReNr%m+{=97V zdCo15VQrQk%uP%G<(WJ#c+Yq8-CW9fIjvdH=L8_q6E?azx8M4;1S@(tbes!{%G zviNz^hDT4Et3{r{v%8E;)4ZjFzMs`Rtu(Uwe%IDD}olSEn{2{|P(?p0r!0udnGQ{xjkJWhJa zKm~-vl^|XBKdTTqt#e{1)m1-MGe=jCNOS%i9eojYmEDuXp|e)_V(Bn!?Ny_<%9Qc$ zuBgG}c?e{BmMMkq>%)-8xi7z6(wFVTITzs%lf*yKW$s*yG|wh1hRdt|w~u-FUHZT< z=E?XU(+%%j^QpvB>z#L>UV8-;?7MjtDtdDR{gV52epi!#NCO*~nm}gu`((}zd=7pT zd}$j6qCBSQZd=Q3nQKd-JX~boAj9`uip^|f-VW5U1=Jp+f9BETW|LvMl7_$2K4!?0 z3ky4i3|fUTTCQH&)k^AuT3tl9ih?dacM;q+9=+&n^3e2_DthvBJlI#@JC()jcQ>m= zE_oi7y{7Q_wwg~J4sVSwH10XCox1c$m^$k9UfMmos02gkynT z-3Cf}bVgj1D_&(}vo%}Ps#%#o;h8`5@%<;%8D)}6_J@XYY9O(1V|X6Xc|6At)`}=k z+C<3QXIQcH!JPbZ^UBF9Gy3kj_QTmRZCp99^#rz%EzjyzDTNGirr{Qg%9?u*Ard^V2z>a4~ORH$Npkpp_@-3Yv-x5zv+D80RA4 zA<6OY{}i?j^9c0FZA}mMDbC?{Lvb?D*nSmPj>6YQ5jMOi!d#8Xr9%*3jTg zNoS-ZihXr^&Iz5ArNvtJM_@u%60}WEZTnAXJ&In4@^>SAYTzUGPC^@@Gc`|hI6BK` zLZQUOF}t89UgFS(5On;yutO)e!%|U#JF|?jLw%e^`W9vXi|pivtOAaYCIK!L)>1~A z+DzGe)Go1Z`A(aNF(@AXE&&)7LqF#{)Z$V15B*Ifc`o)s~?Fo;|OkTN%Y0Tx(#% z&l32K;NuIq$&+=u`a*$!ua5SMFKzjHsbgRCzSlR+y1SkiQb_#xIhz!3%afH|=aN$n z2`+$EWE-X|)u1F~>XF_vMNZZSa1mT%5|@m@mV+n>x9fs#^+dDxIEFmd)PL@`(Henk zN&Jw>lJ-vA!MEvEaGj5`bGZl=Cria#2|LX{p$j4=Fy%MF3c*2U#s3_JDmsC1XzZgH z_5`k?P?gw9edFx8U5})E1c6}E{>Q-`rgL2_mpw&_t2J>u#1{0;hMg(868B| zT>36Dtr)Lt&_^|SzQv_|WnGj*gsJ9HbAP240HQ1c|w>i;xl|EIP3$;$7ZO= zb%Yk6i=`;jos^Px@hEMZ9M?@fUgv{^&&Z6gnWk^%Xq`bhUZjF*R?Qa{cU^PZV!855 z*&XTg&?bp0N+rgiRYTKf6_yq;g*u5d^?ZCa1EY+ZbRw#><#kZW6?R=O&+Yj43U#Em z&H{CYYm|v5Va}~TdBrK_L;m#N{U!3%DEpVzb=Zy5?|ZU6cO~nZqZS>NYXLw25L_%G zYCi6-#@SGtJ(93kSru3E=1bCNV~Iia^-hn0fjth?j7Et{S%Ed3`=&EJ1|)H3hTj&0?A`wf9TVqz?ERc9pL&DDei$#W=zOhkhf?t~ zWmr~{*97*Uh+5xB4xqWUMJI3$Qc-C>{j9`uJNP&Qn3w|KP3>dho9Qi@Cqc$cj4xJa zkTV4vMpDJim9bn0Vb)!F`83lbn_@;U=$plnSg@c$xQV}rRU{2>TNiWkkY(Sd=9Vn! zjez&rB`m=sf!&HD;Qaz?vr~tF9$5Q`GFhd0h^UNuoW!Y0GpBrW+MF4$?gjFDa_|`%`y-x5>pTz^m(&7j! zr+O{rE+r#m0gZ_fJ7~nUoW0q3e?Nc=HlUg-t~u`a^0&SmkAPO&)!dHf?{{e|uV1Kz z7}0+%UsF;V+)F0V=echgX>q|)wK!jei|_vvI>*ryO0SbSr?wS)>>97bKClm0OV18< zC*?Eai(QM1xM-r+)P&@MOU0oY7emopmGw~X_8K)!zP&0`1%E>_ZCs6q#oe2(Q>l~I z+lykj#XpiDAITY#j5^zFb&j+lxpz+RG$nJH zPO|s6T%_;fk}ZkfsMG{abYeub4+D-|1D3^-{g)amnklJ?<=E2DvQ6v^VJ-8R-+Zi$ zmgajDKI=|-Cd6I)wv)mW{8CB)A9FoS%oQZ3CiyF@+puH&KN;)TmF0P<7O5;#%NAVM z)a)%iOBKJ45s-mR>8v^T>iD1!bo6_-(4v7+k&(uPWR30`b17_w&mei{p$5fwyYev; z&2}q?pkb=En0;uSD$Op65D52()vo&7{7>kLn3)=Urqe7q(15WUk41F;km1gC6|5il zF|Bx!)D=sZ^ZpP1V%)KIRW%`#9?4OF@JMJX8u2njP1LlE;pG`TI4}m8%WR%+#V1`O zT!)DOT;0xYeM+CJQP>GhunU!f!AHFV>u!+O47JhZSs)nH~dr| z612n*cSlXtDwb$T(iDYiOYN-)Am+Cw40W<@FO~t7nxMtRY``r zYT4Pl6X1#c`TQ8UvzWHl+~~HXm(IGgv*7N@W*`6NNlApV|4sh+ZLWX7Xzk zc{I_t@&KcLb9D#Y!roB5d+J-N9T4670d4v0H^#m5ZINTV6 z9GnORi4-@{*iX(stYp_%o2 z*3R8NDjEjKDPr#AjP&C!*21O=_K_M8Q!DYE3{Fzj0JuR8CCrUUx$@lfwxoCh#A5ow#%jTu--pCD300^#(eC%|xOcf#%hA@VMwX}N2^eayfQ7>4EVS)x z;KzoN z2mwNZt_mv7Q?uCcQff6E+FvKoCMZ$~?Z*)y>;@oo%ggYBEvxIu5dDs?^nP_3dC@ zm798rBgA-YEnRliiC7V&t**S^Ogp+ROuP3%qp4GCvq>D(PbOA zi`FhhsjoSN6Cj^F7PVoQeE+Mmwf~RepU`XK%{}l(_2%O-004vgdnfk)Xzl;M%>PXV F{s&K>WuyQA diff --git a/client/mp3/timeout.mp3 b/client/mp3/timeout.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..68d843589948ccc32f38eeeba2ac8635c0a112c2 GIT binary patch literal 7725 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 { return false } return gemColors.map((color: GemT) => { var cName = color + "chip" if (color === '*') cName = "schip" + const clickable = color !== '*' return ( -
+
{gems[color]}
-
{symbol}
+
{clickable ? symbol : ''}
); }); @@ -375,7 +383,10 @@ let globalShowError = (resp: ServerResponse) => { return false } } } - class Game extends React.PureComponent<{ gid: string, pid: number, uuid: string }, GameState> { +class Game extends React.PureComponent<{ gid: string, pid: number, uuid: string }, GameState> { + ticker: number | null = null + timeoutAudio: HTMLAudioElement | null = null + state = { players: [], gems: {}, @@ -393,6 +404,9 @@ let globalShowError = (resp: ServerResponse) => { return false } showChat: false, showLog: false, chatNotify: false, + turn_seconds: 30, + turn_deadline: null, + nowMs: Date.now(), } as GameState isMyTurn = (turn: number) => { @@ -423,6 +437,9 @@ let globalShowError = (resp: ServerResponse) => { return false } gems: r.state.gems, nobles: r.state.nobles, turn: r.state.turn, + turn_seconds: r.state.turn_seconds, + turn_deadline: r.state.turn_deadline, + nowMs: Date.now(), }); if (r.state.winner !== null && this.state.phase != "postgame") { @@ -516,6 +533,47 @@ let globalShowError = (resp: ServerResponse) => { return false } componentDidMount() { this.stat() this.poll() + this.ticker = window.setInterval(() => this.setState({ nowMs: Date.now() }), 1000) + this.timeoutAudio = document.getElementById("timeout") as HTMLAudioElement | null + if (this.timeoutAudio) { + this.timeoutAudio.loop = true + } + } + + componentDidUpdate() { + this.syncTimeoutAudio() + } + + componentWillUnmount() { + if (this.ticker !== null) { + window.clearInterval(this.ticker) + } + this.stopTimeoutAudio() + } + + turnRemaining = () => { + if (this.state.turn < 0 || !this.state.turn_deadline) return null + const remaining = Math.ceil(this.state.turn_deadline - this.state.nowMs / 1000) + return Math.max(0, remaining) + } + + stopTimeoutAudio = () => { + if (!this.timeoutAudio) return + this.timeoutAudio.pause() + this.timeoutAudio.currentTime = 0 + } + + syncTimeoutAudio = () => { + if (!this.timeoutAudio) return + const remaining = this.turnRemaining() + const shouldPlay = remaining !== null && remaining > 0 && remaining <= 10 && this.state.turn === this.props.pid + if (shouldPlay) { + if (this.timeoutAudio.paused) { + this.timeoutAudio.play().catch(() => {}) + } + } else { + this.stopTimeoutAudio() + } } chat = async (e: React.KeyboardEvent) => { @@ -578,6 +636,7 @@ let globalShowError = (resp: ServerResponse) => { return false } /> ) }); + const turnRemaining = this.turnRemaining() return (
@@ -617,8 +676,15 @@ let globalShowError = (resp: ServerResponse) => { return false }
- {this.state.turn >= 0 && this.props.pid >= 0 && this.props.pid < 4 && - + {(this.state.turn >= 0 && this.props.pid >= 0 && this.props.pid < 4) && +
+ {turnRemaining !== null && +
+ {this.state.turn === this.props.pid ? "Your turn" : "Turn"}: {turnRemaining}s +
+ } + +
}
); @@ -636,6 +702,7 @@ let globalShowError = (resp: ServerResponse) => { return false } loading: boolean lobby: boolean gameName: string + turnSecondsInput: string joined: boolean pid: number uuid: string @@ -654,6 +721,7 @@ let globalShowError = (resp: ServerResponse) => { return false } uuid: '', gid: '', gameName: '', + turnSecondsInput: '30', errorOpacity: 0, error: null, } as GameCreatorState @@ -717,7 +785,13 @@ let globalShowError = (resp: ServerResponse) => { return false } if (this.creating) return this.creating = true const gameName = this.state.gid === '' ? this.state.gameName : this.state.gid - const resp = await fetch(`/create/${gameName}`, { method: "POST" }) + const parsedTurnSeconds = parseInt((this.state.turnSecondsInput || '').trim(), 10) + const turnSeconds = Number.isFinite(parsedTurnSeconds) && parsedTurnSeconds > 0 ? parsedTurnSeconds : 30 + const resp = await fetch(`/create/${gameName}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ turnSeconds }), + }) const json = await resp.json() if (this.showError(json)) return @@ -790,6 +864,10 @@ let globalShowError = (resp: ServerResponse) => { return false } this.setState({ gameName: e.target.value }) } + turnSecondsChange = (e: React.ChangeEvent) => { + this.setState({ turnSecondsInput: e.target.value }) + } + keyPress = (e: React.KeyboardEvent) => { if (e.key === "Enter") this.createGame() } @@ -805,9 +883,19 @@ let globalShowError = (resp: ServerResponse) => { return false } return
Splendor
Play Splendor online with others. Enter a game name or use the suggested game name to start a game.
-
- - +
+
+ Game Name: + +
+
+ Turn Time: + + seconds / turn +
+
+ +
diff --git a/docker-compose.yml b/docker-compose.yml index f33afe6..9c9d9d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,3 +10,9 @@ services: - ./client:/client working_dir: / command: python /server/splendor.py + client-build: + image: node:16-alpine + volumes: + - ./client:/client + working_dir: /client + command: sh -lc "npm install && npx webpack --config webpack.config.js --entry ./src/index.tsx --output ./dist/main.js" diff --git a/server/player_and_game.py b/server/player_and_game.py index d62733a..6f5aad6 100644 --- a/server/player_and_game.py +++ b/server/player_and_game.py @@ -336,7 +336,7 @@ def noble_from_dict(obj): return self class Game(object): - def __init__(self): + def __init__(self, turn_seconds=30): level_1 = [ Card('b', 0, 1, 1, 1, 1, 0), Card('b', 0, 1, 2, 1, 1, 0), @@ -472,6 +472,8 @@ def __init__(self): card.level = level self.updated_at = time.time() + self.turn_seconds = turn_seconds + self.turn_deadline = None def dict(self, player_id=None): if player_id is None: @@ -488,6 +490,8 @@ def dict(self, player_id=None): 'decks': {}, 'winner': self.winner, 'turn': self.active_player_index, + 'turn_seconds': self.turn_seconds, + 'turn_deadline': self.turn_deadline, } for level in LEVELS: result['cards'][level] = array_dict(self.cards[level]) @@ -524,6 +528,8 @@ def private_dict(self): 'winner': self.winner, 'state': self.state, 'turn': self.active_player_index, + 'turn_seconds': self.turn_seconds, + 'turn_deadline': self.turn_deadline, } def log(self, msg): @@ -585,11 +591,22 @@ def next_turn(self): self.state = 'postgame' self.winner = self.determine_winner() self.active_player_index = -1 + self.turn_deadline = None else: self.active_player().start_turn() self.refill() + self.turn_deadline = time.time() + self.turn_seconds return {} + def is_turn_timed_out(self): + if self.state != 'game': + return False + if self.active_player_index < 0: + return False + if self.turn_deadline is None: + return False + return time.time() >= self.turn_deadline + def determine_winner(self): players = [(p.tiebreak_score(), p.id) for p in self.players[:self.num_players]] return sorted(players)[-1][1] @@ -646,6 +663,10 @@ def game_from_dict(obj): self.pids = obj['pids'] self.state = obj['state'] self.active_player_index = obj['turn'] + self.turn_seconds = obj.get('turn_seconds', 30) + self.turn_deadline = obj.get('turn_deadline') + if self.state == 'game' and self.active_player_index >= 0 and self.turn_deadline is None: + self.turn_deadline = time.time() + self.turn_seconds self.num_players = obj['num_players'] self.players = [player_from_dict(p, self) if p else None for p in obj['players']] for k, v in obj['cards'].iteritems(): @@ -654,4 +675,3 @@ def game_from_dict(obj): self.decks[k] = [card_from_dict(c) for c in v] self.nobles = [noble_from_dict(n) for n in obj['nobles']] return self - diff --git a/server/splendor.py b/server/splendor.py index daa7284..999dcd0 100644 --- a/server/splendor.py +++ b/server/splendor.py @@ -14,6 +14,7 @@ client_dir = os.path.join(os.getcwd(), 'client') words = [] num_created = 0 +DEFAULT_TURN_SECONDS = 30 def json_response(f): @wraps(f) @@ -23,13 +24,13 @@ def decorated_function(*args, **kwargs): return decorated_function class GameManager(object): - def __init__(self, name): + def __init__(self, name, turn_seconds=DEFAULT_TURN_SECONDS): global game_map self.uuid = name self.starter = uuid.uuid4().hex game_map[self.uuid] = self - self.game = Game() + self.game = Game(turn_seconds) self.changed = {} self.chats = [] self.ended = {} @@ -59,6 +60,10 @@ def poll(self, pid): global game_map while not self.changed[pid]: + if self.game.is_turn_timed_out(): + self.game.next() + self.has_changed() + continue time.sleep(POLL_INTERVAL) yield " " @@ -161,7 +166,16 @@ def create_game(game): if game in game_map: return {'result': {'error': 'Game already exists, try another name'}} - new_game = GameManager(game) + payload = request.get_json(force=True, silent=True) or {} + turn_seconds = payload.get('turnSeconds', DEFAULT_TURN_SECONDS) + try: + turn_seconds = int(turn_seconds) + except (TypeError, ValueError): + turn_seconds = DEFAULT_TURN_SECONDS + if turn_seconds <= 0: + turn_seconds = DEFAULT_TURN_SECONDS + + new_game = GameManager(game, turn_seconds) num_created += 1 return {'game': new_game.uuid, 'start': new_game.starter, 'state': new_game.game.dict()} @@ -347,4 +361,4 @@ def save_and_exit(number, frame): pass signal.signal(signal.SIGHUP, save_and_exit) - app.run(host='127.0.0.1', port=8000, threaded=True) + app.run(host='0.0.0.0', port=8000, threaded=True)
\n
\n
\n Share this link with friends to join in or watch: {`${document.location.href}`}\n
\n
\n {!this.state.joined &&\n \n }\n {this.state.startKey && this.state.pid == 0 &&\n \n }\n
\n
\n {this.state.pid >= 0 && this.state.gid && this.state.uuid &&\n \n }\n \n