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 8b0d12e..7867e14 100644 Binary files a/client/mp3/double.mp3 and b/client/mp3/double.mp3 differ diff --git a/client/mp3/timeout.mp3 b/client/mp3/timeout.mp3 new file mode 100644 index 0000000..68d8435 Binary files /dev/null and b/client/mp3/timeout.mp3 differ diff --git a/client/src/index.tsx b/client/src/index.tsx index 5f8a7b5..9430e3d 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -66,6 +66,8 @@ interface GameT { nobles: NobleT[] winner: number | null turn: number + turn_seconds?: number + turn_deadline?: number | null } interface ChatT { @@ -83,6 +85,7 @@ interface GameState extends GameT { showLog: boolean chat: ChatT[] chatNotify: boolean + nowMs: number } interface ServerResponse { @@ -107,10 +110,15 @@ let globalShowError = (resp: ServerResponse) => { 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