├── .gitignore ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── controllers ├── AmbientStatusController.ts ├── Application.ts ├── ChannelController.ts ├── ConnectionController.ts ├── ControllerBase.ts ├── HomeController.ts ├── LayoutController.ts ├── SoundController.ts ├── ViewControllerBase.ts └── channels │ ├── ChallengeChannel.ts │ ├── ChannelBase.ts │ ├── ConversationChannel.ts │ ├── GameChannel.ts │ ├── ListChannel.ts │ └── RoomChannel.ts ├── gulpfile.js ├── images ├── avatar-default.png ├── loading-primary-lighter.gif ├── stone-black.png ├── stone-white.png └── wood.jpg ├── index.html ├── jsconfig.json ├── kgs ├── Constants.ts ├── DataDigest.ts ├── Database.ts ├── JSONClient.ts ├── RegularExpressions.tests.ts └── protocol │ ├── DataTypes.ts │ ├── Downstream.ts │ ├── SGF.ts │ └── Upstream.ts ├── models ├── AutomatchState.ts ├── Channel.ts ├── ChannelType.ts ├── Chat.ts ├── ChatImportance.ts ├── GameActions.ts ├── GameChannel.ts ├── GameClock.ts ├── GameMarks.ts ├── GamePhase.ts ├── GamePosition.tests.ts ├── GamePosition.ts ├── GameResult.ts ├── GameRules.ts ├── GameState.tests.ts ├── GameState.ts ├── GameStone.ts ├── GameTree.ts ├── GameTreeNode.ts ├── GameType.ts ├── PlayerTeam.ts ├── RoomChannel.ts ├── User.tests.ts └── User.ts ├── package.json ├── scss ├── _classes.scss ├── _font-awesome.scss ├── _layout.scss ├── _reset.scss ├── _variables.scss └── leben.scss ├── sounds ├── pass.m4a ├── pass.ogg ├── stone.m4a └── stone.ogg ├── tsconfig.json ├── tsd ├── jquery │ └── jquery.d.ts ├── mocha │ └── mocha.d.ts ├── node │ └── node.d.ts └── webkitAudioContext.d.ts ├── utilities ├── Array.ts ├── Audio.ts ├── HTML.ts ├── Log.ts ├── Object.ts ├── Random.tests.ts ├── Set.ts └── Types.ts ├── views ├── DataBoundList.ts ├── Templates.ts ├── View.ts ├── ambient-status-panel │ ├── AmbientStatusPanel.html │ ├── AmbientStatusPanel.scss │ └── AmbientStatusPanel.ts ├── automatch-form │ ├── AutomatchForm.html │ ├── AutomatchForm.scss │ └── AutomatchForm.ts ├── channel-list │ └── ChannelList.ts ├── chat-form │ ├── ChatForm.scss │ └── ChatForm.ts ├── chat-member-list │ ├── ChatMemberList.scss │ └── ChatMemberList.ts ├── chat-message-list │ ├── ChatMessageList.scss │ └── ChatMessageList.ts ├── form │ ├── Button.scss │ ├── Form.scss │ ├── Input.scss │ └── Switch.scss ├── game-list │ ├── GameList.scss │ └── GameList.ts ├── game-proposal │ ├── GameProposal.html │ ├── GameProposal.scss │ └── GameProposal.ts ├── game-report │ ├── GameReport.html │ ├── GameReport.scss │ └── GameReport.ts ├── game-table-body │ ├── GameTableBody.scss │ └── GameTableBody.ts ├── go-board-player │ ├── GoBoardPlayer.html │ ├── GoBoardPlayer.scss │ └── GoBoardPlayer.ts ├── go-board │ ├── GoBoard.html │ ├── GoBoard.scss │ └── GoBoard.ts ├── go-clock │ ├── GoClock.scss │ └── GoClock.ts ├── home-main │ ├── HomeMain.html │ ├── HomeMain.scss │ └── HomeMain.ts ├── home-sidebar │ ├── HomeSidebar.scss │ └── HomeSidebar.ts ├── lcd-clock │ ├── LCDClock.html │ ├── LCDClock.scss │ └── LCDClock.ts ├── lcd-counter │ ├── LCDCounter.html │ ├── LCDCounter.scss │ └── LCDCounter.ts ├── lcd-display │ ├── LCDDisplay.html │ ├── LCDDisplay.scss │ └── LCDDisplay.ts ├── lightbox-container │ ├── LightboxContainer.scss │ └── LightboxContainer.ts ├── safety-button │ ├── SafetyButton.html │ ├── SafetyButton.scss │ └── SafetyButton.ts ├── sidebar-menu │ ├── SidebarMenu.scss │ └── SidebarMenu.ts ├── sign-in-form │ ├── SignInForm.html │ ├── SignInForm.scss │ └── SignInForm.ts └── table-body │ └── TableBody.ts └── wgo.js ├── LICENSE ├── wgo.d.ts └── wgo.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js NPM dependency directory 2 | node_modules 3 | 4 | # Artefacts produced by Gulp script 5 | .build 6 | dist 7 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | Contributors to *KGS Leben* 2 | =========================== 3 | 4 | * **[Stephen "Charlie" Martindale](https://github.com/stephenmartindale/)** 5 | 6 | * Founder and author of the project. 7 | 8 | Special Thanks 9 | ============== 10 | 11 | * **[Bill "wms" Shubert](https://plus.google.com/+Gokgs)** 12 | 13 | * Creator of the [KGS Go Server](http://www.gokgs.com/) itself and author of the [JSON API](https://www.gokgs.com/help/protocol.html). Without these, this project simply would not exist. 14 | 15 | * **[Jan "waltheri" Prokop](http://wgo.waltheri.net/)** 16 | 17 | * Author of [WGo.js](https://github.com/waltheri/wgo.js), an open-source JavaScript implementation of a _Goban_, released under the [MIT License](wgo.js/LICENSE) and used (forked) for rending the Goban in _KGS Leben_. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Stephen Martindale 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | KGS Leben 2 | ========= 3 | 4 | *KGS Leben* is a client for the [KGS Go Server](http://www.gokgs.com/), written for modern, standards-compliant web browsers such as Chrome, Firefox and Microsoft Edge. It connects to the server via the [JSON API for KGS](https://www.gokgs.com/help/protocol.html) and has no other dynamic, server-side dependencies. The source-code for the client is released under the [MIT License](LICENSE) and a list of [contributors and noteable people](CONTRIBUTORS.md) can be found in the root of the source tree. 5 | 6 | The *KGS Leben* project is only a few months old and not presently suited for general consumption. The following features have been implemented: 7 | 8 | * **Spectating** -- Games played by other people can be joined and the user can watch as play continues. _Kibitz_ messages sent to the chat box will be broadcast to other spectators in the channel but the chat feature is currently non-functional in _game_ channels so the user will see neither these nor the messages typed by others. 9 | * **Game Playing** -- Once a game has started in which the user is a player, the user can play moves, pass, resign, win and lose on time and mark live and dead groups, should the game proceed to the scoring phase, and agree on the game's result. Sound cues are played for stones and *pass* moves. 10 | * **Joining open Challenges** -- The user can join an existing _game challenge_ in a room, negotiate the terms of the game via the usual dialogue and proceed to play. 11 | * **Auto-match** -- On the _Home_ view, in the sidebar, a selection of controls allows the user to set their preferences for the _automatic match-making queue_ and provides a button to join (and leave) the queue. Preferences are synchronised with the server automatically. 12 | * **Chatting in Rooms** -- Conversing in _rooms_ is operational in both directions. Upon a successful sign-in, the user will join the rooms that they were in when they last signed out of the server, from the _Leben_ client or the legacy one, _Cgoban_. Joining new rooms is not implemented simply because no room-list screen has been created - behind the scenes, work to do this is complete. 13 | 14 | ##Project Status 15 | 16 | *KGS Leben* is currently **on hold**. That is to say, I simply do not have time to work on the project at present due to a career-related change in priorities. [Ilya Kirillov's *Go Universe*](https://github.com/IlyaKirillov/GoUniverse) looks like another promising project for Go players to follow. 17 | 18 | ##Technologies 19 | 20 | *KGS Leben* is implemented _low-tech_, without undue dependence on third-party frameworks or packages. The artefacts produced by the build-script are all static content which can be served by even the most primitive HTTP web-server and they depend only on the JQuery Javascript library. 21 | 22 | Development dependencies are limited: 23 | 24 | * [Node.js](http://nodejs.org/) and its package manager, NPM 25 | * [Sass](http://sass-lang.com/) for convenient and maintainable style-sheets 26 | * [TypeScript](http://www.typescriptlang.org/) provides type-safety for scripts and insulates the code-base from obscure JavaScript quirks 27 | * [Gulp](http://gulpjs.com/) is used as a flexible and scriptable build system 28 | * [Mocha](https://mochajs.org/) provides a framework for testing domain models and algorithms 29 | 30 | ##Building the Source 31 | 32 | Prior to compiling artefacts to be served, one must first install [Node.js](http://nodejs.org/) and clone the source repository. Thereafter, simply follow the instructions below. (Command-line commands should be executed relative to the root of the cloned source tree; paths are also relative.) 33 | 34 | 1. Use NPM to fetch the dependencies and development dependencies defined in [package.json](package.json) 35 | 36 | `npm install` 37 | 38 | 2. Execute the _build_ script defined in [package.json](package.json). This will run the Gulp task: _build_ 39 | 40 | `npm run build` 41 | 42 | 3. A sub-directory named `dist` will be produced, containing all artefacts required to host _KGS Leben_. 43 | 44 | ##Running the Unit-Tests 45 | 46 | The suite of automated tests for various domain models and algorithms within the project, powered by [Mocha](https://mochajs.org/), can be built and executed with a single NPM script: 47 | 48 | npm test 49 | 50 | ##Serving _KGS Leben_ 51 | 52 | Any HTTP web-server should be able to serve the _KGS Leben_ web application but the path to the [JSON API](https://www.gokgs.com/help/protocol.html) is currently not configurable so the ability to serve Java Servlets is also required. [Apache Tomcat](http://tomcat.apache.org/) is recommended. The web-server must be configured to serve two _routes_: 53 | 54 | * `/` should route to the static content found in the `dist` sub-directory produced by the build script 55 | * `/jsonClient/` should route to the [KGS JSON Client Servlet](https://www.gokgs.com/help/protocol.html) (distributed as a .WAR archive) 56 | 57 | ##See Also 58 | 59 | * [Probabilism](https://probabilism.wordpress.com/category/kgs-leben/) for news on the project 60 | * [Life in 19x19](http://www.lifein19x19.com/forum/viewtopic.php?f=24&t=13145) for a discussion thread 61 | * [KGS Client Coding Google Group](https://groups.google.com/forum/#!forum/kgs-client-coding) for discussions related to the [KGS JSON API](https://www.gokgs.com/help/protocol.html) 62 | -------------------------------------------------------------------------------- /controllers/AmbientStatusController.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | namespace Controllers { 4 | export class AmbientStatusController extends ViewControllerBase { 5 | private _view: Views.AmbientStatusPanel; 6 | 7 | constructor(parent: ChannelController) { 8 | super(parent); 9 | 10 | this.initialisePanel(); 11 | } 12 | 13 | private initialisePanel() { 14 | this._view = new Views.AmbientStatusPanel(); 15 | this._view.automatchCancelCallback = this._automatchCancelCallback; 16 | 17 | this.registerView(this._view, LayoutZone.Sidebar, (digest?: KGS.DataDigest) => { 18 | if ((digest == null) || (digest.automatch)) { 19 | this._view.automatchSeeking = this.database.automatch.seeking; 20 | } 21 | }); 22 | } 23 | 24 | private _automatchCancelCallback = () => { 25 | if (this.database.automatch.seeking) { 26 | this.client.post({ type: KGS.Upstream._AUTOMATCH_CANCEL }); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /controllers/Application.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | namespace Controllers { 4 | export class Application extends ControllerBase { 5 | private _layoutController: Controllers.LayoutController; 6 | private _connectionController: Controllers.ConnectionController; 7 | private _channelController: Controllers.ChannelController; 8 | private _soundController: Controllers.SoundController; 9 | 10 | constructor() { 11 | super(null); 12 | 13 | this.application = this; 14 | this.client = new KGS.JSONClient((m?: string) => this.logout(m), 15 | (d: KGS.DataDigest) => this.digest(d)); 16 | this.database = this.client.database; 17 | 18 | this._layoutController = new Controllers.LayoutController(this); 19 | this._connectionController = new Controllers.ConnectionController(this); 20 | this._channelController = new Controllers.ChannelController(this); 21 | this._soundController = SoundController.initialise(this); 22 | } 23 | 24 | private static initialise() { 25 | $app = new Controllers.Application(); 26 | } 27 | 28 | public get layout(): Controllers.LayoutController { 29 | return this._layoutController; 30 | } 31 | 32 | public get sounds(): Controllers.SoundController { 33 | return this._soundController; 34 | } 35 | 36 | public reinitialise() { 37 | this.layout.main.clear(); 38 | this.layout.sidebar.clear(); 39 | this._channelController.reinitialise(); 40 | } 41 | } 42 | } 43 | 44 | declare var $app: Controllers.Application; 45 | -------------------------------------------------------------------------------- /controllers/ConnectionController.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | namespace Controllers { 4 | export class ConnectionController extends ControllerBase { 5 | private _signInForm: Views.SignInForm; 6 | 7 | constructor(parent: Application) { 8 | super(parent); 9 | 10 | this.beginSignIn(null, true); 11 | window.setTimeout(() => this.attemptAutomaticSignIn(), 80); 12 | } 13 | 14 | private beginSignIn(errorNotice?: string, suppressTransitions?: boolean) { 15 | if (!this._signInForm) { 16 | this._signInForm = new Views.SignInForm(); 17 | this.application.layout.showLightbox(this._signInForm, suppressTransitions); 18 | } 19 | 20 | this._signInForm.errorNotice = errorNotice; 21 | this._signInForm.submitCallback = (form) => this.submitSignIn(); 22 | this._signInForm.focus(); 23 | } 24 | 25 | private attemptAutomaticSignIn() { 26 | if (null == Storage) return; 27 | 28 | if (this._signInForm) { 29 | let lastUsername = localStorage.getItem("KGSUsername"); 30 | if ((lastUsername) && (this._signInForm.username == lastUsername) && (this._signInForm.password)) { 31 | this._signInForm.rememberMe = true; 32 | this.submitSignIn(); 33 | return; 34 | } 35 | } 36 | 37 | localStorage.removeItem("KGSUsername"); 38 | } 39 | 40 | private submitSignIn() { 41 | this._signInForm.disabled = true; 42 | this._signInForm.errorNotice = null; 43 | 44 | let username: string = this._signInForm.username; 45 | if (Utils.isDefined(Storage)) { 46 | if (this._signInForm.rememberMe) 47 | localStorage.setItem("KGSUsername", username); 48 | else 49 | localStorage.removeItem("KGSUsername"); 50 | } 51 | 52 | this.application.reinitialise(); 53 | 54 | let password: string = this._signInForm.password; 55 | this.client.loginAsync(username, password).done(() => this.signInSuccess()).fail(() => this.signInFailed()); 56 | } 57 | 58 | private signInSuccess() { 59 | this.application.layout.hideLightbox(); 60 | } 61 | 62 | private signInFailed() { 63 | this._signInForm.errorNotice = "Invalid username or password."; 64 | this._signInForm.password = ""; 65 | this._signInForm.disabled = false; 66 | this._signInForm.focus(); 67 | } 68 | 69 | protected logout(message?: string) { 70 | this.beginSignIn("You have been disconnected."); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /controllers/ControllerBase.ts: -------------------------------------------------------------------------------- 1 | namespace Controllers { 2 | export abstract class ControllerBase> { 3 | protected application: Controllers.Application; 4 | protected client: KGS.JSONClient; 5 | protected database: KGS.Database; 6 | 7 | protected parent: ParentType; 8 | protected children: ControllerBase[]; 9 | 10 | constructor(parent: ParentType) { 11 | this.attachParent(parent); 12 | this.children = []; 13 | } 14 | 15 | protected attachParent(parent: ParentType) { 16 | if (this.parent) { 17 | this.detachParent(); 18 | } 19 | 20 | this.parent = parent; 21 | if (this.parent) this.parent.children.push(this); 22 | 23 | this.resolveApplication(); 24 | } 25 | protected resolveApplication() { 26 | this.application = (this.parent)? this.parent.application : null; 27 | this.client = (this.application)? this.application.client : null; 28 | this.database = (this.client)? this.client.database : null; 29 | } 30 | 31 | protected detachChildren() { 32 | for (let c = (this.children.length - 1); c >= 0; --c) { 33 | this.children[c].detach(); 34 | } 35 | } 36 | protected detachParent() { 37 | if (this.parent) { 38 | for (let c = (this.parent.children.length - 1); c >= 0; --c) { 39 | if (this === this.parent.children[c]) { 40 | this.parent.children.splice(c, 1); 41 | break; 42 | } 43 | } 44 | } 45 | 46 | this.parent = null; 47 | this.resolveApplication(); 48 | } 49 | public detach() { 50 | this.detachChildren(); 51 | this.detachParent(); 52 | } 53 | 54 | protected logout(message?: string) { 55 | for (let c = 0; c < this.children.length; ++c) { 56 | this.children[c].logout(message); 57 | } 58 | } 59 | 60 | protected digest(digest: KGS.DataDigest) { 61 | for (let c = 0; c < this.children.length; ++c) { 62 | this.children[c].digest(digest); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /controllers/HomeController.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Controllers { 3 | export class HomeController extends ViewControllerBase { 4 | public static channelId: number = 0; 5 | 6 | private _homeSidebar: Views.HomeSidebar; 7 | 8 | constructor(parent: ChannelController) { 9 | super(parent); 10 | 11 | this.initialiseMainView(); 12 | this.initialiseSidebarView(); 13 | } 14 | 15 | private get user(): Models.User { 16 | return this.database.users[this.database.username]; 17 | } 18 | 19 | private initialiseMainView() { 20 | var view = new Views.HomeMain(); 21 | 22 | this.registerView(view, LayoutZone.Main, (digest?: KGS.DataDigest) => { 23 | if ((digest == null) || (digest.users[this.database.username])) { 24 | view.update(this.user); 25 | } 26 | }); 27 | } 28 | 29 | private initialiseSidebarView() { 30 | this._homeSidebar = new Views.HomeSidebar(); 31 | this._homeSidebar.automatchForm.automatchPreferencesCallback = this._automatchPreferencesCallback; 32 | this._homeSidebar.automatchForm.automatchSeekCallback = this._automatchSeekCallback; 33 | this._homeSidebar.automatchForm.automatchCancelCallback = this._automatchCancelCallback; 34 | 35 | this.registerView(this._homeSidebar, LayoutZone.Sidebar, (digest?: KGS.DataDigest) => { 36 | if ((digest == null) || (digest.automatch) || (digest.users[this.database.username])) { 37 | let denyRankEstimate = (this.user) && (null != Models.User.rankToRating(this.user.rank)); 38 | this._homeSidebar.automatchForm.update(this.database.automatch, denyRankEstimate); 39 | } 40 | }); 41 | } 42 | 43 | private postAutomatchMessage(type: string, estimatedRank: string, maxHandicap: number, criteria: Models.AutomatchCriteria) { 44 | let message = { 45 | type: type, 46 | 47 | maxHandicap: ((maxHandicap != null)? maxHandicap : 9), 48 | 49 | freeOk: ((criteria & Models.AutomatchCriteria.FreeGames) != 0), 50 | rankedOk: ((criteria & Models.AutomatchCriteria.RankedGames) != 0), 51 | 52 | robotOk: ((criteria & Models.AutomatchCriteria.RobotPlayers) != 0), 53 | humanOk: ((criteria & Models.AutomatchCriteria.HumanPlayers) != 0), 54 | unrankedOk: ((criteria & Models.AutomatchCriteria.UnrankedPlayers) != 0), 55 | 56 | blitzOk: ((criteria & Models.AutomatchCriteria.BlitzSpeed) != 0), 57 | fastOk: ((criteria & Models.AutomatchCriteria.FastSpeed) != 0), 58 | mediumOk: ((criteria & Models.AutomatchCriteria.MediumSpeed) != 0) 59 | }; 60 | 61 | let denyRankEstimate = (this.user) && (null != Models.User.rankToRating(this.user.rank)); 62 | if ((!denyRankEstimate) && (estimatedRank)) { 63 | message.estimatedRank = estimatedRank; 64 | } 65 | 66 | this.client.post(message); 67 | } 68 | 69 | private _automatchPreferencesCallback = (estimatedRank: string, maxHandicap: number, criteria: Models.AutomatchCriteria) => { 70 | this.postAutomatchMessage(KGS.Upstream._AUTOMATCH_SET_PREFS, estimatedRank, maxHandicap, criteria); 71 | } 72 | 73 | private _automatchSeekCallback = (estimatedRank: string, maxHandicap: number, criteria: Models.AutomatchCriteria) => { 74 | if (!this.database.automatch.seeking) { 75 | this.postAutomatchMessage(KGS.Upstream._AUTOMATCH_CREATE, estimatedRank, maxHandicap, criteria); 76 | } 77 | } 78 | 79 | private _automatchCancelCallback = () => { 80 | if (this.database.automatch.seeking) { 81 | this.client.post({ type: KGS.Upstream._AUTOMATCH_CANCEL }); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /controllers/LayoutController.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | namespace Controllers { 4 | export const enum LayoutZone { 5 | Main, 6 | Sidebar, 7 | Lightbox 8 | } 9 | 10 | export class LayoutZoneController { 11 | private _container: HTMLElement; 12 | private _views: { wrapper: HTMLDivElement, view: Views.View }[]; 13 | 14 | constructor(container: HTMLElement) { 15 | this._container = container; 16 | this._views = []; 17 | } 18 | 19 | public show(view: Views.View): HTMLDivElement { 20 | let wrapper = document.createElement('div'); 21 | this._container.appendChild(wrapper); 22 | 23 | view.attach(wrapper); 24 | 25 | this._views.push({ wrapper: wrapper, view: view }); 26 | 27 | view.activate(); 28 | 29 | return wrapper; 30 | } 31 | 32 | public hide(view: Views.View): boolean { 33 | for (let j = (this._views.length - 1); j >= 0; --j) { 34 | if (this._views[j].view == view) { 35 | view.deactivate(); 36 | this._container.removeChild(this._views[j].wrapper); 37 | this._views.splice(j, 1); 38 | return true; 39 | } 40 | } 41 | 42 | return false; 43 | } 44 | 45 | public clear() { 46 | if (this._views) { 47 | for (let j = 0; j < this._views.length; ++j) { 48 | this._views[j].view.deactivate(); 49 | } 50 | 51 | this._views.length = 0; 52 | } 53 | 54 | $(this._container).children().detach(); 55 | } 56 | } 57 | 58 | export class LayoutController extends ControllerBase { 59 | private _lightbox: Views.LightboxContainer; 60 | 61 | public main: LayoutZoneController; 62 | public sidebar: LayoutZoneController; 63 | 64 | constructor(parent: Application) { 65 | super(parent); 66 | $layout = this; 67 | 68 | let mainContainer = document.querySelector('#main') as HTMLDivElement; 69 | this.main = new LayoutZoneController(mainContainer); 70 | this.main.clear(); 71 | 72 | let sidebarContainer = document.querySelector('#sidebar') as HTMLDivElement; 73 | this.sidebar = new LayoutZoneController(sidebarContainer); 74 | this.sidebar.clear(); 75 | } 76 | 77 | public showView(view: Views.View, zone: LayoutZone) { 78 | switch (zone) { 79 | case LayoutZone.Main: this.main.show(view); break; 80 | case LayoutZone.Sidebar: this.sidebar.show(view); break; 81 | case LayoutZone.Lightbox: this.showLightbox(view); break; 82 | } 83 | } 84 | 85 | public hideView(view: Views.View, zone?: LayoutZone) { 86 | if (zone != null) { 87 | switch (zone) { 88 | case LayoutZone.Main: this.main.hide(view); break; 89 | case LayoutZone.Sidebar: this.sidebar.hide(view); break; 90 | case LayoutZone.Lightbox: this.hideLightbox(); break; 91 | } 92 | } 93 | else throw "Not Implemented"; 94 | } 95 | 96 | public showLightbox(view: Views.View, suppressTransitions?: boolean) { 97 | this.hideLightbox(); 98 | this._lightbox = Views.LightboxContainer.showLightbox(view, suppressTransitions); 99 | } 100 | 101 | public hideLightbox() { 102 | if (this._lightbox) { 103 | this._lightbox.hideLightbox(); 104 | this._lightbox = null; 105 | } 106 | } 107 | } 108 | } 109 | 110 | declare var $layout: Controllers.LayoutController; 111 | -------------------------------------------------------------------------------- /controllers/SoundController.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | namespace Controllers { 4 | export abstract class SoundController extends ControllerBase { 5 | protected _codec: Utils.AudioCodecInfo; 6 | 7 | constructor(parent: Application, codec: Utils.AudioCodecInfo) { 8 | super(parent); 9 | this._codec = codec; 10 | $sounds = this; 11 | } 12 | 13 | protected bufferSounds(): void { 14 | this.bufferSound('stone'); 15 | this.bufferSound('pass'); 16 | } 17 | 18 | protected abstract bufferSound(name: string): void; 19 | 20 | public abstract play(name: string): void; 21 | 22 | public static initialise(application: Application): SoundController { 23 | if (!Utils.isFunction(Audio)) return new NullSoundController(application, "audio objects not supported"); 24 | 25 | let audio: HTMLAudioElement = new Audio(); 26 | if (!audio.canPlayType) return new NullSoundController(application, "unable to test audio codecs"); 27 | 28 | let codec: Utils.AudioCodecInfo; 29 | for (let j = 0; j < Utils.AudioCodecs.length; ++j) { 30 | let support: string = audio.canPlayType(Utils.AudioCodecs[j].type); 31 | 32 | if (support == 'maybe') { 33 | codec = Utils.AudioCodecs[j]; 34 | } 35 | else if (support == 'probably') { 36 | codec = Utils.AudioCodecs[j]; 37 | break; 38 | } 39 | } 40 | 41 | if (codec == null) return new NullSoundController(application, "no supported audio formats"); 42 | 43 | let context: AudioContext = WebAudioSoundController.createAudioContext(); 44 | if (context) return new WebAudioSoundController(application, codec, context); 45 | else return new ElementSoundController(application, codec); 46 | } 47 | 48 | protected getSoundURI(name: string): string { 49 | return "sounds/" + name + this._codec.extension; 50 | } 51 | } 52 | 53 | class WebAudioSoundController extends SoundController { 54 | private _context: AudioContext; 55 | private _soundBank: { [name: string]: AudioBuffer }; 56 | 57 | constructor(parent: Application, codec: Utils.AudioCodecInfo, context: AudioContext) { 58 | super(parent, codec); 59 | Utils.log(Utils.LogSeverity.Debug, "Sound Controller: Web Audio API (" + codec.name + ")"); 60 | 61 | this._context = context; 62 | this._soundBank = {}; 63 | this.bufferSounds(); 64 | } 65 | 66 | public static createAudioContext(): AudioContext { 67 | if ((window).AudioContext) return new AudioContext(); 68 | else if ((window).webkitAudioContext) return new webkitAudioContext(); 69 | else return null; 70 | } 71 | 72 | protected bufferSound(name: string) { 73 | this._soundBank[name] = null; 74 | 75 | var xhr = new XMLHttpRequest(); 76 | xhr.open('GET', this.getSoundURI(name), true); 77 | xhr.responseType = 'arraybuffer'; 78 | xhr.onload = (event: Event) => this.onRequestLoad(name, xhr, event); 79 | xhr.onerror = (event: ErrorEvent) => this.onRequestError(name, xhr); 80 | xhr.send(); 81 | } 82 | 83 | private onRequestLoad(name: string, xhr: XMLHttpRequest, event: Event) { 84 | if (xhr.status == 200) 85 | this._context.decodeAudioData(xhr.response, (decodedData: AudioBuffer) => this.onAudioDecoded(name, decodedData), (error: DOMException) => this.onAudioDecodeError(name, error)); 86 | else 87 | this.onRequestError(name, xhr); 88 | } 89 | 90 | private onRequestError(name: string, xhr: XMLHttpRequest) { 91 | delete this._soundBank[name]; 92 | Utils.log(Utils.LogSeverity.Error, "Sound Controller: failed to GET sound '" + name + "' (" + xhr.statusText + ")"); 93 | } 94 | 95 | private onAudioDecoded(name: string, decodedData: AudioBuffer) { 96 | this._soundBank[name] = decodedData; 97 | } 98 | 99 | private onAudioDecodeError(name: string, error: DOMException) { 100 | delete this._soundBank[name]; 101 | Utils.log(Utils.LogSeverity.Error, "Sound Controller: failed to decode sound '" + name + "'"); 102 | } 103 | 104 | public play(name: string) { 105 | let buffer: AudioBuffer = this._soundBank[name]; 106 | if (buffer === undefined) Utils.log(Utils.LogSeverity.Warning, "Sound Controller: '" + name + "' not found in sound bank"); 107 | else if (buffer == null) Utils.log(Utils.LogSeverity.Warning, "Sound Controller: '" + name + "' not buffered"); 108 | else { 109 | let source = this._context.createBufferSource(); 110 | source.buffer = buffer; 111 | source.connect(this._context.destination); 112 | source.start(0); 113 | } 114 | } 115 | } 116 | 117 | class ElementSoundController extends SoundController { 118 | private _soundBank: { [name: string]: HTMLAudioElement }; 119 | 120 | constructor(parent: Application, codec: Utils.AudioCodecInfo) { 121 | super(parent, codec); 122 | Utils.log(Utils.LogSeverity.Debug, "Sound Controller: HTML 5 Audio (" + codec.name + ")"); 123 | 124 | this._soundBank = {}; 125 | this.bufferSounds(); 126 | } 127 | 128 | protected bufferSound(name: string) { 129 | this._soundBank[name] = new Audio(this.getSoundURI(name)); 130 | } 131 | 132 | public play(name: string) { 133 | if (name in this._soundBank) { 134 | this._soundBank[name].play(); 135 | } 136 | else Utils.log(Utils.LogSeverity.Warning, "Sound Controller: '" + name + "' not found in sound bank"); 137 | } 138 | } 139 | 140 | class NullSoundController extends SoundController { 141 | constructor(parent: Application, warning: string) { 142 | super(parent, null); 143 | Utils.log(Utils.LogSeverity.Warning, "Sound Controller not initialised: " + warning); 144 | } 145 | 146 | protected bufferSound(name: string): void {}; 147 | public play(name: string): void { } 148 | } 149 | } 150 | 151 | declare var $sounds: Controllers.SoundController; 152 | -------------------------------------------------------------------------------- /controllers/ViewControllerBase.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | namespace Controllers { 4 | export abstract class ViewControllerBase> extends ControllerBase { 5 | private _activated: boolean; 6 | private _views: { view: Views.View, zone: Controllers.LayoutZone, update: (digest?: KGS.DataDigest) => void }[]; 7 | 8 | constructor(parent: ParentType) { 9 | super(parent); 10 | this._activated = false; 11 | this._views = []; 12 | } 13 | 14 | protected digest(digest: KGS.DataDigest) { 15 | if (this._activated) { 16 | for (let j = 0; j < this._views.length; ++j) { 17 | this._views[j].update(digest); 18 | } 19 | } 20 | 21 | super.digest(digest); 22 | } 23 | 24 | protected registerView(view: Views.View, zone: Controllers.LayoutZone, update: (digest?: KGS.DataDigest) => void) { 25 | this._views.push({ view: view, zone: zone, update: update }); 26 | } 27 | 28 | public activate(): boolean { 29 | if (this._activated) return false; 30 | 31 | for (let j = 0; j < this._views.length; ++j) { 32 | this.application.layout.showView(this._views[j].view, this._views[j].zone); 33 | this._views[j].update(); 34 | } 35 | 36 | this._activated = true; 37 | return true; 38 | } 39 | 40 | public deactivate(): boolean { 41 | if (!this._activated) return false; 42 | 43 | if (this._views) { 44 | for (let j = (this._views.length - 1); j >= 0; --j) { 45 | this.application.layout.hideView(this._views[j].view, this._views[j].zone); 46 | } 47 | } 48 | 49 | this._activated = false; 50 | return true; 51 | } 52 | 53 | public detach() { 54 | this.deactivate(); 55 | super.detach(); 56 | } 57 | 58 | public get activated(): boolean { 59 | return this._activated; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /controllers/channels/ChannelBase.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | namespace Controllers { 4 | export abstract class ChannelBase extends ViewControllerBase { 5 | public channelId: number; 6 | 7 | constructor(parent: ChannelController, channelId: number) { 8 | super(parent); 9 | this.channelId = channelId; 10 | } 11 | 12 | public get channel(): Models.Channel { 13 | return this.database.channels[this.channelId]; 14 | } 15 | 16 | protected initialiseGameList() { 17 | let gameList = new Views.GameList(); 18 | gameList.tableBody.userDataSource = (name) => this.database.users[name]; 19 | gameList.tableBody.selectionCallback = (cid) => this.parent.joinChannel(cid); 20 | 21 | this.registerView(gameList, LayoutZone.Main, (digest?: KGS.DataDigest) => { 22 | if ((digest == null) || (digest.channelGames[this.channelId])) { 23 | gameList.tableBody.update(this.database.channels as { [key: string]: Models.GameChannel }, (this.channel).games); 24 | } 25 | }); 26 | } 27 | 28 | protected initialiseChat() { 29 | let chat = new Views.ChatForm(); 30 | chat.submitCallback = (form) => this.submitChatMessage(form); 31 | 32 | this.registerView(chat, LayoutZone.Sidebar, (digest?: KGS.DataDigest) => { 33 | if ((digest == null) || (digest.channelChat[this.channelId])) { 34 | chat.messageList.update(this.channel.chats); 35 | } 36 | if ((digest == null) || (digest.channelUsers[this.channelId])) { 37 | chat.memberList.update(this.database.users, this.channel.users); 38 | } 39 | }); 40 | } 41 | 42 | private submitChatMessage(chatForm: Views.ChatForm) { 43 | let text = chatForm.message; 44 | if ((text) && (text.length > 0) && (text.length <= KGS.Upstream._CHAT_MaxLength)) { 45 | this.client.post({ 46 | type: KGS.Upstream._CHAT, 47 | channelId: this.channelId, 48 | text:text 49 | }); 50 | 51 | chatForm.message = ""; 52 | chatForm.focus(); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /controllers/channels/ConversationChannel.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | namespace Controllers { 4 | export class ConversationChannel extends ChannelBase { 5 | 6 | constructor(parent: ChannelController, channelId: number) { 7 | super(parent, channelId); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /controllers/channels/ListChannel.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | namespace Controllers { 4 | export class ListChannel extends ChannelBase { 5 | 6 | constructor(parent: ChannelController, channelId: number) { 7 | super(parent, channelId); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /controllers/channels/RoomChannel.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | namespace Controllers { 4 | export class RoomChannel extends ChannelBase { 5 | 6 | constructor(parent: ChannelController, channelId: number) { 7 | super(parent, channelId); 8 | 9 | this.initialiseGameList(); 10 | this.initialiseChat(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /images/avatar-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenmartindale/kgs-leben/a74112fb2b535c3e4eb71e535029fd781164473d/images/avatar-default.png -------------------------------------------------------------------------------- /images/loading-primary-lighter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenmartindale/kgs-leben/a74112fb2b535c3e4eb71e535029fd781164473d/images/loading-primary-lighter.gif -------------------------------------------------------------------------------- /images/stone-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenmartindale/kgs-leben/a74112fb2b535c3e4eb71e535029fd781164473d/images/stone-black.png -------------------------------------------------------------------------------- /images/stone-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenmartindale/kgs-leben/a74112fb2b535c3e4eb71e535029fd781164473d/images/stone-white.png -------------------------------------------------------------------------------- /images/wood.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenmartindale/kgs-leben/a74112fb2b535c3e4eb71e535029fd781164473d/images/wood.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | KGS Leben 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5" 4 | }, 5 | 6 | "exclude": [ 7 | "node_modules", 8 | ".build", 9 | "dist" 10 | ] 11 | } -------------------------------------------------------------------------------- /kgs/Constants.ts: -------------------------------------------------------------------------------- 1 | namespace KGS { 2 | export namespace Constants { 3 | export const DefaultKomi: number = 6.5; 4 | export const HandicapKomi: number = 0.5; 5 | 6 | export const DefaultMainTime: number = 1800; 7 | export const DefaultJapaneseByoYomi: number = 30; 8 | export const DefaultJapanesePeriods: number = 5; 9 | export const DefaultCanadianByoYomi: number = 600; 10 | export const DefaultCanadianStones: number = 25; 11 | 12 | export const AvatarURIPrefix: string = "http://goserver.gokgs.com/avatars/"; 13 | export const AvatarURISuffix: string = ".jpg"; 14 | 15 | export const RatingShodan: number = 3000; 16 | export const Rating9Dan: number = 3899; 17 | export const RatingNull: number = 0x7fff; 18 | 19 | export namespace TimeSystems { 20 | export const None: string = "none"; 21 | export const Absolute: string = "absolute"; 22 | export const Japanese: string = "byo_yomi"; 23 | export const Canadian: string = "canadian"; 24 | } 25 | 26 | export namespace RegularExpressions { 27 | export const Name: RegExp = /[A-Za-z][A-Za-z0-9]{0,9}/; 28 | export const Rank: RegExp = /((\d+)\s*([kKdD])[yYuUaAnN]*)?\s*(\??)/; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /kgs/DataDigest.ts: -------------------------------------------------------------------------------- 1 | namespace KGS { 2 | export class DataDigest { 3 | public timestamp: Date = new Date(); 4 | public perfstamp: number = performance.now(); 5 | 6 | public username: boolean = false; 7 | public joinedChannelIds: boolean = false; 8 | public joinFailedChannelIds: number[]; 9 | public notifyChannelId: number; 10 | 11 | public channels: { [channelId: number]: boolean } = {}; 12 | public touchChannel(channelId: number) { this.channels[channelId] = true; } 13 | 14 | public channelOwners: { [channelId: number]: boolean } = {}; 15 | public touchChannelOwners(channelId: number) { this.channelOwners[channelId] = true; } 16 | public channelUsers: { [channelId: number]: boolean } = {}; 17 | public touchChannelUsers(channelId: number) { this.channelUsers[channelId] = true; } 18 | public channelGames: { [channelId: number]: boolean } = {}; 19 | public touchChannelGames(channelId: number) { this.channelGames[channelId] = true; } 20 | 21 | public channelChat: { [channelId: number]: boolean } = {}; 22 | public touchChannelChat(channelId: number) { this.channelChat[channelId] = true; } 23 | 24 | public users: { [name: string]: boolean } = {}; 25 | public touchUser(name: string) { this.users[name] = true; } 26 | 27 | public gameTrees: { [channelId: number]: boolean } = {}; 28 | public touchGameTree(channelId: number) { this.gameTrees[channelId] = true; } 29 | public gameClocks: { [channelId: number]: boolean } = {}; 30 | public touchGameClocks(channelId: number) { this.gameClocks[channelId] = true; } 31 | public gameActions: { [channelId: number]: boolean } = {}; 32 | public touchGameActions(channelId: number) { this.gameActions[channelId] = true; } 33 | 34 | public automatch: boolean = false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /kgs/protocol/DataTypes.ts: -------------------------------------------------------------------------------- 1 | namespace KGS { 2 | export interface Message { 3 | type: string 4 | } 5 | 6 | export interface ChannelMessage extends Message { 7 | channelId: number 8 | } 9 | 10 | export interface CallbackMessage extends Message { 11 | callbackId: number 12 | } 13 | 14 | export interface User { 15 | name: string, 16 | flags?: string, 17 | rank?: string 18 | } 19 | 20 | export interface GameUserMap { // An object mapping roles to user objects, telling who was in the game. 21 | white?: User, 22 | black?: User, 23 | white_2?: User, 24 | black_2?: User, 25 | challengeCreator?: User, // TODO: Add support for this user when the game is still a challenge 26 | owner?: User 27 | } 28 | 29 | export interface GameScore { 30 | // The result of the game. Not present if the game hasn't ended yet. 31 | /* Scores may be a floating point number, or a string. Numbers indicate the 32 | score difference (positive a black win, negative a white win). Strings 33 | may be UNKNOWN, UNFINISHED, NO_RESULT, B+RESIGN, W+RESIGN, B+FORFEIT, 34 | W+FORFEIT, B+TIME, or W+TIME. */ 35 | score?: string | number; 36 | } 37 | 38 | export interface GameSummary extends GameScore { 39 | size: number, // The size of the board from this game. 40 | timestamp: string, // The time stamp of when this game was started. This is also used as a serverwide ID for the game; no two games will ever have the same timestamp, and the time stamp is used to refer to the game summary. 41 | gameType: string, // One of demonstration, review, rengo_review, teaching, simul, rengo, free, ranked, or tournament. 42 | revision?: number, // The revision is used when downloading an SGF file. 43 | players?: GameUserMap, 44 | tag?: any, // Only present in tag archives. The tag associated with the game summary. 45 | private?: boolean, // If set, this is a private game. 46 | inPlay?: boolean // If set, the game is currently in play. 47 | } 48 | 49 | export interface GameChannelRules { 50 | size: number, 51 | handicap?: number, 52 | komi: number 53 | } 54 | export interface GameRules extends GameChannelRules { 55 | rules: "japanese" | "chinese" | "aga" | "new_zealand", 56 | timeSystem: "none" | "absolute" | "byo_yomi" | "canadian", 57 | mainTime?: number, 58 | byoYomiTime?: number, 59 | byoYomiPeriods?: number, 60 | byoYomiStones?: number 61 | } 62 | 63 | export interface GameFlags { 64 | "over"?: boolean, // If set, it means that the game has been scored. 65 | "adjourned"?: boolean, // The game cannot continue because the player whose turn it is has left. 66 | "private"?: boolean, // Only users specified by the owner are allowed in. 67 | "subscribers"?: boolean, // Only KGS Plus subscribers are allowed in. 68 | "event"?: boolean, // This game is a server event, and should appear at the top of game lists. 69 | "uploaded"?: boolean, // This game was created by uploading an SGF file. 70 | "audio"?: boolean, // This game includes a live audio track. 71 | "paused"?: boolean, // The game is paused. Tournament games are paused when they are first created, to give players time to join before the clocks start. 72 | "named"?: boolean, // This game has a name (most games are named after the players involved). In some cases, instead of seeing this flag when it is set, a text field name will appear instead. 73 | "saved"?: boolean, // This game has been saved to the KGS archives. Most games are saved automatically, but demonstration and review games must be saved by setting this flag. 74 | "global"?: boolean // This game may appear on the open or active game lists. 75 | } 76 | 77 | export interface GameProposalPlayer { 78 | role: "white" | "black" | "white_2" | "black_2" | "challengeCreator" | "owner"; 79 | handicap?: number; // (only for simultaneous games) 80 | komi?: number; // (only for simultaneous games) 81 | } 82 | export interface DownstreamProposalPlayer extends GameProposalPlayer { 83 | user?: KGS.User; 84 | } 85 | export interface UpstreamProposalPlayer extends GameProposalPlayer { 86 | name: string; 87 | } 88 | 89 | export interface GameProposal extends GameFlags { 90 | gameType: string; 91 | nigiri?: boolean; 92 | rules: KGS.GameRules; 93 | } 94 | export interface DownstreamProposal extends GameProposal { 95 | players: DownstreamProposalPlayer[] 96 | } 97 | export interface UpstreamProposal extends GameProposal { 98 | players: UpstreamProposalPlayer[] 99 | } 100 | 101 | export interface GameChannelBase extends GameFlags { 102 | channelId: number, 103 | gameType: string, 104 | roomId: number 105 | name?: string, 106 | players?: GameUserMap 107 | } 108 | export interface GameChannel extends GameChannelBase, GameChannelRules, GameScore { 109 | moveNum: number 110 | } 111 | export interface ChallengeChannel extends KGS.GameChannelBase { 112 | gameType: "challenge", 113 | initialProposal: DownstreamProposal, 114 | } 115 | 116 | export interface Coordinates { 117 | x: number; 118 | y: number; 119 | } 120 | export interface Location { 121 | loc: "PASS" | Coordinates 122 | } 123 | 124 | export interface AutomatchPreferences { 125 | maxHandicap: number, // The maximum number of handicap stones accepted in an automatch game. 126 | estimatedRank?: string, // The rank we claim to be. 1k is the highest allowed. 127 | freeOk?: boolean, // If set, free (unrated) games are OK. 128 | rankedOk?: boolean, // If set, rated games are OK. 129 | robotOk?: boolean, // If set, games against robots are OK. 130 | humanOk?: boolean, // If set, games against humans are OK. 131 | blitzOk?: boolean, // If set, blitz games are OK. 132 | fastOk?: boolean, // If set, fast games are OK. 133 | mediumOk?: boolean, // If set, medium speed games are OK. 134 | unrankedOk?: boolean, // If set, playing against unranked players are OK. 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /kgs/protocol/Upstream.ts: -------------------------------------------------------------------------------- 1 | namespace KGS { 2 | export namespace Upstream { 3 | export function validateMessage(message: KGS.Message): string { 4 | if (message == null) return "No message was provided"; 5 | if (message.type == null) return "Message type was not set"; 6 | if (message.type.length == 0) return "Message type was empty"; 7 | 8 | switch (message.type) { 9 | case _CHAT: 10 | case _ANNOUNCE: 11 | case _ANNOUNCE_TO_PLAYERS: 12 | let text = (message).text; 13 | if ((!text) || (text.length == 0) || (text.length > _CHAT_MaxLength)) { 14 | return "Message text not provided or too long"; 15 | } 16 | break; 17 | } 18 | 19 | return null; 20 | } 21 | 22 | export const _LOGIN: string = "LOGIN"; 23 | export interface LOGIN extends KGS.Message { 24 | name: string, 25 | password: string, 26 | locale: string 27 | } 28 | 29 | export const _LOGOUT: string = "LOGOUT"; 30 | export interface LOGOUT extends KGS.Message { 31 | } 32 | 33 | export const _JOIN_REQUEST: string = "JOIN_REQUEST"; 34 | export interface JOIN_REQUEST extends KGS.ChannelMessage { 35 | } 36 | 37 | export const _UNJOIN_REQUEST: string = "UNJOIN_REQUEST"; 38 | export interface UNJOIN_REQUEST extends KGS.ChannelMessage { 39 | } 40 | 41 | export const _CHAT: string = "CHAT"; 42 | export const _CHAT_MaxLength: number = 1000; 43 | export interface CHAT extends KGS.ChannelMessage { 44 | text: string 45 | } 46 | export const _ANNOUNCE: string = "ANNOUNCE"; 47 | export interface ANNOUNCE extends CHAT { 48 | } 49 | export const _ANNOUNCE_TO_PLAYERS: string = "ANNOUNCE_TO_PLAYERS"; 50 | export interface ANNOUNCE_TO_PLAYERS extends CHAT { 51 | } 52 | 53 | export const _GLOBAL_LIST_JOIN_REQUEST: string = "GLOBAL_LIST_JOIN_REQUEST"; 54 | export interface GLOBAL_LIST_JOIN_REQUEST { 55 | list: "CHALLENGES" | "ACTIVES" | "FANS"; 56 | } 57 | 58 | export interface ChallengeResponse extends KGS.ChannelMessage, KGS.UpstreamProposal { 59 | } 60 | export const _CHALLENGE_SUBMIT: string = "CHALLENGE_SUBMIT"; 61 | export interface CHALLENGE_SUBMIT extends ChallengeResponse { 62 | } 63 | export const _CHALLENGE_ACCEPT: string = "CHALLENGE_ACCEPT"; 64 | export interface CHALLENGE_ACCEPT extends ChallengeResponse { 65 | } 66 | 67 | export const _AUTOMATCH_CREATE: string = "AUTOMATCH_CREATE"; 68 | export interface AUTOMATCH_CREATE extends KGS.Message, KGS.AutomatchPreferences { 69 | } 70 | export const _AUTOMATCH_SET_PREFS: string = "AUTOMATCH_SET_PREFS"; 71 | export interface AUTOMATCH_SET_PREFS extends KGS.Message, KGS.AutomatchPreferences { 72 | } 73 | export const _AUTOMATCH_CANCEL: string = "AUTOMATCH_CANCEL"; 74 | export interface AUTOMATCH_CANCEL extends KGS.Message { 75 | } 76 | 77 | export const _GAME_MOVE: string = "GAME_MOVE"; 78 | export interface GAME_MOVE extends ChannelMessage, KGS.Location { 79 | } 80 | export const _GAME_MARK_LIFE: string = "GAME_MARK_LIFE"; 81 | export interface GAME_MARK_LIFE extends ChannelMessage, KGS.Coordinates { 82 | alive: boolean; 83 | } 84 | export const _GAME_SCORING_DONE: string = "GAME_SCORING_DONE"; 85 | export interface GAME_SCORING_DONE extends ChannelMessage { 86 | doneId: number; 87 | } 88 | export const _GAME_RESIGN: string = "GAME_RESIGN"; 89 | export interface GAME_RESIGN extends ChannelMessage { 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /models/AutomatchState.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export const enum AutomatchCriteria { 3 | FreeGames = (1 << 0), // If set, free (unrated) games are OK. 4 | RankedGames = (1 << 1), // If set, rated games are OK. 5 | 6 | RobotPlayers = (1 << 2), // If set, games against robots are OK. 7 | HumanPlayers = (1 << 3), // If set, games against humans are OK. 8 | UnrankedPlayers = (1 << 4), // If set, playing against unranked players are OK. 9 | 10 | BlitzSpeed = (1 << 5), // If set, blitz games are OK. 11 | FastSpeed = (1 << 6), // If set, fast games are OK. 12 | MediumSpeed = (1 << 7) // If set, medium speed games are OK. 13 | } 14 | 15 | export class AutomatchState { 16 | public maxHandicap: number; 17 | public estimatedRank: string; 18 | public criteria: AutomatchCriteria; 19 | public seeking: boolean; 20 | 21 | constructor() { 22 | this.seeking = false; 23 | } 24 | 25 | public mergePreferences(preferences: KGS.AutomatchPreferences): boolean { 26 | let touch: boolean = false; 27 | 28 | if (this.maxHandicap != preferences.maxHandicap) { this.maxHandicap = preferences.maxHandicap; touch = true; } 29 | if (this.estimatedRank != preferences.estimatedRank) { this.estimatedRank = preferences.estimatedRank; touch = true; } 30 | 31 | let criteria: number = 0; 32 | 33 | if (preferences.freeOk) criteria |= AutomatchCriteria.FreeGames; 34 | if (preferences.rankedOk) criteria |= AutomatchCriteria.RankedGames; 35 | 36 | if (preferences.robotOk) criteria |= AutomatchCriteria.RobotPlayers; 37 | if (preferences.humanOk) criteria |= AutomatchCriteria.HumanPlayers; 38 | if (preferences.unrankedOk) criteria |= AutomatchCriteria.UnrankedPlayers; 39 | 40 | if (preferences.blitzOk) criteria |= AutomatchCriteria.BlitzSpeed; 41 | if (preferences.fastOk) criteria |= AutomatchCriteria.FastSpeed; 42 | if (preferences.mediumOk) criteria |= AutomatchCriteria.MediumSpeed; 43 | 44 | if (this.criteria != criteria) { this.criteria = criteria; touch = true; } 45 | 46 | return touch; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /models/Channel.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export abstract class Channel { 3 | channelId: number; 4 | channelType: Models.ChannelType; 5 | 6 | owners: string[] = []; 7 | users: string[] = []; 8 | chats: Models.Chat[] = []; 9 | 10 | name: string; 11 | 12 | constructor(channelId: number, channelType: Models.ChannelType) { 13 | this.channelId = channelId; 14 | this.channelType = channelType; 15 | this.name = "Channel #" + channelId.toString(); 16 | } 17 | 18 | public static createChannel(channelId: number, channelType: Models.ChannelType): Channel { 19 | switch (channelType) { 20 | case ChannelType.Room: return new RoomChannel(channelId); 21 | case ChannelType.Game: return new GameChannel(channelId); 22 | } 23 | 24 | throw "Unknown or unsupported channel type: " + channelType.toString(); 25 | } 26 | 27 | public syncOwners(owners: KGS.User[]): boolean { 28 | let ownerNames: string[] = new Array(owners.length); 29 | for (let i = 0; i < owners.length; ++i) { 30 | ownerNames[i] = owners[i].name; 31 | } 32 | 33 | return Utils.setSync(this.owners, ownerNames); 34 | } 35 | 36 | public mergeUsers(users: KGS.User[]): boolean { 37 | let touch: boolean = false; 38 | for (let i = 0; i < users.length; ++i) { 39 | if (Utils.setAdd(this.users, users[i].name)) touch = true; 40 | } 41 | return touch; 42 | } 43 | 44 | public appendChat(message: KGS.Downstream.CHAT, importance: Models.ChatImportance, timestamp: Date): boolean { 45 | this.chats.push({ 46 | sender: message.user.name, 47 | text: message.text, 48 | importance: importance, 49 | received: timestamp 50 | }); 51 | 52 | return true; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /models/ChannelType.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export const enum ChannelType { 3 | Room, 4 | List, 5 | Conversation, 6 | Game 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /models/Chat.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export interface Chat { 3 | sender: string, 4 | text: string, 5 | importance: Models.ChatImportance, 6 | received: Date 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /models/ChatImportance.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export const enum ChatImportance { 3 | Chat, 4 | Announcement, 5 | Moderated 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /models/GameActions.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export const enum GameActions { 3 | Move = (1 << 0), 4 | Edit = (1 << 1), 5 | Score = (1 << 2), 6 | ChallengeCreate = (1 << 3), 7 | ChallengeSetup = (1 << 4), 8 | ChallengeWait = (1 << 5), 9 | ChallengeAccept = (1 << 6), 10 | ChallengeSubmitted = (1 << 7), 11 | EditDelay = (1 << 8) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /models/GameClock.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export interface GameClockState { 3 | running?: boolean; 4 | overtime?: boolean; 5 | expired?: boolean; 6 | 7 | time?: number; 8 | periods?: number; 9 | stones?: number; 10 | } 11 | 12 | export class GameClock { 13 | public updated: number; 14 | public timeSystem: string; 15 | 16 | public rules: Models.GameRules; 17 | 18 | public running: boolean; 19 | public overtime: boolean; 20 | 21 | public time: number; 22 | public periods: number; 23 | public stones: number; 24 | 25 | constructor(perfstamp: number) { 26 | this.updated = perfstamp; 27 | } 28 | 29 | public mergeClockState(perfstamp: number, gamePhase: Models.GamePhase, clockState: KGS.Downstream.ClockState) { 30 | this.updated = perfstamp; 31 | 32 | this.running = ((gamePhase == GamePhase.Active) && (!clockState.paused) && (clockState.running))? true : false; 33 | 34 | this.time = clockState.time; 35 | 36 | switch (this.rules.timeSystem) { 37 | case Models.TimeSystem.Japanese: 38 | this.periods = clockState.periodsLeft; 39 | if ((this.periods) && (this.periods > 0)) this.overtime = true; 40 | break; 41 | 42 | case Models.TimeSystem.Canadian: 43 | this.stones = clockState.stonesLeft; 44 | if ((this.stones) && (this.stones > 0)) this.overtime = true; 45 | break; 46 | } 47 | } 48 | 49 | public now(perfstamp?: number): Models.GameClockState { 50 | if (this.rules.timeSystem == Models.TimeSystem.None) return null; 51 | 52 | let expired: boolean = (this.time <= 0); 53 | if ((expired) || (!this.running)) { 54 | return { 55 | running: false, 56 | overtime: this.overtime, 57 | expired: expired, 58 | time: Math.round(this.time), 59 | periods: this.periods, 60 | stones: this.stones 61 | }; 62 | } 63 | 64 | let seconds: number = this.time; 65 | perfstamp = (perfstamp != null)? perfstamp : performance.now(); 66 | seconds -= (perfstamp - this.updated) / 1000.0; 67 | seconds = Math.round(seconds); 68 | 69 | let japaneseByoYomi: boolean = (this.rules.timeSystem == Models.TimeSystem.Japanese); 70 | let canadianByoYomi: boolean = (this.rules.timeSystem == Models.TimeSystem.Canadian); 71 | 72 | let overtime: boolean = (this.overtime)? true : false; 73 | let periods: number; 74 | if (seconds <= 0) { 75 | if ((japaneseByoYomi) || (canadianByoYomi)) { 76 | let periodLength: number = this.rules.byoYomiTime; 77 | let periodsAvailable: number; 78 | if (japaneseByoYomi) { 79 | periodsAvailable = (!overtime)? this.rules.byoYomiPeriods : (this.periods - 1); 80 | } 81 | else if (canadianByoYomi) { 82 | periodsAvailable = (!overtime)? 1 : 0; 83 | } 84 | 85 | periods = - Math.ceil(seconds / periodLength); 86 | overtime = true; 87 | 88 | if (periods < periodsAvailable) { 89 | seconds += (periods + 1) * periodLength; 90 | periods = periodsAvailable - periods; 91 | } 92 | else expired = true; 93 | } 94 | else expired = true; 95 | } 96 | else periods = this.periods; 97 | 98 | if (!expired) { 99 | if (japaneseByoYomi) { 100 | return { 101 | running: true, 102 | overtime: overtime, 103 | time: seconds, 104 | periods: (overtime)? periods : null 105 | }; 106 | } 107 | else if (canadianByoYomi) { 108 | return { 109 | running: true, 110 | overtime: overtime, 111 | time: seconds, 112 | stones: (!overtime)? null : (this.stones)? this.stones : this.rules.byoYomiStones 113 | }; 114 | } 115 | else { 116 | return { 117 | running: true, 118 | time: seconds 119 | }; 120 | } 121 | } 122 | 123 | if (this.rules.timeSystem == Models.TimeSystem.Canadian) { 124 | return { expired: true, stones: (this.stones) ? this.stones : this.rules.byoYomiStones }; 125 | } 126 | else { 127 | return { expired: true }; 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /models/GameMarks.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export const enum GameMarks { 3 | WhiteStone = (1 << (16 + 0)), // 65536 4 | BlackStone = (1 << (16 + 1)), // 131072 5 | 6 | Dead = (1 << (16 + 2)), // 262144 7 | WhiteTerritory = (1 << (16 + 3)), // 524288 8 | BlackTerritory = (1 << (16 + 4)), // 1048576 9 | 10 | LastMove = (1 << (16 + 5)), // 2097152 11 | Ko = (1 << (16 + 6)), // 4194304 12 | 13 | Circle = (1 << (16 + 7)), // 8388608 14 | Triangle = (1 << (16 + 8)), // 16777216 15 | Square = (1 << (16 + 9)), // 33554432 16 | Cross = (1 << (16 + 10)), // 67108864 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /models/GamePhase.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export const enum GamePhase { 3 | Active, 4 | Paused, 5 | Adjourned, 6 | Concluded 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /models/GameResult.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | // Scores may be a floating point number, or a string. Numbers indicate the 3 | // score difference (positive a black win, negative a white win). Strings 4 | // may be UNKNOWN, UNFINISHED, NO_RESULT, B+RESIGN, W+RESIGN, B+FORFEIT, 5 | // W+FORFEIT, B+TIME, or W+TIME. 6 | export const enum GameResultType { 7 | Unknown, // The game has not yet finished or the result is not known 8 | Scored, // The game was concluded and points were tallied 9 | Timeout, // One of the players ran out of time before the game ended 10 | Resignation, // One of the players resigned 11 | Anulled, // The game was resolved without result by an edge-case such as Japanese Super-Ko 12 | Forfeit // One of the players was forced to forfeit the game 13 | } 14 | 15 | export class GameResult { 16 | private _score: string | number; 17 | 18 | public resultType: Models.GameResultType; 19 | public victor: Models.GameStone; 20 | public points: number; 21 | 22 | constructor() { 23 | this.resultType = GameResultType.Unknown; 24 | } 25 | 26 | public mergeScore(score: string | number): boolean { 27 | if (this._score == score) return false; 28 | 29 | if (GameResult.isKnownResult(score)) { 30 | this._score = score; 31 | if (Utils.isNumber(score)) { 32 | if (score > 0) { 33 | this.resultType = GameResultType.Scored; 34 | this.victor = Models.GameStone.Black; 35 | this.points = +score; 36 | } 37 | else if (score < 0) { 38 | this.resultType = GameResultType.Scored; 39 | this.victor = Models.GameStone.White; 40 | this.points = -(+score); 41 | } 42 | else { 43 | this.resultType = GameResultType.Scored; 44 | this.victor = null; 45 | this.points = 0; 46 | } 47 | } 48 | else if (score == "NO_RESULT") { 49 | this.resultType = GameResultType.Anulled; 50 | this.victor = null; 51 | this.points = null; 52 | } 53 | else { 54 | let v = (score).substr(0, 1); 55 | this.victor = (v == "B")? Models.GameStone.Black : (v == "W")? Models.GameStone.White : null; 56 | this.points = null; 57 | 58 | let w = (score).substr(2); 59 | switch (w) { 60 | case "TIME": this.resultType = GameResultType.Timeout; break; 61 | case "RESIGN": this.resultType = GameResultType.Resignation; break; 62 | case "FORFEIT": this.resultType = GameResultType.Forfeit; break; 63 | } 64 | } 65 | 66 | return true; 67 | } 68 | else { 69 | if (this.resultType == GameResultType.Unknown) return false; 70 | 71 | this._score = null; 72 | 73 | this.resultType = GameResultType.Unknown; 74 | this.victor = null; 75 | this.points = null; 76 | 77 | return true; 78 | } 79 | } 80 | 81 | public static isKnownResult(score: string | number): boolean { 82 | return ((score != null) && (score != "UNKNOWN") && (score != "UNFINISHED")); 83 | } 84 | 85 | public getHeadline(userColour?: Models.GameStone, whiteName?: string, blackName?: string): string { 86 | if (this.resultType == GameResultType.Anulled) { 87 | return "no result"; 88 | } 89 | else if ((this.resultType == GameResultType.Scored) && (this.points == 0)) { 90 | return "jigo"; 91 | } 92 | 93 | let byLine: string; 94 | switch (this.resultType) { 95 | case GameResultType.Scored: byLine = " by " + this.points.toString() + " points"; break; 96 | case GameResultType.Timeout: byLine = " by time"; break; 97 | case GameResultType.Resignation: byLine = " by resignation"; break; 98 | case GameResultType.Forfeit: byLine = " by forfeiture"; break; 99 | } 100 | 101 | if (byLine) { 102 | if (userColour != null) 103 | return ((this.victor == userColour)? "victory" : "defeat") + byLine; 104 | else 105 | return ((this.victor == Models.GameStone.White)? (whiteName || "White") : (blackName || "Black")) + " won" + byLine; 106 | } 107 | 108 | return "unknown result"; 109 | } 110 | 111 | public getShortFormat() { 112 | if (this.resultType == GameResultType.Anulled) { 113 | return ""; 114 | } 115 | else if ((this.resultType == GameResultType.Scored) && (this.points == 0)) { 116 | return "JIGO"; 117 | } 118 | 119 | let byLine: string; 120 | switch (this.resultType) { 121 | case GameResultType.Scored: byLine = this.points.toString(); break; 122 | case GameResultType.Timeout: byLine = "TIME"; break; 123 | case GameResultType.Resignation: byLine = "RESIGN"; break; 124 | case GameResultType.Forfeit: byLine = "FORFEIT"; break; 125 | } 126 | 127 | if (byLine) { 128 | return ((this.victor == Models.GameStone.White)? "W+" : "B+") + byLine; 129 | } 130 | 131 | return "??"; 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /models/GameRules.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export const enum RuleSet { 3 | Japanese, // "japanese" 4 | Chinese, // "chinese" 5 | AGA, // "aga" 6 | NewZealand // "new_zealand" 7 | } 8 | 9 | export const enum TimeSystem { 10 | None, // "none" 11 | Absolute, // "absolute" 12 | Japanese, // "byo_yomi" 13 | Canadian // "canadian" 14 | } 15 | 16 | export interface KomiSplit { 17 | white: { base: number, half?: boolean }; 18 | black?: { base: number, half?: boolean }; 19 | } 20 | 21 | export class GameRules { 22 | public ruleSet: Models.RuleSet; 23 | 24 | public timeSystem: Models.TimeSystem; 25 | public mainTime: number; 26 | public byoYomiTime: number; 27 | public byoYomiPeriods: number; 28 | public byoYomiStones: number; 29 | public byoYomiMaximum: number; 30 | 31 | public komi: number; 32 | 33 | constructor(rules: KGS.GameRules) { 34 | this.setRules(rules); 35 | } 36 | 37 | public setRules(rules: KGS.GameRules) { 38 | if (rules) { 39 | switch (rules.rules) { 40 | case "chinese": this.ruleSet = Models.RuleSet.Chinese; break; 41 | case "aga": this.ruleSet = Models.RuleSet.AGA; break; 42 | case "new_zealand": this.ruleSet = Models.RuleSet.NewZealand; break; 43 | default: this.ruleSet = Models.RuleSet.Japanese; break; 44 | } 45 | 46 | switch (rules.timeSystem) { 47 | case "absolute": this.timeSystem = Models.TimeSystem.Absolute; break; 48 | case "byo_yomi": this.timeSystem = Models.TimeSystem.Japanese; break; 49 | case "canadian": this.timeSystem = Models.TimeSystem.Canadian; break; 50 | default: this.timeSystem = Models.TimeSystem.None; break; 51 | } 52 | 53 | if (this.timeSystem != Models.TimeSystem.None) { 54 | this.mainTime = rules.mainTime; 55 | this.byoYomiTime = (this.timeSystem != Models.TimeSystem.Absolute)? rules.byoYomiTime : null; 56 | this.byoYomiPeriods = (this.timeSystem == Models.TimeSystem.Japanese)? rules.byoYomiPeriods : null; 57 | this.byoYomiStones = (this.timeSystem == Models.TimeSystem.Canadian)? rules.byoYomiStones : null; 58 | this.byoYomiMaximum = (this.timeSystem != Models.TimeSystem.Absolute)? (this.byoYomiPeriods || this.byoYomiStones) : null; 59 | } 60 | else { 61 | this.mainTime = null; 62 | this.byoYomiTime = null; 63 | this.byoYomiPeriods = null; 64 | this.byoYomiStones = null; 65 | this.byoYomiMaximum = null; 66 | } 67 | 68 | this.komi = rules.komi; 69 | } 70 | else { 71 | this.ruleSet = Models.RuleSet.Japanese; 72 | 73 | this.timeSystem = Models.TimeSystem.None; 74 | this.mainTime = null; 75 | this.byoYomiTime = null; 76 | this.byoYomiPeriods = null; 77 | this.byoYomiStones = null; 78 | this.byoYomiMaximum = null; 79 | 80 | this.komi = null; 81 | } 82 | } 83 | 84 | public static splitKomi(komi: number): KomiSplit { 85 | let reverse: boolean = false; 86 | let base: number = 0; 87 | let half: boolean = false; 88 | 89 | if (komi) { 90 | reverse = (komi < 0); 91 | base = Math.floor(komi); 92 | half = (komi != base); 93 | } 94 | 95 | if (!reverse) { 96 | return { white: { base: base, half: half }}; 97 | } 98 | else { 99 | return { 100 | white: { base: 0, half: half }, 101 | black: { base: Math.abs(base) } 102 | }; 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /models/GameStone.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export const enum GameStone { 3 | Black = +1, // WGo.B 4 | White = -1 // WGo.W 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /models/GameTree.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | const RootNodeId: number = 0; 3 | 4 | export class GameTree { 5 | public size: number; 6 | public root: GameTreeNode; 7 | public nodes: GameTreeNode[]; 8 | 9 | private _activeNode: GameTreeNode; 10 | 11 | private _facade: { [type: string]: ((tree: GameTree, event: KGS.SGF.NodeEvent) => void) } 12 | 13 | constructor(size?: number) { 14 | this.size = size; 15 | this.root = new GameTreeNode(this, RootNodeId); 16 | this.nodes = [this.root]; 17 | 18 | this._activeNode = null; 19 | } 20 | 21 | public get(nodeId: number): GameTreeNode { 22 | if (nodeId < this.nodes.length) { 23 | let node = this.nodes[nodeId]; 24 | if (node != null) return node; 25 | } 26 | 27 | throw "Game Tree Node [" + nodeId.toString() + "] not found"; 28 | } 29 | 30 | public create(nodeId: number): GameTreeNode { 31 | let node: GameTreeNode; 32 | if (nodeId < this.nodes.length) { 33 | node = this.nodes[nodeId]; 34 | if (node != null) return node; 35 | } 36 | 37 | node = new GameTreeNode(this, nodeId); 38 | this.nodes[nodeId] = node; 39 | return node; 40 | } 41 | 42 | public get activeNode(): Models.GameTreeNode { 43 | return this._activeNode; 44 | } 45 | 46 | public get position(): Models.GamePosition { 47 | return (null != this._activeNode)? this._activeNode.position : null; 48 | } 49 | 50 | public activate(changedNodes: { [nodeId: number]: boolean }, activateNodeId: number) { 51 | // Path of nodes to activate (in reverse order) 52 | let activate: number[] = []; 53 | 54 | if (null == activateNodeId) { 55 | if (null != this._activeNode) { 56 | activateNodeId = this._activeNode.nodeId; 57 | } 58 | else return; 59 | } 60 | 61 | // Count of nodes to deactivate 62 | let changedCount: number = Object.keys(changedNodes).length; 63 | 64 | // Traverse up the tree until you find a node on the active path... 65 | let currentNode: GameTreeNode = this.nodes[activateNodeId]; 66 | while ((null != currentNode) && (null == currentNode.position)) { 67 | let currentNodeId: number = currentNode.nodeId; 68 | activate.push(currentNodeId); 69 | if (changedNodes[currentNodeId]) -- changedCount; 70 | currentNode = currentNode.parent; 71 | } 72 | 73 | // Deactivate obsolete nodes on the active path... 74 | let deactivateNode: GameTreeNode = this._activeNode; 75 | while ((null != deactivateNode) && (currentNode !== deactivateNode)) { 76 | deactivateNode.position = null; 77 | if (changedNodes[deactivateNode.nodeId]) -- changedCount; 78 | deactivateNode = deactivateNode.parent; 79 | } 80 | 81 | // Continue deactivating the active path untill all changed nodes have been deactivated... 82 | while ((null != currentNode) && (changedCount > 0)) { 83 | let currentNodeId: number = currentNode.nodeId; 84 | currentNode.position = null; 85 | activate.push(currentNodeId); 86 | if (changedNodes[currentNodeId]) -- changedCount; 87 | currentNode = currentNode.parent; 88 | } 89 | 90 | // Activate the new path... 91 | for (let j = (activate.length - 1); j >= 0; --j) { 92 | let activateNode = this.nodes[activate[j]]; 93 | 94 | // ASSERT 95 | if ((null != currentNode) && (currentNode.nodeId != activateNode.parentId)) throw new Error("Attempted to activate off a path"); 96 | if (null != activateNode.position) throw new Error("Attempted to activate an activenode"); 97 | 98 | let previousPosition: Models.GamePosition = (null != currentNode)? currentNode.position : null; 99 | 100 | if (previousPosition) { 101 | activateNode.position = new Models.GamePosition(previousPosition); 102 | } 103 | else if (this.size) { 104 | activateNode.position = new Models.GamePosition(this.size); 105 | } 106 | else { 107 | let rules = activateNode.getProperty(KGS.SGF._RULES) as KGS.SGF.RULES; 108 | let size: number = 19; 109 | if ((rules) && (rules.size)) size = rules.size; 110 | activateNode.position = new Models.GamePosition(size); 111 | } 112 | 113 | activateNode.position.effectEvent(activateNode.properties); 114 | currentNode = activateNode; 115 | } 116 | 117 | // The node is activated 118 | this._activeNode = currentNode; 119 | } 120 | 121 | public testMove(x: number, y: number, colour: GameStone): GameMoveResult { 122 | if (null == this._activeNode) { 123 | Utils.log(Utils.LogSeverity.Warning, "The game tree does not have an active path"); 124 | return null; 125 | } 126 | else if (null == this._activeNode.position) { 127 | Utils.log(Utils.LogSeverity.Warning, "The game tree does not have an active position"); 128 | return null; 129 | } 130 | 131 | let position: Models.GamePosition = this._activeNode.position; 132 | let clone = new Models.GamePosition(position); 133 | return clone.play(x, y, colour); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /models/GameTreeNode.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export class GameTreeNode { 3 | public tree: GameTree; 4 | public nodeId: number; 5 | public parentId: number; 6 | public children: number[]; 7 | 8 | public position: Models.GamePosition; 9 | 10 | private _properties: KGS.SGF.Property[]; 11 | 12 | constructor(tree: GameTree, nodeId: number) { 13 | this.tree = tree; 14 | this.nodeId = nodeId; 15 | } 16 | 17 | public addChild(childNodeId: number): Models.GameTreeNode { 18 | let child = this.tree.create(childNodeId); 19 | child.parentId = this.nodeId; 20 | 21 | if (this.children == null) 22 | this.children = [childNodeId]; 23 | else 24 | this.children.push(childNodeId); 25 | 26 | return child; 27 | } 28 | 29 | public get parent(): GameTreeNode { 30 | if (null == this.parentId) return null; 31 | else if (null != this.tree) return this.tree.get(this.parentId); 32 | else throw "Game Tree Node [" + this.nodeId.toString() + "] is an orphan"; 33 | } 34 | 35 | public get properties(): KGS.SGF.Property[] { 36 | return this._properties; 37 | } 38 | 39 | public addProperty(property: KGS.SGF.Property): boolean { 40 | if (this._properties == null) this._properties = [property]; 41 | else this._properties.push(property); 42 | 43 | return true; 44 | } 45 | 46 | private locationsEqual(left: KGS.Location, right: KGS.Location) { 47 | if (left.loc == right.loc) return true; 48 | 49 | if ((left.loc != null) && (Utils.isObject(left.loc)) 50 | && (right.loc != null) && (Utils.isObject(right.loc))) { 51 | let l = left.loc as KGS.Coordinates; 52 | let r = right.loc as KGS.Coordinates; 53 | return ((l.x == r.x) && (l.y == r.y)); 54 | } 55 | 56 | return false; 57 | } 58 | 59 | private findProperty(property: string | KGS.SGF.Property): number { 60 | if (property != null) { 61 | const notFound: number = -1; 62 | if (this._properties == null) { 63 | return notFound; 64 | } 65 | else if (Utils.isString(property)) { 66 | for (let i = 0; i < this._properties.length; ++i) { 67 | if (this._properties[i].name == property) return i; 68 | } 69 | 70 | return notFound; 71 | } 72 | else if ((property).name) { 73 | let propertyName: string = (property).name; 74 | if ((property).loc == null) return this.findProperty(propertyName); 75 | else { 76 | let location = property; 77 | let firstProperty: number = notFound; 78 | for (let i = 0; i < this._properties.length; ++i) { 79 | if (this._properties[i].name == propertyName) { 80 | if (this.locationsEqual((this._properties[i]), location)) return i; 81 | else if (firstProperty == notFound) firstProperty = i; 82 | } 83 | } 84 | 85 | return firstProperty; 86 | } 87 | } 88 | } 89 | 90 | throw 'Argument was not a valid SGF Property'; 91 | } 92 | 93 | public setProperty(property: KGS.SGF.Property) : boolean { 94 | let i: number = this.findProperty(property); 95 | if (i >= 0) { 96 | this._properties[i] = property; 97 | return true; 98 | } 99 | else { 100 | return this.addProperty(property); 101 | } 102 | } 103 | 104 | public removeProperty(property: string | KGS.SGF.Property): boolean { 105 | let i: number = this.findProperty(property); 106 | if (i >= 0) { 107 | this._properties.splice(i, 1); 108 | return true; 109 | } 110 | else return false; 111 | } 112 | 113 | public getProperty(name: string): KGS.SGF.Property { 114 | let i: number = this.findProperty(name); 115 | if (i >= 0) { 116 | return this._properties[i]; 117 | } 118 | else return undefined; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /models/GameType.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export const enum GameType { 3 | Challenge, // (Actually not a game, a challenge is a user trying to set up a custom game) 4 | Demonstration, 5 | Review, 6 | ReviewRengo, 7 | Teaching, 8 | Simultaneous, 9 | Rengo, 10 | Free, 11 | Ranked, 12 | Tournament 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /models/PlayerTeam.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export const enum PlayerTeam { 3 | Home, 4 | Away 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /models/RoomChannel.ts: -------------------------------------------------------------------------------- 1 | namespace Models { 2 | export class RoomChannel extends Models.Channel { 3 | description: string; 4 | games: number[] = []; 5 | 6 | private: boolean; 7 | tournamentOnly: boolean; 8 | globalGamesOnly: boolean; 9 | 10 | constructor(channelId: number) { 11 | super(channelId, ChannelType.Room); 12 | } 13 | 14 | public mergeRoomName(room: KGS.Downstream.RoomName): boolean { 15 | let touch: boolean = false; 16 | if (this.name != room.name) { this.name = room.name; touch = true; } 17 | if (this.private != room.private) { this.private = room.private; touch = true; } 18 | if (this.tournamentOnly != room.tournOnly) { this.tournamentOnly = room.tournOnly; touch = true; } 19 | if (this.globalGamesOnly != room.globalGamesOnly) { this.globalGamesOnly = room.globalGamesOnly; touch = true; } 20 | return touch; 21 | } 22 | 23 | public mergeRoomDescription(description: string): boolean { 24 | if (this.description != description) { 25 | this.description = description; 26 | return true; 27 | } 28 | else return false; 29 | } 30 | 31 | public addGame(gameChannelId: number): boolean { 32 | return Utils.setAdd(this.games, gameChannelId); 33 | } 34 | public removeGame(gameChannelId: number): boolean { 35 | return Utils.setRemove(this.games, gameChannelId); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.0", 3 | "name": "kgs-leben", 4 | "description": "", 5 | 6 | "license": "MIT", 7 | "private": true, 8 | 9 | "author": "Stephen Martindale", 10 | "homepage": "https://github.com/stephenmartindale/kgs-leben#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/stephenmartindale/kgs-leben.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/stephenmartindale/kgs-leben/issues" 17 | }, 18 | 19 | "scripts": { 20 | "build": "gulp build", 21 | "clean": "gulp clean", 22 | "rebuild": "gulp rebuild", 23 | "test": "gulp build:tests && mocha --timeout 1000 .build/tests.js" 24 | }, 25 | 26 | "devDependencies": { 27 | "typescript": "^2.0.3", 28 | "node-sass": "^3.10.0", 29 | "autoprefixer": "^6.4.1", 30 | 31 | "mocha": "^3.1.0", 32 | 33 | "gulp": "^3.9.1", 34 | "rimraf": "^2.5.4", 35 | "merge-stream": "^1.0.0", 36 | "gulp-concat": "^2.6.0", 37 | "gulp-sourcemaps": "^1.6.0", 38 | "gulp-typescript": "^3.0.1", 39 | "gulp-sass": "^2.3.2", 40 | "gulp-postcss": "^6.2.0", 41 | 42 | "jquery": "^3", 43 | "normalize.css": "^4", 44 | "font-awesome": "^4" 45 | }, 46 | 47 | "dependencies": {} 48 | } 49 | -------------------------------------------------------------------------------- /scss/_classes.scss: -------------------------------------------------------------------------------- 1 | .theme-dark { 2 | background-color: $colour-dark; 3 | color: $colour-light; 4 | } 5 | .theme-light { 6 | background-color: $colour-light; 7 | color: $colour-dark; 8 | } 9 | 10 | .div-fill { 11 | top: 0; 12 | right: 0; 13 | bottom: 0; 14 | left: 0; 15 | width: auto; 16 | height: auto; 17 | box-sizing: border-box; 18 | } 19 | .block-fixed { 20 | @extend .div-fill; 21 | display: block; 22 | position: fixed; 23 | } 24 | .block-absolute { 25 | @extend .div-fill; 26 | display: block; 27 | position: absolute; 28 | } 29 | .flex-fixed { 30 | @extend .div-fill; 31 | display: flex; 32 | position: fixed; 33 | } 34 | .flex-absolute { 35 | @extend .div-fill; 36 | display: flex; 37 | position: absolute; 38 | } 39 | 40 | .flex-centerer { 41 | display: flex; 42 | align-items: center; 43 | justify-content: center; 44 | width: 100%; 45 | height: 100%; 46 | } 47 | .flex-column-set { 48 | display: flex; 49 | flex-direction: row; 50 | } 51 | .flex-column-set > * { 52 | flex: 1 1 auto; 53 | } 54 | 55 | .hidden { 56 | visibility: hidden; 57 | } 58 | 59 | .list-none, .list-none > li { 60 | margin: 0; 61 | padding: 0; 62 | list-style: none; 63 | } 64 | 65 | .secondary { 66 | font-size: small; 67 | } 68 | .theme-light .secondary { 69 | color: lighten($colour-dark, 30%); 70 | } 71 | .theme-dark .secondary { 72 | color: darken($colour-light, 30%); 73 | } 74 | 75 | .table { 76 | display: table; 77 | } 78 | .table > * { 79 | display: table-row; 80 | } 81 | .table > * > * { 82 | display: table-cell; 83 | } 84 | 85 | .control { 86 | user-select: none; 87 | cursor: default; 88 | } 89 | .control.disabled { 90 | cursor: default !important; 91 | } 92 | .control-text { 93 | @extend .control; 94 | user-select: text; 95 | cursor: text; 96 | } 97 | .control-clickable { 98 | @extend .control; 99 | cursor: pointer; 100 | } 101 | -------------------------------------------------------------------------------- /scss/_font-awesome.scss: -------------------------------------------------------------------------------- 1 | // Minimalist Font-Awesome 2 | @font-face { 3 | font-family: 'FontAwesome'; 4 | src: url("../fonts/fontawesome-webfont.eot?v=4.5.0"); 5 | src: url("../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0") format("embedded-opentype"), url("../fonts/fontawesome-webfont.woff2?v=4.5.0") format("woff2"), url("../fonts/fontawesome-webfont.woff?v=4.5.0") format("woff"), url("../fonts/fontawesome-webfont.ttf?v=4.5.0") format("truetype"), url("../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular") format("svg"); 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | @mixin font-awesome-pseudo($content) { 11 | display: inline-block; 12 | font-family: FontAwesome; 13 | content: $content; 14 | font-weight: normal; 15 | text-decoration: none; 16 | user-select: none; 17 | } 18 | 19 | .fa-user:before { @include font-awesome-pseudo("\f007"); } 20 | .fa-th:before { @include font-awesome-pseudo("\f00a"); } 21 | .fa-times:before { @include font-awesome-pseudo("\f00d"); } 22 | .fa-home:before { @include font-awesome-pseudo("\f015"); font-size: 115%; } 23 | .fa-lock:before { @include font-awesome-pseudo("\f023"); } 24 | .fa-plus:before { @include font-awesome-pseudo("\f067"); } 25 | .fa-minus:before { @include font-awesome-pseudo("\f068"); } 26 | .fa-asterisk:before { @include font-awesome-pseudo("\f069"); } 27 | .fa-comment:before { @include font-awesome-pseudo("\f075"); } 28 | .fa-comments:before { @include font-awesome-pseudo("\f086"); } 29 | .fa-sign-out:before { @include font-awesome-pseudo("\f08b"); } 30 | .fa-trophy:before { @include font-awesome-pseudo("\f091"); } 31 | .fa-globe:before { @include font-awesome-pseudo("\f0ac"); } 32 | .fa-users:before { @include font-awesome-pseudo("\f0c0"); } 33 | .fa-caret-down:before { @include font-awesome-pseudo("\f0d7"); } 34 | .fa-caret-right:before { @include font-awesome-pseudo("\f0da"); } 35 | .fa-coffee:before { @include font-awesome-pseudo("\f0f4"); } 36 | .fa-circle-o:before { @include font-awesome-pseudo("\f10c"); } 37 | .fa-circle:before { @include font-awesome-pseudo("\f111"); } 38 | .fa-flag-checkered:before { @include font-awesome-pseudo("\f11e"); } 39 | .fa-graduation-cap:before { @include font-awesome-pseudo("\f19d"); } 40 | .fa-thumbs-up:before { @include font-awesome-pseudo("\f164"); } 41 | .fa-thumbs-down:before { @include font-awesome-pseudo("\f165"); } 42 | .fa-commenting:before { @include font-awesome-pseudo("\f27a"); } 43 | -------------------------------------------------------------------------------- /scss/_layout.scss: -------------------------------------------------------------------------------- 1 | #main { 2 | @extend .block-fixed; 3 | @extend .theme-dark; 4 | right: 33%; 5 | z-index: $zindex-main; 6 | } 7 | 8 | #sidebar { 9 | @extend .flex-fixed; 10 | @extend .theme-light; 11 | flex-direction: column; 12 | left: auto; 13 | width: 33%; 14 | z-index: $zindex-sidebar; 15 | } 16 | #sidebar > div { 17 | position: relative; 18 | overflow: visible; 19 | } 20 | #sidebar > div:last-child { 21 | flex: 1 1 auto; 22 | } 23 | 24 | #main > div { 25 | @extend .block-absolute; 26 | z-index: $zindex-main; 27 | } 28 | 29 | @media (min-width: map-get($responsive-breakpoints, "xxl")) { 30 | #main { 31 | right: 528px; 32 | } 33 | #sidebar { 34 | width: 528px; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scss/_reset.scss: -------------------------------------------------------------------------------- 1 | table { 2 | border-spacing: 0; 3 | border-collapse: collapse; 4 | } 5 | td { 6 | padding: 0; 7 | } 8 | -------------------------------------------------------------------------------- /scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // Colours 2 | $colour-black: #000000; 3 | $colour-dark: #292f36; 4 | $colour-medium: #bcbcbc; 5 | $colour-light: #e5e5e5; 6 | $colour-lighter: #f2f2f2; 7 | $colour-white: #ffffff; 8 | $colour-positive: #4ecdc4; 9 | $colour-negative: #ff6b6b; 10 | 11 | // Go Specific Variables 12 | $go-stone-diameter: 22mm; 13 | $go-board-size-min: 9; 14 | $go-board-size-max: 19; 15 | 16 | // View Parameters 17 | $text-box-padding-vertical: 0.6rem; 18 | $text-box-padding-horizontal: 0.6rem; 19 | 20 | $view-sidebar-right-margin: 28px; 21 | $view-go-clock-segment-off: darken($colour-dark, 2%); 22 | $view-go-clock-segment-on: $colour-light; 23 | $view-go-clock-width: 240px; 24 | $view-go-clock-height: 86px; 25 | 26 | // Z-indices 27 | $zindex-main: 1; 28 | $zindex-sidebar: $zindex-main + 100; 29 | $zindex-lightbox: 2000; 30 | 31 | // Responsive Breakpoints 32 | $responsive-breakpoints: ( 33 | xs: 0, 34 | sm: 544px, 35 | md: 768px, 36 | lg: 992px, 37 | xl: 1200px, 38 | xxl: 1600px 39 | ); 40 | -------------------------------------------------------------------------------- /scss/leben.scss: -------------------------------------------------------------------------------- 1 | // Sass Configuration Partial 2 | @import "variables"; 3 | 4 | // Dependency: Font-Awesome 5 | @import "font-awesome"; 6 | 7 | // KGS Leben 8 | @import "reset"; 9 | @import "classes"; 10 | @import "layout"; 11 | 12 | // Rolled up Views Partial 13 | /* __views.scss is produced by Gulp in its temporary working directory 14 | by concatenating all the Sass scripts nested inside the 'views' directory. */ 15 | @import "../.build/views"; 16 | -------------------------------------------------------------------------------- /sounds/pass.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenmartindale/kgs-leben/a74112fb2b535c3e4eb71e535029fd781164473d/sounds/pass.m4a -------------------------------------------------------------------------------- /sounds/pass.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenmartindale/kgs-leben/a74112fb2b535c3e4eb71e535029fd781164473d/sounds/pass.ogg -------------------------------------------------------------------------------- /sounds/stone.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenmartindale/kgs-leben/a74112fb2b535c3e4eb71e535029fd781164473d/sounds/stone.m4a -------------------------------------------------------------------------------- /sounds/stone.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenmartindale/kgs-leben/a74112fb2b535c3e4eb71e535029fd781164473d/sounds/stone.ogg -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "noImplicitAny": true, 5 | "declaration": false, 6 | 7 | "out": "leben.js", 8 | 9 | "sourceMap": true, 10 | "inlineSourceMap": false 11 | }, 12 | 13 | "exclude": [ 14 | "node_modules", 15 | ".build", 16 | "dist" 17 | ] 18 | } -------------------------------------------------------------------------------- /tsd/webkitAudioContext.d.ts: -------------------------------------------------------------------------------- 1 | declare var webkitAudioContext: { 2 | prototype: AudioContext; 3 | new(): AudioContext; 4 | } 5 | -------------------------------------------------------------------------------- /utilities/Array.ts: -------------------------------------------------------------------------------- 1 | namespace Utils { 2 | export function arrayEquals(left: T[], right: T[], comparisonFlags: ComparisonFlags): boolean { 3 | if (left === right) return true; 4 | if ((left == null) || (right == null)) return false; 5 | if (left.length != right.length) return false; 6 | 7 | if (comparisonFlags == ComparisonFlags.Shallow) { 8 | for (let i = 0; i < left.length; ++i) { 9 | if (left[i] !== right[i]) return false; 10 | } 11 | } 12 | else { 13 | for (let i = 0; i < left.length; ++i) { 14 | if (!valueEquals(left[i], right[i], comparisonFlags)) return false; 15 | } 16 | } 17 | 18 | return true; 19 | } 20 | 21 | export function cloneArray(o: T[], shallow?: boolean): T[] { 22 | if (shallow) { 23 | return o.slice(); 24 | } 25 | else { 26 | let clone: T[] = new Array(o.length); 27 | for (let j = 0; j < o.length; ++j) { 28 | clone[j] = valueClone(o[j]); 29 | } 30 | return clone; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /utilities/Audio.ts: -------------------------------------------------------------------------------- 1 | namespace Utils { 2 | export interface AudioCodecInfo { 3 | name: string; 4 | type: string; 5 | extension: string; 6 | } 7 | 8 | export const AudioCodecs: AudioCodecInfo[] = [ 9 | { 10 | name: 'Ogg Vorbis Audio', 11 | type: 'audio/ogg; codecs="vorbis"', 12 | extension: '.ogg' 13 | }, 14 | { 15 | name: 'MP4 AAC Audio', 16 | type: 'audio/mp4; codecs="mp4a.40.5"', 17 | extension: '.m4a' 18 | } 19 | ]; 20 | } 21 | -------------------------------------------------------------------------------- /utilities/HTML.ts: -------------------------------------------------------------------------------- 1 | namespace Utils { 2 | export function htmlTimeToSeconds(time: string): number { 3 | if (!time) return null; 4 | else { 5 | let parts = time.split(":"); 6 | return +parts[0] * 3600 + +parts[1] * 60 + +parts[2]; 7 | } 8 | } 9 | 10 | export function htmlSecondsToTime(seconds: number): string { 11 | let h = ~~(seconds / 3600); 12 | seconds -= h * 3600; 13 | let m = ~~(seconds / 60); 14 | seconds -= m * 60; 15 | 16 | return ("00" + h.toString()).slice(-2) + ":" 17 | + ("00" + m.toString()).slice(-2) + ":" 18 | + ("00" + seconds.toString()).slice(-2); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /utilities/Log.ts: -------------------------------------------------------------------------------- 1 | namespace Utils { 2 | export const enum LogSeverity { 3 | Default, 4 | Info, 5 | Success, 6 | Debug, 7 | Error, 8 | Warning 9 | } 10 | 11 | interface LogOptions { 12 | [severity: number]: boolean; 13 | } 14 | 15 | let logOptions: LogOptions = {}; 16 | logOptions[LogSeverity.Default] = true; 17 | logOptions[LogSeverity.Info] = true; 18 | logOptions[LogSeverity.Success] = true; 19 | logOptions[LogSeverity.Debug] = false; 20 | logOptions[LogSeverity.Error] = true; 21 | logOptions[LogSeverity.Warning] = true; 22 | 23 | interface LogMethods { 24 | [severity: number]: (message?: string, ...optionalParams: any[]) => void; 25 | } 26 | 27 | let logMethods: LogMethods = {}; 28 | if (console) { 29 | logMethods[LogSeverity.Default] = (console.log)? console.log.bind(console) : undefined; 30 | logMethods[LogSeverity.Info] = (console.info)? console.info.bind(console) : logMethods[LogSeverity.Default]; 31 | logMethods[LogSeverity.Success] = logMethods[LogSeverity.Info]; 32 | logMethods[LogSeverity.Debug] = logMethods[LogSeverity.Info]; 33 | logMethods[LogSeverity.Warning] = (console.warn)? console.warn.bind(console) : logMethods[LogSeverity.Default]; 34 | logMethods[LogSeverity.Error] = (console.error)? console.error.bind(console) : logMethods[LogSeverity.Warning]; 35 | } 36 | 37 | export function logEnabled(severity: Utils.LogSeverity): boolean { 38 | return ((logOptions[severity]) && (logMethods[severity]))? true : false; 39 | } 40 | 41 | export function log(severity: Utils.LogSeverity, message?: string, ...optionalParams: any[]) { 42 | if ((logOptions[severity]) && (logMethods[severity])) logMethods[severity](message, ...optionalParams); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /utilities/Object.ts: -------------------------------------------------------------------------------- 1 | namespace Utils { 2 | export const enum ComparisonFlags { 3 | Shallow = 0, 4 | Values = (1 << 0), 5 | ArraysAsSets = Values | (1 << 1) 6 | } 7 | 8 | export function valueClone(data: T): T { 9 | if (isPrimitive(data)) return data; 10 | else if (Utils.isArray(data)) return cloneArray(data, false); 11 | else if (Utils.isFunction(data)) throw "Cloning functions is not supported"; 12 | 13 | let clone: any = {}; 14 | for (let k in data) { 15 | let v: any = (data)[k]; 16 | if (!Utils.isFunction(v)) { 17 | clone[k] = valueClone(v); 18 | } 19 | } 20 | 21 | return clone; 22 | } 23 | 24 | export function valueEquals(left: T, right: T, comparisonFlags: ComparisonFlags): boolean { 25 | if (left == right) return true; 26 | else if ((left == null) || (right == null)) return false; 27 | 28 | let arraysAsSets: boolean = ((comparisonFlags & ComparisonFlags.ArraysAsSets) == ComparisonFlags.ArraysAsSets); 29 | if ((isArray(left)) && (isArray(right))) { 30 | return (arraysAsSets)? setEquals(left, right, comparisonFlags) : arrayEquals(left, right, comparisonFlags); 31 | } 32 | else if ((isObject(left)) && (isObject(right))) { 33 | let leftKeys = Object.keys(left); 34 | let rightKeys = Object.keys(right); 35 | if (leftKeys.length != rightKeys.length) return false; 36 | 37 | for (let j = 0; j < rightKeys.length; ++j) { 38 | let k = rightKeys[j]; 39 | if ((leftKeys.indexOf(k) < 0) || (!valueEquals((left)[k], (right)[k], comparisonFlags))) return false; 40 | } 41 | 42 | return true; 43 | } 44 | else return false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /utilities/Random.tests.ts: -------------------------------------------------------------------------------- 1 | namespace Utils { 2 | export function randomInteger(min: number, max: number, inclusive?: boolean): number { 3 | let w: number = Math.floor(max - min); 4 | if (inclusive)++w; 5 | if (w < 1) throw new Error("Random range may not be negative"); 6 | return Math.floor((Math.random() * w) + min); 7 | } 8 | 9 | export function randomBoolean(p?: number): boolean { 10 | if (null == p) p = 0.5; 11 | return (Math.random() < p); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /utilities/Set.ts: -------------------------------------------------------------------------------- 1 | namespace Utils { 2 | export function setSync(target: T[], source: T[]): boolean { 3 | let matched: { [index: number]: number } = {}; 4 | let originalLength: number = target.length; 5 | let removed: number = 0; 6 | for (let j = 0; j < originalLength; ++j) { 7 | let idx: number = source.indexOf(target[j]); 8 | if (idx >= 0) { 9 | matched[idx] = j; 10 | if (removed > 0) target[j - removed] = target[j]; 11 | } 12 | else ++removed; 13 | } 14 | 15 | if (removed > 0) target.length = (originalLength - removed); 16 | 17 | for (let i = 0; i < source.length; ++i) { 18 | if (matched[i] == null) { 19 | target.push(source[i]); 20 | } 21 | } 22 | 23 | return ((removed > 0) || (target.length != originalLength)); 24 | } 25 | 26 | export function setAdd(target: T[], value: T): boolean { 27 | if (target.lastIndexOf(value) < 0) { 28 | target.push(value); 29 | return true; 30 | } 31 | else return false; 32 | } 33 | 34 | export function setRemove(target: T[], value: T): boolean { 35 | let index = target.lastIndexOf(value); 36 | if (index < 0) return false; 37 | else { 38 | target.splice(index, 1); 39 | return true; 40 | } 41 | } 42 | 43 | export function setEquals(left: T[], right: T[], comparisonFlags: ComparisonFlags): boolean { 44 | if (left === right) return true; 45 | if ((left == null) || (right == null)) return false; 46 | if (left.length != right.length) return false; 47 | 48 | if (comparisonFlags == ComparisonFlags.Shallow) { 49 | for (let j = 0; j < right.length; ++j) { 50 | if (left.indexOf(right[j]) < 0) return false; 51 | } 52 | } 53 | else { 54 | for (let j = 0; j < right.length; ++j) { 55 | let found: boolean = false; 56 | for (let k = 0; k < left.length; ++k) { 57 | if (valueEquals(left[k], right[j], comparisonFlags)) { 58 | found = true; 59 | break; 60 | } 61 | } 62 | 63 | if (!found) return false; 64 | } 65 | } 66 | 67 | return true; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /utilities/Types.ts: -------------------------------------------------------------------------------- 1 | namespace Utils { 2 | const _undefined: string = 'undefined'; 3 | const _object: string = 'object'; 4 | const _function: string = 'function'; 5 | const _string: string = 'string'; 6 | const _number: string = 'number'; 7 | const _boolean: string = 'boolean'; 8 | 9 | // Returns true if and only if the parameter is a primitive value type or an immutable wrapper 10 | export function isPrimitive(o: any, excludeWrappers?: boolean): boolean { 11 | switch (typeof o) { 12 | case _undefined: 13 | case _string: 14 | case _number: 15 | case _boolean: 16 | return true; 17 | 18 | case _object: 19 | return ((o == null) || ((!excludeWrappers) && (isWrapper(o)))); 20 | 21 | default: return false; 22 | } 23 | } 24 | 25 | // Returns true if and only if the parameter is an instance of an immutable primitive wrapper 26 | export function isWrapper(o: any): boolean { 27 | return ((o instanceof String) 28 | || (o instanceof Number) 29 | || (o instanceof Boolean)); 30 | } 31 | 32 | export function isDefined(o: any): boolean { 33 | return (typeof o !== _undefined); 34 | } 35 | 36 | export function isObject(o: any, includeWrappers?: boolean): boolean { 37 | return ((typeof o === _object) && ((includeWrappers) || (!isWrapper(o)))); 38 | } 39 | 40 | export function isFunction(o: any): boolean { 41 | return (typeof o === _function); 42 | } 43 | 44 | export function isString(o: any): boolean { 45 | return ((typeof o === _string) || (o instanceof String)); 46 | } 47 | 48 | export function isNumber(o: any): boolean { 49 | return ((typeof o === _number) || (o instanceof Number)); 50 | } 51 | 52 | export function isBoolean(o: any): boolean { 53 | return ((typeof o === _boolean) || (o instanceof Boolean)); 54 | } 55 | 56 | export function isArray(o: any): boolean { 57 | return (o.constructor === Array); 58 | } 59 | 60 | export function isNode(o: any, nodeType?: number): boolean { 61 | if ((typeof o !== _object) || ((o).nodeName === undefined)) return false; 62 | return (nodeType == null)? (typeof (o).nodeType === _number) : ((o).nodeType === nodeType); 63 | } 64 | 65 | export function isElement(o: any): boolean { 66 | return isNode(o, 1); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /views/DataBoundList.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export abstract class DataBoundList extends Views.View { 4 | private _initialised: boolean; 5 | private _keys: string[]; 6 | 7 | constructor(container: C) { 8 | super(container); 9 | this._initialised = false; 10 | this._keys = []; 11 | } 12 | 13 | private initialise() { 14 | if ((!this._initialised) && (this.root.childElementCount > 0)) { 15 | Utils.log(Utils.LogSeverity.Warning, "Data Bound List: " + this.root.childElementCount.toString() + " alien element(s) removed"); 16 | } 17 | 18 | $(this.root).empty(); 19 | this._initialised = true; 20 | } 21 | 22 | private beginBind(dataCount: number): boolean { 23 | // Initialise on first Bind or if incoming data is empty 24 | if ((!this._initialised) || (dataCount == 0)) this.initialise(); 25 | 26 | // Short-circuit if no data was supplied 27 | return (dataCount > 0); 28 | } 29 | 30 | protected bind(source: (key: string) => T, keys: (number | string)[], moveKeys?: boolean) { 31 | // Examine incoming data 32 | let oldKeys: string[] = this._keys; 33 | let dataMap: { [key: string]: boolean } = {}; 34 | let dataCount: number = 0; 35 | if (source != null) { 36 | this._keys = keys; 37 | if (!moveKeys) this._keys = this._keys.slice(); 38 | for (let i = 0; i < this._keys.length; ++i) { 39 | let k: string = this._keys[i]; 40 | if (source(k)) { 41 | if (dataCount != i) this._keys[dataCount] = k; 42 | dataMap[k] = true; 43 | ++dataCount; 44 | } 45 | } 46 | 47 | this._keys.length = dataCount; 48 | } 49 | 50 | // Begin Binding ... 51 | if (!this.beginBind(dataCount)) return; 52 | 53 | // Process known keys and remove obsolete elements 54 | let oldIndex: { [key: string]: number } = {}; 55 | let oldCount: number = 0; 56 | for (let j = 0; j < oldKeys.length; ++j) { 57 | let k: string = oldKeys[j]; 58 | if (dataMap[k]) { 59 | oldIndex[k] = oldCount; 60 | if (oldCount != j) { 61 | oldKeys[oldCount] = k; 62 | } 63 | 64 | ++oldCount; 65 | } 66 | else $(this.root.children[oldCount]).remove(); 67 | } 68 | 69 | oldKeys.length = oldCount; 70 | 71 | // Process data 72 | let created: number = 0; 73 | for (let i = 0; i < dataCount; ++i) { 74 | let k: string = this._keys[i]; 75 | let datum: T = source(k); 76 | let idx: number = oldIndex[k]; 77 | if (null == idx) { 78 | this.root.insertBefore(this.createChild(k, datum), this.root.children[i]); 79 | ++created; 80 | } 81 | else { 82 | let child: Element = this.root.children[idx + created]; 83 | if ((idx + created) > i) { 84 | let right: Node = child.nextSibling; 85 | let left: Node = this.root.children[i]; 86 | let leftKey: string = oldKeys[i - created]; 87 | 88 | this.root.insertBefore(child, left); 89 | if ((idx + created) > (i + 1)) { 90 | this.root.insertBefore(left, right); 91 | } 92 | 93 | oldKeys[idx] = leftKey; 94 | oldIndex[leftKey] = idx; 95 | } 96 | 97 | this.updateChild(k, datum, child as E); 98 | } 99 | } 100 | } 101 | 102 | protected bindDictionary(data: { [key: string]: T }, keys?: (number | string)[]) { 103 | if (data != null) { 104 | let moveKeys: boolean = false; 105 | if (keys == null) { 106 | keys = Object.keys(data); 107 | moveKeys = true; 108 | } 109 | 110 | return this.bind((k: string) => data[k], keys, moveKeys); 111 | } 112 | } 113 | 114 | protected bindArray(data: T[]) { 115 | // Examine incoming data 116 | let dataCount: number = (data == null)? 0 : data.length; 117 | if (data) data 118 | 119 | // Begin Binding ... 120 | if (!this.beginBind(dataCount)) return; 121 | 122 | // Process data 123 | let oldCount: number = Math.min(this._keys.length, this.root.childElementCount); 124 | for (let i = 0; i < dataCount; ++i) { 125 | let k: string = i.toString(); 126 | let datum: T = data[i]; 127 | if (i < oldCount) { 128 | let child: Element = this.root.children[i]; 129 | this.updateChild(k, datum, child as E); 130 | } 131 | else { 132 | let child: Element = this.createChild(k, datum); 133 | this.root.appendChild(child); 134 | this._keys[i] = k; 135 | } 136 | } 137 | 138 | // Remove old elements 139 | for (let j = (oldCount - 1); j >= dataCount; --j) { 140 | $(this.root.children[j]).remove(); 141 | } 142 | 143 | // Truncate key array 144 | this._keys.length = dataCount; 145 | } 146 | 147 | protected abstract createChild(key: string, datum: T): E; 148 | protected abstract updateChild(key: string, datum: T, element: E): void; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /views/Templates.ts: -------------------------------------------------------------------------------- 1 | namespace Views { 2 | export namespace Templates { 3 | let _templates: { [view: string]: Element }; 4 | 5 | function initialiseSuccess(data: any, textStatus: string, jqXHR: JQueryXHR, callback: Function) { 6 | let templates: { [view: string]: Element } = {}; 7 | let jquery = $(data); 8 | for (let i = 0; i < jquery.length; ++i) { 9 | let template = jquery[i]; 10 | if ((template) && (template.getAttribute) && (template.getAttribute('data-view'))) { 11 | templates[template.getAttribute('data-view')] = template; 12 | } 13 | } 14 | 15 | _templates = templates; 16 | callback(); 17 | } 18 | 19 | function initialiseError(jqXHR: JQueryXHR, textStatus: string, errorThrown?: string) { 20 | Utils.log(Utils.LogSeverity.Error, "Failed to initialise template framework", textStatus, errorThrown); 21 | } 22 | 23 | export function initialise(url: string, callback: Function) { 24 | if (_templates) return; 25 | 26 | $.ajax({ 27 | type: 'GET', 28 | url: url, 29 | success: (data, textStatus, jqXHR) => initialiseSuccess(data, textStatus, jqXHR, callback), 30 | error: initialiseError 31 | }); 32 | } 33 | 34 | export function cloneTemplate(view: string): T { 35 | if (!_templates) throw "Template framework not initialised"; 36 | 37 | let template = _templates[view]; 38 | if (!template) throw "Template not found: " + view; 39 | 40 | return template.firstElementChild.cloneNode(true); 41 | } 42 | 43 | export function createDiv(className: string): HTMLDivElement { 44 | let div = document.createElement('div'); 45 | div.className = className; 46 | return div; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /views/View.ts: -------------------------------------------------------------------------------- 1 | namespace Views { 2 | export abstract class View { 3 | protected root: RootElement; 4 | 5 | private _parent: HTMLElement; 6 | private _activated: boolean; 7 | 8 | constructor(root: RootElement) { 9 | this.root = root; 10 | } 11 | 12 | protected get parent(): HTMLElement { 13 | return this._parent; 14 | } 15 | 16 | // N.B.: Sub-classes should call super.attach() at the beginning of their attach routine 17 | public attach(parent: HTMLElement, insertBefore?: boolean | Element): void { 18 | if (parent == null) throw "Parent element may not be null"; 19 | 20 | if (insertBefore) { 21 | parent.insertBefore(this.root, (Utils.isElement(insertBefore))? (insertBefore) : parent.firstElementChild); 22 | } 23 | else { 24 | parent.appendChild(this.root); 25 | } 26 | 27 | this._parent = parent; 28 | } 29 | 30 | protected get activated(): boolean { 31 | return this._activated; 32 | } 33 | 34 | // N.B.: Sub-classes should call super.activate() at the end of their activation routine 35 | public activate(): void { 36 | this._activated = true; 37 | } 38 | 39 | // N.B.: Sub-classes should call super.deactivate() at the beginning of their deactivation routine 40 | public deactivate(): void { 41 | this._activated = false; 42 | } 43 | 44 | public hide() { 45 | this.root.classList.add('hidden'); 46 | } 47 | public show() { 48 | this.root.classList.remove('hidden'); 49 | } 50 | public get hidden(): boolean { 51 | return this.root.classList.contains('hidden'); 52 | } 53 | public set hidden(hide: boolean) { 54 | if (hide) this.hide(); 55 | else this.show(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /views/ambient-status-panel/AmbientStatusPanel.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 8 |
9 |
10 | -------------------------------------------------------------------------------- /views/ambient-status-panel/AmbientStatusPanel.scss: -------------------------------------------------------------------------------- 1 | .ambient-status-panel div { 2 | @extend .control; 3 | text-align: center; 4 | line-height: 26px; 5 | vertical-align: top; 6 | overflow: hidden; 7 | } 8 | .ambient-status-panel > div.hidden { 9 | display: none; 10 | } 11 | .ambient-status-panel > div:last-child { 12 | margin-bottom: 18px; 13 | } 14 | 15 | .ambient-status-panel div * { 16 | display: inline-block; 17 | height: 26px; 18 | line-height: 26px; 19 | vertical-align: top; 20 | } 21 | .ambient-status-panel div span { 22 | font-style: oblique; 23 | } 24 | 25 | .ambient-status-panel div button { 26 | margin-left: 12px; 27 | color: $colour-negative; 28 | } 29 | .ambient-status-panel div button:hover { 30 | color: $colour-negative; 31 | } 32 | -------------------------------------------------------------------------------- /views/ambient-status-panel/AmbientStatusPanel.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class AmbientStatusPanel extends Views.View { 4 | private _automatchSeekingDiv: HTMLDivElement; 5 | 6 | public automatchCancelCallback: Function; 7 | 8 | constructor() { 9 | super(Views.Templates.cloneTemplate('ambient-status-panel')); 10 | this._automatchSeekingDiv = this.root.querySelector('div.automatch-seeking') as HTMLDivElement; 11 | } 12 | 13 | public activate(): void { 14 | this._automatchSeekingDiv.querySelector('button').addEventListener('click', this._onAutomatchCancelClick); 15 | 16 | super.activate(); 17 | } 18 | 19 | public deactivate(): void { 20 | super.deactivate(); 21 | 22 | this._automatchSeekingDiv.querySelector('button').removeEventListener('click', this._onAutomatchCancelClick); 23 | } 24 | 25 | public set automatchSeeking(seeking: boolean) { 26 | if (seeking) { 27 | this._automatchSeekingDiv.classList.remove('hidden'); 28 | } 29 | else { 30 | this._automatchSeekingDiv.classList.add('hidden'); 31 | } 32 | } 33 | 34 | private _onAutomatchCancelClick = () => { 35 | if (this.automatchCancelCallback) this.automatchCancelCallback(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /views/automatch-form/AutomatchForm.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Auto-match

4 |
5 |
6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 |
14 |
15 |
16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 |
24 |
25 |
    26 |
  • 27 | 28 | 29 |
  • 30 |
  • 31 | 32 | 33 |
  • 34 |
  • 35 | 36 | 37 |
  • 38 |
39 |
40 |
41 |
    42 |
  • 43 | 44 | 45 |
  • 46 |
  • 47 | 48 | 49 |
  • 50 |
  • 51 | 52 | 53 |
  • 54 |
55 |
56 |
57 |
58 |
59 | 60 | 61 |
62 |
63 | 64 | 65 |
66 |
67 | 68 | 69 |
70 |
71 | 72 |
73 |
74 |
-------------------------------------------------------------------------------- /views/automatch-form/AutomatchForm.scss: -------------------------------------------------------------------------------- 1 | .automatch { 2 | padding-left: 12px; 3 | } 4 | 5 | .disabled .automatch label, .disabled .automatch input { 6 | color: $colour-medium; 7 | cursor: default; 8 | } 9 | 10 | .automatch > .hidden { 11 | display: none; 12 | } 13 | .automatch > div { 14 | margin-bottom: 12px; 15 | } 16 | 17 | .automatch > div.automatch-float-row { 18 | overflow: hidden; 19 | } 20 | .automatch > div.automatch-float-row > div { 21 | float: left; 22 | line-height: 1.8em; 23 | vertical-align: baseline; 24 | } 25 | .automatch > div.automatch-float-row > div + div { 26 | margin-left: 12px; 27 | } 28 | 29 | .automatch div.input-estimated-rank, .automatch div.input-max-handicap { 30 | & input { 31 | float: left; 32 | } 33 | 34 | &::after { 35 | float: left; 36 | margin-left: -50px; 37 | pointer-events: none; 38 | color: $colour-medium; 39 | font-size: 8pt; 40 | } 41 | } 42 | .automatch div.input-estimated-rank::after { 43 | content: "kyu"; 44 | } 45 | .automatch div.input-max-handicap::after { 46 | content: "stones"; 47 | } 48 | 49 | .automatch div.hidden { 50 | display: none; 51 | } 52 | -------------------------------------------------------------------------------- /views/channel-list/ChannelList.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class ChannelList extends SidebarMenu { 4 | constructor() { 5 | super(); 6 | } 7 | 8 | public update(channels: { [key: string]: Models.Channel }, keys: number[], activeChannelId: number) { 9 | let source: (key: string) => SidebarMenuItem = (key) => { 10 | let channelId: number; 11 | let className: string = "fa-minus"; 12 | let name: string; 13 | let closeable: boolean; 14 | 15 | if (+key != Controllers.HomeController.channelId) { 16 | let channel: Models.Channel = channels[key]; 17 | channelId = channel.channelId; 18 | 19 | if (channel.channelType != null) { 20 | switch (channel.channelType) { 21 | case Models.ChannelType.Room: className = 'fa-users'; break; 22 | case Models.ChannelType.List: className = 'fa-th'; break; 23 | case Models.ChannelType.Conversation: className = 'fa-commenting'; break; 24 | case Models.ChannelType.Game: className = 'fa-circle'; break; 25 | } 26 | } 27 | 28 | name = channel.name; 29 | closeable = true; 30 | } 31 | else { 32 | channelId = Controllers.HomeController.channelId; 33 | name = "Home"; 34 | className = "fa-home"; 35 | closeable = false; 36 | } 37 | 38 | if (activeChannelId == channelId) className += " active"; 39 | 40 | return { 41 | id: channelId, 42 | name: name, 43 | className: className, 44 | closeable: closeable 45 | }; 46 | } 47 | 48 | keys = keys.slice(); 49 | keys.unshift(Controllers.HomeController.channelId); 50 | 51 | this.bind(source, keys, true); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /views/chat-form/ChatForm.scss: -------------------------------------------------------------------------------- 1 | div.chat-form > div { 2 | @extend .block-absolute; 3 | bottom: 3rem; 4 | } 5 | 6 | div.chat-form > form { 7 | @extend .block-absolute; 8 | height: 3rem; 9 | top: auto; 10 | } 11 | div.chat-form > form > input[type="text"] { 12 | height: 3rem; 13 | width: 100%; 14 | border-right: 0; 15 | border-bottom: 0; 16 | } 17 | -------------------------------------------------------------------------------- /views/chat-form/ChatForm.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class ChatForm extends Views.View { 4 | private _messageList: Views.ChatMessageList; 5 | private _memberList: Views.ChatMemberList; 6 | private _form: HTMLFormElement; 7 | private _inputMessage: HTMLInputElement; 8 | 9 | public submitCallback: (form: ChatForm) => void; 10 | 11 | constructor() { 12 | super(Templates.createDiv('chat-form')); 13 | 14 | let wrapper = document.createElement('div'); 15 | this.root.appendChild(wrapper); 16 | 17 | this._messageList = new Views.ChatMessageList(); 18 | this._messageList.attach(wrapper); 19 | 20 | this._memberList = new Views.ChatMemberList(); 21 | this._memberList.attach(wrapper); 22 | 23 | this._form = document.createElement('form'); 24 | this._inputMessage = document.createElement('input'); 25 | this._inputMessage.type = 'text'; 26 | this._inputMessage.placeholder = 'send a message...'; 27 | this._inputMessage.autocomplete = 'off'; 28 | this._form.appendChild(this._inputMessage); 29 | 30 | this.root.appendChild(this._form); 31 | $(this._form).submit((e) => this.onFormSubmit(e)); 32 | } 33 | 34 | public activate(): void { 35 | this._messageList.activate(); 36 | this._memberList.activate(); 37 | super.activate(); 38 | } 39 | 40 | public deactivate(): void { 41 | super.deactivate(); 42 | this._messageList.deactivate(); 43 | this._memberList.deactivate(); 44 | } 45 | 46 | public get message() { return this._inputMessage.value; } 47 | public set message(value: string) { this._inputMessage.value = value; } 48 | 49 | public get messageList(): Views.ChatMessageList { 50 | return this._messageList; 51 | } 52 | 53 | public get memberList(): Views.ChatMemberList { 54 | return this._memberList; 55 | } 56 | 57 | public focus() { 58 | $(this._inputMessage).focus(); 59 | } 60 | 61 | private onFormSubmit(e: JQueryEventObject) { 62 | e.preventDefault(); 63 | if (this.message.length == 0) return; 64 | if (this.submitCallback) this.submitCallback(this); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /views/chat-member-list/ChatMemberList.scss: -------------------------------------------------------------------------------- 1 | ul.chat-member-list { 2 | @extend .list-none; 3 | @extend .block-absolute; 4 | width: 40%; 5 | left: auto; 6 | white-space: nowrap; 7 | overflow-x: hidden; 8 | overflow-y: auto; 9 | } 10 | 11 | ul.chat-member-list li span:nth-child(2) { 12 | margin-left: 0.5rem; 13 | white-space: nowrap; 14 | } 15 | 16 | ul.chat-member-list li { 17 | margin-left: 0.4rem; 18 | } 19 | -------------------------------------------------------------------------------- /views/chat-member-list/ChatMemberList.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class ChatMemberList extends Views.DataBoundList { 4 | constructor() { 5 | super(document.createElement('ul')); 6 | this.root.className = 'chat-member-list'; 7 | } 8 | 9 | public update(users: { [name: string]: Models.User }, memberNames: string[]) { 10 | this.bindDictionary(users, memberNames); 11 | } 12 | 13 | protected createChild(key: string, datum: Models.User): HTMLLIElement { 14 | let element = document.createElement('li'); 15 | element.appendChild(document.createElement('span')); 16 | 17 | let rankSpan = document.createElement('span'); 18 | rankSpan.className = 'secondary'; 19 | element.appendChild(rankSpan); 20 | 21 | this.updateChild(key, datum, element); 22 | return element; 23 | } 24 | 25 | protected updateChild(key: string, datum: Models.User, element: HTMLLIElement): void { 26 | let spans = element.getElementsByTagName('span'); 27 | spans[0].innerText = datum.name; 28 | spans[1].innerText = (datum.rank)? datum.rank : ""; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /views/chat-message-list/ChatMessageList.scss: -------------------------------------------------------------------------------- 1 | ul.chat-message-list { 2 | @extend .list-none; 3 | @extend .block-absolute; 4 | height: auto; 5 | top: auto; 6 | max-height: 100%; 7 | width: 60%; 8 | right: auto; 9 | overflow-x: hidden; 10 | overflow-y: auto; 11 | } 12 | 13 | ul.chat-message-list > li > p { 14 | margin: 0; 15 | } 16 | ul.chat-message-list > li > p + p { 17 | margin-top: 1rem; 18 | } 19 | 20 | ul.chat-message-list li { 21 | padding-left: 0.7rem; 22 | padding-bottom: 0.7rem; 23 | } 24 | ul.chat-message-list li span:first-child { 25 | white-space: nowrap; 26 | font-weight: bold; 27 | cursor: pointer; 28 | } 29 | ul.chat-message-list li span:nth-child(2) { 30 | margin-left: 0.5rem; 31 | white-space: nowrap; 32 | cursor: help; 33 | } 34 | -------------------------------------------------------------------------------- /views/chat-message-list/ChatMessageList.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class ChatMessageList extends Views.DataBoundList { 4 | private _updateDateTime: number; 5 | 6 | private _onResize: (e: UIEvent) => void; 7 | 8 | constructor() { 9 | super(document.createElement('ul')); 10 | this.root.className = 'chat-message-list'; 11 | 12 | this._onResize = (e: UIEvent) => this.scrollToEnd(); 13 | } 14 | 15 | public activate(): void { 16 | window.addEventListener("resize", this._onResize); 17 | super.activate(); 18 | } 19 | 20 | public deactivate(): void { 21 | super.deactivate(); 22 | window.removeEventListener("resize", this._onResize); 23 | } 24 | 25 | public update(chats: Models.Chat[]) { 26 | this._updateDateTime = new Date().getTime(); 27 | 28 | this.bindArray(chats); 29 | this.scrollToEnd(); 30 | } 31 | 32 | private updateInfoSpan(datum: Models.Chat, span: HTMLSpanElement) { 33 | let receivedTime = datum.received.getTime(); 34 | 35 | let minutes: number = (this._updateDateTime - receivedTime) / 60000; 36 | if (minutes < 5) 37 | span.innerText = "minutes ago"; 38 | else if (minutes < 60) 39 | span.innerText = "within the hour"; 40 | else if (minutes < 1440) 41 | span.innerText = "today"; 42 | else if (minutes < 2880) 43 | span.innerText = "yesterday"; 44 | else 45 | span.innerText = "long ago"; 46 | 47 | if (!span.title) span.title = new Date(receivedTime - datum.received.getTimezoneOffset() * 60000).toISOString().replace("T", " ").substring(0, 16); 48 | } 49 | 50 | private buildInfoTitle(datum: Models.Chat): string { 51 | return new Date(datum.received.getTime() - datum.received.getTimezoneOffset() * 60000).toISOString().replace("T", " ").substring(0, 16); 52 | } 53 | 54 | protected createChild(key: string, datum: Models.Chat): HTMLLIElement { 55 | let senderName = document.createElement('span'); 56 | senderName.innerText = datum.sender; 57 | 58 | let messageInfo = document.createElement('span'); 59 | messageInfo.className = 'secondary'; 60 | this.updateInfoSpan(datum, messageInfo); 61 | 62 | let paragraph = document.createElement('p'); 63 | paragraph.innerText = datum.text; 64 | 65 | let listItem = document.createElement('li'); 66 | listItem.appendChild(senderName); 67 | listItem.appendChild(messageInfo); 68 | listItem.appendChild(paragraph); 69 | return listItem; 70 | } 71 | 72 | protected updateChild(key: string, datum: Models.Chat, element: HTMLLIElement): void { 73 | let spans = element.getElementsByTagName('span'); 74 | this.updateInfoSpan(datum, spans[1]); 75 | } 76 | 77 | private scrollToEnd() { 78 | if (this.root.lastElementChild) (this.root.lastElementChild).scrollIntoView(false); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /views/form/Button.scss: -------------------------------------------------------------------------------- 1 | $button-padding-vertical: 1px; 2 | $button-padding-horizontal: 10px; 3 | 4 | button, .button { 5 | @extend .control-clickable; 6 | font-size: 12pt; 7 | line-height: 24px; 8 | text-align: center; 9 | background-color: darken($colour-light, 5%); 10 | border: 0; 11 | outline: none; 12 | box-sizing: border-box; 13 | padding: $button-padding-vertical $button-padding-horizontal; 14 | 15 | &.large { 16 | font-size: large; 17 | line-height: 28px; 18 | padding: 4px 12px; 19 | } 20 | 21 | &:hover, &:active { 22 | border: 0; 23 | background-color: darken($colour-light, 8%); 24 | } 25 | 26 | .theme-dark & { 27 | background-color: lighten($colour-dark, 10%); 28 | } 29 | .theme-dark &.disabled, .theme-dark &.disabled:hover, .theme-dark .disabled &, .theme-dark .disabled &:hover { 30 | background-color: lighten($colour-dark, 5%); 31 | color: darken($colour-light, 40%); 32 | } 33 | .theme-dark &:hover { 34 | background-color: lighten($colour-dark, 15%); 35 | } 36 | 37 | .theme-dark &.positive, .theme-light &.positive { 38 | background-color: $colour-positive; 39 | } 40 | .theme-dark &.positive:hover, .theme-light &.positive:hover { 41 | background-color: lighten($colour-positive, 10%); 42 | } 43 | 44 | .theme-dark &.negative, .theme-light &.negative { 45 | background-color: $colour-negative; 46 | } 47 | .theme-dark &.negative:hover, .theme-light &.negative:hover { 48 | background-color: lighten($colour-negative, 10%); 49 | } 50 | 51 | & span + span { 52 | margin-left: 0.4rem; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /views/form/Form.scss: -------------------------------------------------------------------------------- 1 | form * { 2 | @extend .control; 3 | } 4 | 5 | label { 6 | @extend .control; 7 | } 8 | -------------------------------------------------------------------------------- /views/form/Input.scss: -------------------------------------------------------------------------------- 1 | input[type=text], input[type=password], input[type=number], input[type=time] { 2 | @extend .control-text; 3 | box-sizing: border-box; 4 | background-color: $colour-white; 5 | border: 1px solid darken($colour-light, 15%); 6 | outline: none; 7 | padding: $text-box-padding-vertical $text-box-padding-horizontal; 8 | 9 | &.disabled, &:disabled, .theme-dark &.disabled, .theme-dark &:disabled { 10 | background-color: transparent; 11 | border-color: transparent; 12 | } 13 | 14 | .theme-dark & { 15 | color: $colour-light; 16 | background-color: $colour-dark; 17 | border: 1px solid lighten($colour-dark, 15%); 18 | } 19 | } 20 | 21 | input[type=number] { 22 | font-size: larger; 23 | line-height: 1.5rem; 24 | padding: 0 0 0 4px; 25 | } 26 | 27 | input[type=checkbox] { 28 | @extend .control-clickable; 29 | } 30 | -------------------------------------------------------------------------------- /views/form/Switch.scss: -------------------------------------------------------------------------------- 1 | $switch-height: 1.8em; 2 | $switch-width: 60px; 3 | $switch-narrow-width: 25px; 4 | 5 | .switch { 6 | position: relative; 7 | overflow: hidden; 8 | font-size: 14px; 9 | margin-bottom: 0.2em; 10 | } 11 | .switch label { 12 | position: relative; 13 | float: left; 14 | z-index: $zindex-main + 2; 15 | border: 0; 16 | margin: 0; 17 | width: $switch-width; 18 | line-height: $switch-height; 19 | vertical-align: middle; 20 | text-align: center; 21 | font-weight: bold; 22 | font-variant: small-caps; 23 | cursor: pointer; 24 | } 25 | .switch-narrow label { 26 | width: $switch-narrow-width; 27 | } 28 | 29 | .switch input:disabled + label { 30 | cursor: default; 31 | color: darken($colour-light, 40%); 32 | } 33 | 34 | .switch input { 35 | display: none; 36 | } 37 | .switch input:nth-child(3):checked ~ .switch-selection { left: ($switch-width * 1); } 38 | .switch input:nth-child(5):checked ~ .switch-selection { left: ($switch-width * 2); } 39 | .switch input:nth-child(7):checked ~ .switch-selection { left: ($switch-width * 3); } 40 | .switch-narrow input:nth-child(3):checked ~ .switch-selection { left: ($switch-narrow-width * 1); } 41 | .switch-narrow input:nth-child(5):checked ~ .switch-selection { left: ($switch-narrow-width * 2); } 42 | .switch-narrow input:nth-child(7):checked ~ .switch-selection { left: ($switch-narrow-width * 3); } 43 | .switch-narrow input:nth-child(9):checked ~ .switch-selection { left: ($switch-narrow-width * 4); } 44 | .switch-narrow input:nth-child(11):checked ~ .switch-selection { left: ($switch-narrow-width * 5); } 45 | .switch-narrow input:nth-child(13):checked ~ .switch-selection { left: ($switch-narrow-width * 6); } 46 | .switch-narrow input:nth-child(15):checked ~ .switch-selection { left: ($switch-narrow-width * 7); } 47 | .switch-narrow input:nth-child(17):checked ~ .switch-selection { left: ($switch-narrow-width * 8); } 48 | .switch-narrow input:nth-child(19):checked ~ .switch-selection { left: ($switch-narrow-width * 9); } 49 | 50 | .switch-selection { 51 | position: absolute; 52 | display: block; 53 | z-index: $zindex-main + 1; 54 | left: 0px; 55 | width: $switch-width; 56 | height: $switch-height; 57 | background-color: darken($colour-medium, 30%); 58 | transition: left 0.15s ease-out; 59 | border-radius: 3px; 60 | } 61 | 62 | .switch-narrow .switch-selection { 63 | width: $switch-narrow-width; 64 | } 65 | -------------------------------------------------------------------------------- /views/game-list/GameList.scss: -------------------------------------------------------------------------------- 1 | div.game-list { 2 | display: block; 3 | max-height: 100%; 4 | overflow-x: hidden; 5 | overflow-y: auto; 6 | } 7 | 8 | div.game-list table { 9 | width: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /views/game-list/GameList.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class GameList extends Views.View { 4 | private _table: HTMLTableElement; 5 | public tableBody: Views.GameTableBody; 6 | 7 | constructor() { 8 | super(Templates.createDiv('game-list')); 9 | 10 | this._table = document.createElement('table'); 11 | this.root.appendChild(this._table); 12 | 13 | this.tableBody = new Views.GameTableBody(); 14 | this.tableBody.attach(this._table); 15 | } 16 | 17 | public activate(): void { 18 | this.tableBody.activate(); 19 | super.activate(); 20 | } 21 | 22 | public deactivate(): void { 23 | super.deactivate(); 24 | this.tableBody.deactivate(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /views/game-proposal/GameProposal.scss: -------------------------------------------------------------------------------- 1 | .game-proposal form { 2 | @extend .theme-dark; 3 | max-width: 600px; 4 | padding: 30px; 5 | background-color: rgba($colour-dark, 0.8); 6 | border-radius: 12px; 7 | } 8 | 9 | .game-proposal form h3 { 10 | display: inline-block; 11 | background-color: $colour-dark; 12 | margin-top: 1em; 13 | margin-left: -1.5em; 14 | padding: 0.4em 1.5em; 15 | width: 12em; 16 | font-size: smaller; 17 | } 18 | 19 | .game-proposal form .left-column { 20 | width: 50%; 21 | float: left; 22 | } 23 | .game-proposal form .right-column { 24 | width: 50%; 25 | float: right; 26 | } 27 | 28 | .game-proposal form button { 29 | float: right; 30 | clear: both; 31 | width: 10em; 32 | } 33 | 34 | .game-proposal form .form-group { 35 | overflow: hidden; 36 | margin: 0 1.6em 0 0; 37 | opacity: 1; 38 | transition: opacity 160ms ease-in-out, visibility 160ms ease-in-out; 39 | } 40 | .game-proposal form .form-group.hidden { 41 | opacity: 0; 42 | visibility: hidden; 43 | } 44 | 45 | .game-proposal form .switch + .form-group { 46 | margin-top: 1rem; 47 | } 48 | 49 | .game-proposal form .form-group label, .game-proposal form .form-group input { 50 | box-sizing: border-box; 51 | } 52 | 53 | .game-proposal form .form-group label { 54 | width: 40%; 55 | float: left; 56 | text-align: right; 57 | border: 1px solid transparent; 58 | padding: $text-box-padding-vertical 0.8em $text-box-padding-vertical 0; 59 | vertical-align: middle; 60 | color: $colour-light; 61 | } 62 | .game-proposal form .form-group input { 63 | width: 60%; 64 | float: right; 65 | } 66 | -------------------------------------------------------------------------------- /views/game-report/GameReport.html: -------------------------------------------------------------------------------- 1 |
2 | 13 |
14 | -------------------------------------------------------------------------------- /views/game-report/GameReport.scss: -------------------------------------------------------------------------------- 1 | .game-report { 2 | margin: 0.4rem 0 2rem 0; 3 | } 4 | .game-report.hidden, .game-report table.hidden { 5 | display: none; 6 | } 7 | 8 | .game-report h3 { 9 | text-align: center; 10 | background-color: $colour-lighter; 11 | padding: 0.6rem 0; 12 | } 13 | 14 | .game-report table { 15 | width: 100%; 16 | } 17 | .game-report th { 18 | text-align: right; 19 | font-weight: normal; 20 | font-size: 130%; 21 | } 22 | .game-report th, .game-report td { 23 | vertical-align: text-bottom; 24 | } 25 | .game-report td:nth-child(1), .game-report td:nth-child(4) { 26 | padding-right: 0.8rem; 27 | text-align: right; 28 | } 29 | .game-report td:nth-child(2), .game-report td:nth-child(5) { 30 | padding-right: 0.2rem; 31 | font-size: 140%; 32 | text-align: right; 33 | } 34 | .game-report th:last-child, .game-report td:last-child { 35 | padding-right: $view-sidebar-right-margin; 36 | } 37 | .game-report tbody tr:last-child { 38 | background-color: $colour-lighter; 39 | } 40 | -------------------------------------------------------------------------------- /views/game-report/GameReport.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class GameReport extends Views.View { 4 | private _headline: HTMLHeadingElement; 5 | private _whitePlayerName: HTMLTableHeaderCellElement; 6 | private _blackPlayerName: HTMLTableHeaderCellElement; 7 | private _table: HTMLTableElement; 8 | private _tableBody: Views.TableBody; 9 | 10 | constructor() { 11 | super(Views.Templates.cloneTemplate('game-report')); 12 | 13 | this._headline = this.root.querySelector('h3') as HTMLHeadingElement; 14 | 15 | let tableHeaderCells = this.root.querySelectorAll('th'); 16 | this._whitePlayerName = tableHeaderCells[0]; 17 | this._blackPlayerName = tableHeaderCells[1]; 18 | 19 | this._table = this.root.querySelector('table') as HTMLTableElement; 20 | this._tableBody = new Views.TableBody(); 21 | this._tableBody.attach(this._table); 22 | } 23 | 24 | public update(headline: string, whitePlayer: string, blackPlayer: string, komiSplit?: Models.KomiSplit, whiteScore?: Models.GamePositionScore, blackScore?: Models.GamePositionScore) { 25 | if (whitePlayer == null) whitePlayer = "white"; 26 | this._whitePlayerName.innerText = whitePlayer; 27 | 28 | if (blackPlayer == null) blackPlayer = "black"; 29 | this._blackPlayerName.innerText = blackPlayer; 30 | 31 | this._headline.innerText = headline || (whitePlayer + " vs. " + blackPlayer); 32 | 33 | let tableRows: string[][] = []; 34 | 35 | let whiteBase: number = 0; 36 | let whiteHalf: string = ""; 37 | let blackBase: number = 0; 38 | 39 | if (komiSplit) { 40 | whiteBase = komiSplit.white.base; 41 | whiteHalf = (komiSplit.white.half)? '½' : ''; 42 | 43 | let komiRow = [ "komi", komiSplit.white.base.toString(), whiteHalf, "", "" ]; 44 | if ((komiSplit.black) && (komiSplit.black.base > 0)) { 45 | blackBase = komiSplit.black.base; 46 | 47 | komiRow[3] = "reverse komi"; 48 | komiRow[4] = komiSplit.black.base.toString(); 49 | } 50 | 51 | tableRows.push(komiRow); 52 | } 53 | 54 | if ((whiteScore) || (blackScore)) { 55 | tableRows.push([ "prisoners", ((whiteScore)? whiteScore.prisoners.toString() : ""), "", "", ((blackScore)? blackScore.prisoners.toString() : "") ]); 56 | tableRows.push([ "captures", ((whiteScore)? whiteScore.captures.toString() : ""), "", "", ((blackScore)? blackScore.captures.toString() : "") ]); 57 | tableRows.push([ "territory", ((whiteScore)? whiteScore.territory.toString() : ""), "", "", ((blackScore)? blackScore.territory.toString() : "") ]); 58 | 59 | if (whiteScore) { 60 | whiteBase += whiteScore.prisoners + whiteScore.captures + whiteScore.territory; 61 | } 62 | 63 | if (blackScore) { 64 | blackBase += blackScore.prisoners + blackScore.captures + blackScore.territory; 65 | } 66 | } 67 | 68 | if (tableRows.length > 0) { 69 | tableRows.push([ "total", whiteBase.toString(), whiteHalf, "", blackBase.toString() ]); 70 | 71 | this._tableBody.update(tableRows); 72 | this._table.classList.remove('hidden'); 73 | } 74 | else this._table.classList.add('hidden'); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /views/game-table-body/GameTableBody.scss: -------------------------------------------------------------------------------- 1 | div.game-list tbody td { 2 | @extend .control-clickable; 3 | padding: 4px; 4 | } 5 | 6 | div.game-list tbody tr:nth-child(odd) { 7 | background-color: lighten($colour-dark, 5%); 8 | } 9 | 10 | div.game-list tbody tr:hover { 11 | background-color: lighten($colour-dark, 10%); 12 | } 13 | 14 | div.game-list tbody td span { 15 | margin-right: 0.5rem; 16 | } 17 | 18 | div.game-list tbody td span:before { 19 | margin-right: 0.4rem; 20 | } 21 | 22 | div.game-list tbody td:nth-child(5) { 23 | text-align: right; 24 | } 25 | -------------------------------------------------------------------------------- /views/game-table-body/GameTableBody.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class GameTableBody extends Views.DataBoundList { 4 | public userDataSource: (name: string) => Models.User; 5 | public selectionCallback: (channelId: number) => void; 6 | 7 | constructor() { 8 | super(document.createElement('tbody')); 9 | } 10 | 11 | public update(gameChannels: { [key: string]: Models.GameChannel }, keys: number[]) { 12 | this.bindDictionary(gameChannels, keys); 13 | } 14 | 15 | protected createChild(key: string, datum: Models.GameChannel): HTMLTableRowElement { 16 | let element = document.createElement('tr'); 17 | element.onclick = () => { if (this.selectionCallback) this.selectionCallback(datum.channelId); }; 18 | 19 | let gameTypeDomain = document.createElement('td'); 20 | gameTypeDomain.appendChild(document.createElement('span')); 21 | let gameSubTypeSpan = document.createElement('span'); 22 | gameSubTypeSpan.className = 'secondary'; 23 | gameTypeDomain.appendChild(gameSubTypeSpan); 24 | element.appendChild(gameTypeDomain); // [0] Game Type & Sub-Type 25 | 26 | let strongerPlayerDomain = document.createElement('td'); 27 | strongerPlayerDomain.appendChild(document.createElement('span')); 28 | let strongerPlayerRankSpan = document.createElement('span'); 29 | strongerPlayerRankSpan.className = 'secondary'; 30 | strongerPlayerDomain.appendChild(strongerPlayerRankSpan); 31 | element.appendChild(strongerPlayerDomain); // [1] Stronger Player 32 | 33 | let weakerPlayerDomain = document.createElement('td'); 34 | weakerPlayerDomain.appendChild(document.createElement('span')); 35 | let weakerPlayerRankSpan = document.createElement('span'); 36 | weakerPlayerRankSpan.className = 'secondary'; 37 | weakerPlayerDomain.appendChild(weakerPlayerRankSpan); 38 | element.appendChild(weakerPlayerDomain); // [2] Weaker Player 39 | 40 | element.appendChild(document.createElement('td')); // [3] Board Size 41 | 42 | let gamePhaseDomain = document.createElement('td'); 43 | let gamePhaseSpan = document.createElement('span'); 44 | gamePhaseSpan.className = 'secondary'; 45 | gamePhaseDomain.appendChild(gamePhaseSpan); 46 | gamePhaseDomain.appendChild(document.createElement('span')); 47 | element.appendChild(gamePhaseDomain); // [4] Move Number and Game Phase 48 | 49 | element.appendChild(document.createElement('td')); // [5] Comments et al. 50 | 51 | this.updateChild(key, datum, element); 52 | return element; 53 | } 54 | 55 | protected updateChild(key: string, datum: Models.GameChannel, element: HTMLTableRowElement): void { 56 | let domains = element.getElementsByTagName('td'); 57 | let typeSpans = domains[0].getElementsByTagName('span'); 58 | typeSpans[0].innerText = datum.displayType; 59 | typeSpans[1].innerText = datum.displaySubType; 60 | 61 | if (datum.restrictedPlus) typeSpans[1].className = 'secondary fa-plus'; 62 | else if (datum.restrictedPrivate) typeSpans[1].className = 'secondary fa-lock'; 63 | else typeSpans[1].className = 'secondary'; 64 | 65 | let playersSwapped: boolean = false; 66 | let whitePlayer: Models.User = (datum.playerWhite != null)? this.userDataSource(datum.playerWhite) : null; 67 | let whitePlayerSpans = domains[1].getElementsByTagName('span'); 68 | if (whitePlayer) { 69 | whitePlayerSpans[0].className = (!playersSwapped)? 'fa-circle' : 'fa-circle-o'; 70 | whitePlayerSpans[0].innerText = whitePlayer.name; 71 | whitePlayerSpans[1].innerText = whitePlayer.rank; 72 | } 73 | else { 74 | whitePlayerSpans[0].className = ''; 75 | whitePlayerSpans[0].innerText = ''; 76 | whitePlayerSpans[1].innerText = ''; 77 | } 78 | 79 | let blackPlayer: Models.User = (datum.playerBlack != null)? this.userDataSource(datum.playerBlack) : null; 80 | let blackPlayerSpans = domains[2].getElementsByTagName('span'); 81 | if (blackPlayer) { 82 | blackPlayerSpans[0].className = (!playersSwapped)? 'fa-circle-o' : 'fa-circle'; 83 | blackPlayerSpans[0].innerText = blackPlayer.name; 84 | blackPlayerSpans[1].innerText = blackPlayer.rank; 85 | } 86 | else { 87 | blackPlayerSpans[0].className = ''; 88 | blackPlayerSpans[0].innerText = ''; 89 | blackPlayerSpans[1].innerText = ''; 90 | } 91 | 92 | domains[3].innerText = datum.displaySize; 93 | 94 | let gamePhaseSpans = domains[4].getElementsByTagName('span'); 95 | if ((datum.phase == Models.GamePhase.Active) && (datum.moveNumber != null)) { 96 | gamePhaseSpans[0].innerText = "move"; 97 | gamePhaseSpans[1].innerText = datum.moveNumber.toString(); 98 | } 99 | else if (datum.phase == Models.GamePhase.Paused) { 100 | gamePhaseSpans[0].innerText = "paused"; 101 | gamePhaseSpans[1].innerText = (datum.moveNumber != null)? datum.moveNumber.toString() : ""; 102 | } 103 | else if (datum.phase == Models.GamePhase.Adjourned) { 104 | gamePhaseSpans[0].innerText = "adjourned"; 105 | gamePhaseSpans[1].innerText = (datum.moveNumber != null)? datum.moveNumber.toString() : ""; 106 | } 107 | else if (datum.phase == Models.GamePhase.Concluded) { 108 | gamePhaseSpans[0].innerText = ""; 109 | gamePhaseSpans[1].innerText = (datum.result != null)? datum.result.getShortFormat() : ""; 110 | } 111 | else { 112 | gamePhaseSpans[0].innerText = ""; 113 | gamePhaseSpans[1].innerText = ""; 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /views/go-board-player/GoBoardPlayer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
 
7 |
 
8 | 9 |
10 |
11 |
12 |
13 |
14 | prisoners 15 | 0 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /views/go-board-player/GoBoardPlayer.scss: -------------------------------------------------------------------------------- 1 | $view-go-board-player-padding: 10px; 2 | 3 | .go-board-player { 4 | min-width: $view-go-clock-width + (2 * $view-go-board-player-padding); 5 | min-height: $view-go-clock-height + (2 * $view-go-board-player-padding); 6 | } 7 | 8 | .go-board .go-board-player .player-info > .hidden, 9 | .go-board .go-board-player .player-stats > .hidden { 10 | display: none; 11 | } 12 | 13 | .go-board .go-board-player .player-info, 14 | .go-board .go-board-player .player-stats > * { 15 | margin: $view-go-board-player-padding; 16 | } 17 | 18 | 19 | .go-board.landscape .team-away .go-board-player .player-stats > * { 20 | float: right; 21 | clear: right; 22 | } 23 | .go-board.landscape .go-board-player .player-info { 24 | clear: both; 25 | } 26 | .go-board.landscape .team-home .go-board-player .player-stats .score-info { 27 | margin-top: 22px; 28 | margin-bottom: 26px; 29 | } 30 | .go-board.landscape .team-away .go-board-player .player-stats .score-info { 31 | margin-top: 10px; 32 | margin-bottom: 26px; 33 | } 34 | 35 | 36 | .go-board.portrait .team-home .go-board-player .player-stats, 37 | .go-board.portrait .team-away .go-board-player .player-info { 38 | float: right; 39 | } 40 | .go-board.portrait .team-away .go-board-player .player-stats, 41 | .go-board.portrait .team-home .go-board-player .player-info { 42 | float: left; 43 | } 44 | .go-board.portrait .go-board-player .player-stats > * { 45 | float: left; 46 | } 47 | .go-board.portrait .go-board-player .player-stats > *:not(.hidden) ~ * { 48 | margin-left: 18px; 49 | } 50 | 51 | 52 | .go-board-player .score-info { 53 | @extend .table; 54 | } 55 | .go-board-player .score-info * { 56 | @extend .control; 57 | } 58 | .go-board-player .score-info > div > span { 59 | text-align: right; 60 | } 61 | .go-board-player .score-info > div > span:nth-child(1) { 62 | padding-right: 0.8rem; 63 | } 64 | .go-board-player .score-info > div > span:nth-child(2) { 65 | padding-right: 0.2rem; 66 | font-size: 160%; 67 | } 68 | 69 | 70 | .go-board-player .player-info { 71 | display: block; 72 | overflow: hidden; 73 | } 74 | .go-board-player .player-info * { 75 | @extend .control; 76 | } 77 | .go-board .team-home .go-board-player .player-info > * { 78 | float: left; 79 | } 80 | .go-board .team-home .go-board-player .player-info > * + * { 81 | margin-left: 0.6rem; 82 | } 83 | .go-board .team-away .go-board-player .player-info > * { 84 | float: right; 85 | } 86 | .go-board .team-away .go-board-player .player-info > * + * { 87 | margin-right: 0.6rem; 88 | } 89 | .go-board .team-away .go-board-player .player-info * { 90 | text-align: right; 91 | } 92 | 93 | .go-board-player .player-info img.stone { 94 | height: 30px; 95 | width: 30px; 96 | margin-top: 7px; 97 | } 98 | .go-board-player .player-info img.avatar { 99 | height: $view-go-clock-height; 100 | } 101 | 102 | .go-board-player .player-buttons { 103 | display: block; 104 | overflow: hidden; 105 | } 106 | .go-board-player .player-buttons.hidden { 107 | display: none; 108 | } 109 | .go-board-player .player-buttons .safety-button .button:first-child { 110 | width: 5em; 111 | } 112 | .go-board-player .player-buttons .safety-button + .safety-button { 113 | margin-top: 3px; 114 | } 115 | .go-board-player .player-buttons .safety-button .primary { 116 | width: 70px; 117 | } 118 | -------------------------------------------------------------------------------- /views/go-board-player/GoBoardPlayer.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class GoBoardPlayer extends Views.View { 4 | private _clock: Views.GoClock; 5 | 6 | private _userAvatar: HTMLImageElement; 7 | private _userName: HTMLDivElement; 8 | private _userRank: HTMLDivElement; 9 | private _userStone: HTMLImageElement; 10 | 11 | private _captureCount: HTMLSpanElement; 12 | 13 | private _komiCaption: HTMLSpanElement; 14 | private _komiBase: HTMLSpanElement; 15 | private _komiHalf: HTMLSpanElement; 16 | 17 | private _buttonsDiv: HTMLDivElement; 18 | private _buttons: Views.SafetyButton[]; 19 | 20 | constructor(playerTeam: Models.PlayerTeam) { 21 | super(Views.Templates.cloneTemplate('go-board-player')); 22 | 23 | let playerInfo = this.root.querySelector('.player-info') as HTMLDivElement; 24 | let playerStats = this.root.querySelector('.player-stats') as HTMLDivElement; 25 | 26 | this._clock = new Views.GoClock(); 27 | 28 | if (playerTeam == Models.PlayerTeam.Home) { 29 | this._clock.attach(playerStats, false); 30 | 31 | this._buttonsDiv = Templates.createDiv('player-buttons hidden'); 32 | playerStats.insertBefore(this._buttonsDiv, playerStats.firstElementChild); 33 | this._buttons = new Array(2); 34 | for (let u = 0; u < this._buttons.length; ++u) { 35 | this._buttons[u] = new Views.SafetyButton(); 36 | this._buttons[u].attach(this._buttonsDiv); 37 | } 38 | } 39 | else { 40 | this.root.insertBefore(playerStats, playerInfo); 41 | this._clock.attach(playerStats, true); 42 | } 43 | 44 | this._userAvatar = playerInfo.querySelector('img.avatar') as HTMLImageElement; 45 | this._userName = playerInfo.querySelector('div.name') as HTMLDivElement; 46 | this._userRank = playerInfo.querySelector('div.secondary') as HTMLDivElement; 47 | this._userStone = playerInfo.querySelector('img.stone') as HTMLImageElement; 48 | 49 | this._captureCount = playerStats.querySelector('.capture-count') as HTMLSpanElement; 50 | 51 | this._komiCaption = playerStats.querySelector('.komi-caption') as HTMLSpanElement; 52 | this._komiBase = playerStats.querySelector('.komi-base') as HTMLSpanElement; 53 | this._komiHalf = playerStats.querySelector('.komi-half') as HTMLSpanElement; 54 | } 55 | 56 | public activate(): void { 57 | this._clock.activate(); 58 | super.activate(); 59 | } 60 | 61 | public deactivate(): void { 62 | super.deactivate(); 63 | this._clock.deactivate(); 64 | } 65 | 66 | public update(colour: Models.GameStone, clock: Models.GameClock, prisoners: number, komi: { base: number, half?: boolean }, user: Models.User, buttons?: { text: string, callback: Function, dangerous?: boolean}[]) { 67 | // Player Colour 68 | if (colour == Models.GameStone.White) { 69 | this._userStone.src = 'img/stone-white.png'; 70 | this._userStone.title = 'white'; 71 | } 72 | else { 73 | this._userStone.src = 'img/stone-black.png'; 74 | this._userStone.title = 'black'; 75 | } 76 | 77 | // Clock 78 | this._clock.update(clock); 79 | 80 | // Prisoners 81 | this._captureCount.innerText = (prisoners != null)? prisoners.toString() : "0"; 82 | 83 | // Komi 84 | if ((!komi) || ((!komi.base) && (!komi.half))) { 85 | this._komiCaption.innerText = ""; 86 | this._komiBase.innerText = ""; 87 | this._komiHalf.innerText = ""; 88 | } 89 | else { 90 | this._komiCaption.innerText = (colour == Models.GameStone.White)? "komi" : "reverse komi"; 91 | this._komiBase.innerText = (komi.base)? komi.base.toString() : "0"; 92 | this._komiHalf.innerText = (komi.half)? "½" : ""; 93 | } 94 | 95 | // User Info. 96 | if (user) { 97 | if ((user.flags & Models.UserFlags.HasAvatar) == Models.UserFlags.HasAvatar) { 98 | this._userAvatar.src = KGS.Constants.AvatarURIPrefix + user.name + KGS.Constants.AvatarURISuffix; 99 | this._userAvatar.title = user.name; 100 | } 101 | else { 102 | this._userAvatar.src = 'img/avatar-default.png'; 103 | this._userAvatar.title = ""; 104 | } 105 | 106 | this._userName.innerText = user.name; 107 | this._userRank.innerText = user.rank; 108 | } 109 | else { 110 | this._userAvatar.src = 'img/avatar-default.png'; 111 | this._userAvatar.title = ""; 112 | this._userName.innerHTML = " "; 113 | this._userRank.innerHTML = " "; 114 | } 115 | 116 | // Player Buttons 117 | if (this._buttonsDiv) { 118 | if (buttons) { 119 | this._buttonsDiv.classList.remove('hidden'); 120 | 121 | for (let u = 0; u < this._buttons.length; ++u) { 122 | if (u < buttons.length) { 123 | this._buttons[u].text = buttons[u].text; 124 | this._buttons[u].dangerous = (buttons[u].dangerous)? true : false; 125 | this._buttons[u].callback = buttons[u].callback; 126 | this._buttons[u].disabled = (buttons[u].callback == null); 127 | this._buttons[u].show(); 128 | } 129 | else { 130 | this._buttons[u].callback = null; 131 | this._buttons[u].hide(); 132 | } 133 | } 134 | } 135 | else this._buttonsDiv.classList.add('hidden'); 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /views/go-board/GoBoard.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /views/go-board/GoBoard.scss: -------------------------------------------------------------------------------- 1 | .go-board .flex-absolute { 2 | flex-direction: row; 3 | margin: auto auto; 4 | z-index: $zindex-main + 2; 5 | } 6 | .go-board.portrait .flex-absolute { 7 | flex-direction: column; 8 | } 9 | 10 | .go-board .flex-absolute > div { 11 | position: relative; 12 | min-width: 0; 13 | min-height: 0; 14 | overflow: hidden; 15 | flex: 1 0 auto; 16 | } 17 | 18 | .go-board .goban { 19 | @extend .control; 20 | } 21 | 22 | @for $i from $go-board-size-min through $go-board-size-max { 23 | .go-board.go-#{$i} .flex-absolute > div.goban { 24 | flex: 0 1 ($i * $go-stone-diameter); 25 | max-width: ($i * $go-stone-diameter); 26 | max-height: ($i * $go-stone-diameter); 27 | } 28 | } 29 | 30 | .go-board .team-home > div, .go-board .team-away > div { 31 | @extend .block-absolute; 32 | overflow: hidden; 33 | } 34 | .go-board.landscape .team-away > div { 35 | left: auto; 36 | bottom: auto; 37 | } 38 | .go-board.landscape .team-home > div { 39 | top: auto; 40 | right: auto; 41 | } 42 | .go-board.portrait .team-away > div { 43 | top: auto; 44 | } 45 | .go-board.portrait .team-home > div { 46 | bottom: auto; 47 | } 48 | -------------------------------------------------------------------------------- /views/go-clock/GoClock.scss: -------------------------------------------------------------------------------- 1 | .go-clock { 2 | display: block; 3 | position: relative; 4 | overflow: hidden; 5 | width: $view-go-clock-width; 6 | } 7 | 8 | .go-clock * { 9 | @extend .control; 10 | } 11 | 12 | .go-clock > .lcd-clock { 13 | float: left; 14 | } 15 | .go-clock > .lcd-display { 16 | float: right; 17 | } 18 | .go-clock > .lcd-counter { 19 | float: right; 20 | margin-right: 7px; 21 | } 22 | 23 | .go-clock.expired .lcd-clock .lcd-clock-separator svg, .go-clock.expired svg .segment-on { 24 | fill: $colour-negative; 25 | } 26 | -------------------------------------------------------------------------------- /views/go-clock/GoClock.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class GoClock extends Views.View { 4 | private _clock: Views.LCDClock; 5 | private _overtimeNumber: Views.LCDDisplay; 6 | private _overtimeCounter: Views.LCDCounter; 7 | 8 | private _data: Models.GameClock; 9 | private _rules: Models.GameRules; 10 | private _expired: boolean; 11 | private _hidden: boolean; 12 | 13 | private _resolution: number = 500; 14 | private _timeoutHandler: Function; 15 | private _timeoutHandle: number; 16 | 17 | constructor() { 18 | super(Templates.createDiv('go-clock')); 19 | 20 | this._clock = new Views.LCDClock(); 21 | this._clock.attach(this.root); 22 | 23 | this._overtimeNumber = new Views.LCDDisplay(2, 0, false, true); 24 | this._overtimeNumber.attach(this.root); 25 | 26 | this._overtimeCounter = new Views.LCDCounter(25); 27 | this._overtimeCounter.attach(this.root); 28 | } 29 | 30 | public activate(): void { 31 | this._clock.activate(); 32 | this._overtimeNumber.activate(); 33 | this._overtimeCounter.activate(); 34 | 35 | if (this._timeoutHandle == null) { 36 | this.restoreTimeout(); 37 | } 38 | 39 | super.activate(); 40 | } 41 | public deactivate(): void { 42 | super.deactivate(); 43 | 44 | this.clearTimeout(); 45 | 46 | this._overtimeCounter.deactivate(); 47 | this._overtimeNumber.deactivate(); 48 | this._clock.deactivate(); 49 | } 50 | 51 | private restoreTimeout() { 52 | if (this._timeoutHandler != null) { 53 | this._timeoutHandle = window.setTimeout(this._timeoutHandler, this._resolution); 54 | } 55 | } 56 | 57 | private clearTimeout() { 58 | if (this._timeoutHandle != null) { 59 | window.clearTimeout(this._timeoutHandle); 60 | this._timeoutHandle = null; 61 | } 62 | } 63 | 64 | public update(data: Models.GameClock) { 65 | if ((data) && (data.rules) && (data.rules.timeSystem != Models.TimeSystem.None)) { 66 | // Register a Timeout of the clock is running 67 | let clockState = data.now(); 68 | if (clockState.running) { 69 | if (this._data != data) { 70 | this._data = data; 71 | this._timeoutHandler = this.update.bind(this, data); 72 | } 73 | 74 | this.restoreTimeout(); 75 | } 76 | 77 | if (this._rules != data.rules) { 78 | // Show or hide the Overtime Counters 79 | if ((data.rules.byoYomiMaximum) && (data.rules.byoYomiMaximum <= 30)) { 80 | this._overtimeCounter.setMaximum(data.rules.byoYomiMaximum, (data.rules.byoYomiMaximum <= 15)? 1 : 2); 81 | this._overtimeCounter.show(); 82 | } 83 | else { 84 | this._overtimeCounter.hide(); 85 | } 86 | 87 | if (data.rules.byoYomiMaximum) { 88 | this._overtimeNumber.show(); 89 | } 90 | else { 91 | this._overtimeNumber.hide(); 92 | } 93 | } 94 | 95 | if (!clockState.expired) { 96 | let overtimeValue: number = (clockState.overtime)? (clockState.periods || clockState.stones) : null; 97 | this._overtimeCounter.value = overtimeValue; 98 | this._overtimeNumber.value = overtimeValue; 99 | 100 | this._clock.update(clockState.time, clockState.running); 101 | 102 | if (this._expired) { 103 | this.root.classList.remove('expired'); 104 | this._expired = false; 105 | } 106 | } 107 | else { 108 | let overtimeValue: number = (clockState.stones)? clockState.stones : 0; 109 | this._overtimeCounter.value = overtimeValue; 110 | this._overtimeNumber.value = overtimeValue; 111 | this._clock.update(0, false); 112 | if (!this._expired) { 113 | this.root.classList.add('expired'); 114 | this._expired = true; 115 | } 116 | } 117 | 118 | // Show the clock if it was previously hidden 119 | if (this._hidden) { 120 | this.root.classList.remove('hidden'); 121 | this._hidden = false; 122 | } 123 | } 124 | else { 125 | // Clear Timeout and Hide the clock 126 | this.clearTimeout(); 127 | if (!this._hidden) this.root.classList.add('hidden'); 128 | this._data = null; 129 | this._hidden = true; 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /views/home-main/HomeMain.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /views/home-main/HomeMain.scss: -------------------------------------------------------------------------------- 1 | .home-main .user-info { 2 | position: relative; 3 | display: flex; 4 | flex-direction: row; 5 | padding: 4rem; 6 | } 7 | 8 | .home-main .user-info div:first-child { 9 | width: 170px; 10 | } 11 | .home-main .avatar { 12 | width: 141px; 13 | height: 200px; 14 | margin-bottom: 0.5rem; 15 | } 16 | 17 | .home-main .user-info p.name { 18 | font-size: 120%; 19 | } 20 | 21 | .home-main .user-info div:last-child { 22 | flex: 1 1 auto; 23 | padding: 0 2rem; 24 | } 25 | 26 | .home-main .user-info .rank-graph { 27 | height: 200px; 28 | width: 100%; 29 | } 30 | -------------------------------------------------------------------------------- /views/home-main/HomeMain.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class HomeMain extends Views.View { 4 | private _avatar: HTMLImageElement; 5 | private _username: HTMLSpanElement; 6 | private _rank: HTMLSpanElement; 7 | 8 | constructor() { 9 | super(Views.Templates.cloneTemplate('home-main')); 10 | 11 | this._avatar = this.root.querySelector('img.avatar') as HTMLImageElement; 12 | 13 | let nameSpans = this.root.querySelectorAll('p.name span'); 14 | this._username = nameSpans[0] as HTMLSpanElement; 15 | this._rank = nameSpans[1] as HTMLSpanElement; 16 | } 17 | 18 | public update(user: Models.User) { 19 | if (user) { 20 | if ((user.flags & Models.UserFlags.HasAvatar) == Models.UserFlags.HasAvatar) { 21 | this._avatar.src = KGS.Constants.AvatarURIPrefix + user.name + KGS.Constants.AvatarURISuffix; 22 | this._avatar.title = user.name; 23 | } 24 | else { 25 | this._avatar.src = 'img/avatar-default.png'; 26 | this._avatar.title = ""; 27 | } 28 | 29 | this._username.innerText = user.name; 30 | this._rank.innerText = user.rank; 31 | } 32 | else { 33 | this._avatar.src = 'img/avatar-default.png'; 34 | this._avatar.title = ""; 35 | this._username.innerText = ""; 36 | this._rank.innerText = ""; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /views/home-sidebar/HomeSidebar.scss: -------------------------------------------------------------------------------- 1 | .home-sidebar h3 { 2 | display: block; 3 | background-color: $colour-lighter; 4 | margin-top: 1em; 5 | margin-right: $view-sidebar-right-margin; 6 | padding: 0.5em 30px; 7 | font-size: smaller; 8 | } 9 | 10 | .home-sidebar #button-sign-out { 11 | @extend .block-absolute; 12 | top: auto; 13 | left: auto; 14 | right: 0; 15 | bottom: 0; 16 | font-size: 80%; 17 | background-color: lighten($colour-negative, 20%); 18 | } 19 | -------------------------------------------------------------------------------- /views/home-sidebar/HomeSidebar.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class HomeSidebar extends Views.View { 4 | public automatchForm: Views.AutomatchForm; 5 | 6 | constructor() { 7 | super(Views.Templates.createDiv('home-sidebar')); 8 | 9 | this.automatchForm = new Views.AutomatchForm(); 10 | this.automatchForm.attach(this.root); 11 | } 12 | 13 | public activate(): void { 14 | this.automatchForm.activate(); 15 | super.activate(); 16 | } 17 | 18 | public deactivate(): void { 19 | super.deactivate(); 20 | this.automatchForm.deactivate(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /views/lcd-clock/LCDClock.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /views/lcd-clock/LCDClock.scss: -------------------------------------------------------------------------------- 1 | .lcd-clock, .lcd-clock .lcd-clock-separator { 2 | display: inline-block; 3 | } 4 | .lcd-clock .lcd-clock-separator { 5 | position: relative; 6 | overflow: hidden; 7 | } 8 | 9 | .lcd-clock .lcd-clock-separator svg { 10 | height: 48px; 11 | width: 11px; 12 | stroke-width: 0; 13 | fill: $view-go-clock-segment-on; 14 | } 15 | 16 | .lcd-clock.running .lcd-clock-separator svg { 17 | fill: $view-go-clock-segment-off; 18 | animation: lcd-clock-separator-blink 1s steps(1, start) infinite; 19 | } 20 | @keyframes lcd-clock-separator-blink { 21 | 0% { fill: $view-go-clock-segment-off; } 22 | 50% { fill: $view-go-clock-segment-on; } 23 | } 24 | -------------------------------------------------------------------------------- /views/lcd-clock/LCDClock.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class LCDClock extends Views.View { 4 | private _minutes: Views.LCDDisplay; 5 | private _seconds: Views.LCDDisplay; 6 | 7 | private _value: number; 8 | private _running: boolean; 9 | 10 | constructor() { 11 | super(Templates.createDiv('lcd-clock')); 12 | 13 | this._minutes = new Views.LCDDisplay(2, 0, false, true); 14 | this._minutes.attach(this.root); 15 | 16 | this.root.appendChild(Views.Templates.cloneTemplate('lcd-clock')); 17 | 18 | this._seconds = new Views.LCDDisplay(2, 0, false, true, true); 19 | this._seconds.attach(this.root); 20 | } 21 | 22 | public activate(): void { 23 | this._minutes.activate(); 24 | this._seconds.activate(); 25 | super.activate(); 26 | } 27 | 28 | public deactivate(): void { 29 | super.deactivate(); 30 | this._seconds.deactivate(); 31 | this._minutes.deactivate(); 32 | } 33 | 34 | public update(seconds: number, running?: boolean): void { 35 | seconds = (seconds == null)? null : (seconds >= 0)? seconds : 0; 36 | if (this._value != seconds) { 37 | if (seconds != null) { 38 | let minutes = ~~(seconds / 60); 39 | seconds -= minutes * 60; 40 | 41 | this._minutes.value = minutes; 42 | this._seconds.value = seconds; 43 | } 44 | else { 45 | this._minutes.value = null; 46 | this._seconds.value = null; 47 | } 48 | 49 | this._value = seconds; 50 | } 51 | 52 | running = (running)? true : false; 53 | if (this._running != running) { 54 | if (running) this.root.classList.add('running'); 55 | else this.root.classList.remove('running'); 56 | 57 | this._running = running; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /views/lcd-counter/LCDCounter.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /views/lcd-counter/LCDCounter.scss: -------------------------------------------------------------------------------- 1 | .lcd-counter div { 2 | display: block; 3 | position: relative; 4 | overflow: hidden; 5 | float: right; 6 | } 7 | 8 | .lcd-counter div + div { 9 | margin-right: 10px; 10 | } 11 | 12 | .lcd-counter svg { 13 | display: block; 14 | height: 12px; 15 | width: 65px; 16 | stroke-width: 0; 17 | fill: $view-go-clock-segment-off; 18 | } 19 | .lcd-counter svg + svg { 20 | margin-top: 1px; 21 | } 22 | 23 | .lcd-counter svg .segment-on { 24 | fill: $view-go-clock-segment-on; 25 | } 26 | -------------------------------------------------------------------------------- /views/lcd-counter/LCDCounter.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class LCDCounter extends Views.View { 4 | private _dots: number = 5; 5 | private _maximum: number; 6 | private _rows: number; 7 | 8 | private _value: number; 9 | 10 | constructor(maximum?: number, rows?: number) { 11 | super(Templates.createDiv('lcd-counter')); 12 | 13 | this.setMaximum(maximum || 10, rows || 2, true); 14 | this._value = 0; 15 | } 16 | 17 | public activate(): void { 18 | this.updateVectors(); 19 | super.activate(); 20 | } 21 | 22 | public setMaximum(maximum: number, rows?: number, suppressUpdate?: boolean) { 23 | rows = (rows != null)? rows : this._rows; 24 | if ((this._maximum != maximum) || (this._rows != rows)) { 25 | $(this.root).empty(); 26 | 27 | this._maximum = maximum; 28 | this._rows = rows; 29 | 30 | let rowModulus = rows * this._dots; 31 | let group: HTMLDivElement; 32 | for (let j = 0; j < this._maximum; j += this._dots) { 33 | if ((j % rowModulus) == 0) { 34 | group = document.createElement('div'); 35 | this.root.appendChild(group); 36 | } 37 | 38 | group.appendChild(Views.Templates.cloneTemplate('lcd-counter')); 39 | } 40 | 41 | if (!suppressUpdate) this.updateVectors(); 42 | } 43 | } 44 | 45 | private updateVectors() { 46 | let value: number = (this._value != null)? this._value : 0; 47 | let childCount = this.root.childElementCount; 48 | let offset = 0; 49 | for (let j = 0; j < childCount; ++j) { 50 | let div = this.root.children[j] as HTMLDivElement; 51 | let grandchildCount = div.childElementCount; 52 | for (let i = 0; i < grandchildCount; ++i) { 53 | let svg = div.children[i] as SVGElement; 54 | this.updateVector(svg, value, offset); 55 | 56 | offset += this._dots; 57 | } 58 | } 59 | } 60 | 61 | private updateVector(svg: SVGElement, value: number, offset: number) { 62 | for (let k = 0; k < svg.childNodes.length; ++k) { 63 | let child = svg.childNodes[k] as Element; 64 | if ((child.nodeType == 1) && (child.nodeName == 'g')) { 65 | let x: number = offset + 1; 66 | for (let i = 0; i < child.childNodes.length; ++i) { 67 | let segment = child.childNodes[i] as Element; 68 | if (segment.nodeType == 1) { 69 | segment.setAttribute('class', (x > this._maximum)? 'hidden' : (x <= this._value)? 'segment-on' : ''); 70 | ++x; 71 | } 72 | } 73 | 74 | return; 75 | } 76 | } 77 | } 78 | 79 | public get value() { 80 | return this._value; 81 | } 82 | public set value(value: number) { 83 | this._value = value; 84 | this.updateVectors(); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /views/lcd-display/LCDDisplay.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | -------------------------------------------------------------------------------- /views/lcd-display/LCDDisplay.scss: -------------------------------------------------------------------------------- 1 | .lcd-display, .lcd-display svg { 2 | display: inline-block; 3 | position: relative; 4 | overflow: hidden; 5 | fill: $view-go-clock-segment-off; 6 | } 7 | 8 | .lcd-display svg { 9 | height: 48px; 10 | width: 35px; 11 | stroke-width: 0; 12 | } 13 | 14 | .lcd-display svg .segment-on { 15 | fill: $view-go-clock-segment-on; 16 | } 17 | -------------------------------------------------------------------------------- /views/lcd-display/LCDDisplay.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class LCDDisplay extends Views.View { 4 | private _precision: number; 5 | private _scale: number; 6 | private _signed: boolean; 7 | private _integer: boolean; 8 | private _pad: boolean; 9 | 10 | private _value: number; 11 | 12 | // Precision is the number of digits in a number. 13 | // Scale is the number of digits to the right of the decimal point in a number. 14 | // For example, the number 123.45 has a precision of 5 and a scale of 2. 15 | constructor(precision?: number, scale?: number, signed?: boolean, integer?: boolean, pad?: boolean) { 16 | super(Templates.createDiv('lcd-display')); 17 | 18 | this._precision = precision || 8; 19 | this._scale = (scale != null)? scale : 0; 20 | this._signed = signed; 21 | this._integer = integer; 22 | this._pad = pad; 23 | 24 | let childCount = (this._signed)? this._precision + 1 : this._precision; 25 | for (let j = 0; j < childCount; ++j) { 26 | this.root.appendChild(Views.Templates.cloneTemplate('lcd-display')); 27 | } 28 | 29 | this._value = 0; 30 | } 31 | 32 | public activate(): void { 33 | this.updateVectors(); 34 | super.activate(); 35 | } 36 | 37 | private _bitmaps: { [d: number]: number, [c: string]: number } = { 38 | 0: 0x3f, 1: 0x06, 2: 0x5b, 3: 0x4f, 4: 0x66, 39 | 5: 0x6d, 6: 0x7d, 7: 0x07, 8: 0x7f, 9: 0x6f, 40 | "-": 0x40, 41 | ".": 0x80 42 | }; 43 | 44 | private updateVectors() { 45 | if (this._signed) { 46 | this.updateVector(this.root.children[0] as SVGElement, ~(this._bitmaps["-"]), (this._value < 0)? this._bitmaps["-"] : 0); 47 | } 48 | 49 | let characters = (this._value != null)? Math.abs(this._value).toString() : ""; 50 | let decimalIndex = characters.indexOf('.'); 51 | if (decimalIndex < 0) decimalIndex = characters.length; 52 | 53 | let childCount = this.root.childElementCount; 54 | let order = this._precision - this._scale - 1; 55 | for (let j = (!this._signed)? 0 : 1; j < childCount; ++j) { 56 | let digitIndex = (order >= 0)? decimalIndex - 1 - order : decimalIndex - order; 57 | 58 | let maskHidden: number = 0; 59 | let maskOn: number = ((digitIndex >= characters.length) || ((this._pad) && (digitIndex < 0)))? this._bitmaps[0] 60 | : (digitIndex < 0)? 0 61 | : this._bitmaps[characters[digitIndex]]; 62 | 63 | if ((order != 0) || (this._integer)) { 64 | maskHidden |= this._bitmaps["."]; 65 | } 66 | else if ((this._scale > 0) || (digitIndex != characters.length - 1)) { 67 | maskOn |= this._bitmaps["."]; 68 | } 69 | 70 | let svg = this.root.children[j] as SVGElement; 71 | this.updateVector(svg, maskHidden, maskOn); 72 | 73 | --order; 74 | } 75 | } 76 | 77 | private updateVector(svg: SVGElement, maskHidden: number, maskOn: number) { 78 | for (let k = 0; k < svg.childNodes.length; ++k) { 79 | let child = svg.childNodes[k] as Element; 80 | if ((child.nodeType == 1) && (child.nodeName == 'g')) { 81 | let mask: number = 1; 82 | for (let i = 0; i < child.childNodes.length; ++i) { 83 | let segment = child.childNodes[i] as Element; 84 | if (segment.nodeType == 1) { 85 | segment.setAttribute('class', (maskHidden & mask)? 'hidden' : (maskOn & mask)? 'segment-on' : ''); 86 | mask <<= 1; 87 | } 88 | } 89 | 90 | return; 91 | } 92 | } 93 | } 94 | 95 | public get value() { 96 | return this._value; 97 | } 98 | public set value(value: number) { 99 | this._value = value; 100 | this.updateVectors(); 101 | } 102 | 103 | public set scale(scale: number) { 104 | this._scale = scale; 105 | this.updateVectors(); 106 | } 107 | public set integer(integer: boolean) { 108 | this._integer = integer; 109 | this.updateVectors(); 110 | } 111 | public set pad(pad: boolean) { 112 | this._pad = pad; 113 | this.updateVectors(); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /views/lightbox-container/LightboxContainer.scss: -------------------------------------------------------------------------------- 1 | div.lightbox { 2 | @extend .block-fixed; 3 | background-color: rgba($colour-dark, 0.85); 4 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAQElEQVQYlW2OwQ0AMAgCbw7XcMFubj81RVpeGgIcQHK1gJC/+shjtkLNTmpTaZMm05IB76YzfTedaW46k28Opg1VYQ5hW3JdzwAAAABJRU5ErkJggg==); 5 | z-index: $zindex-lightbox; 6 | transition: opacity 200ms ease-in-out; 7 | opacity: 1; 8 | } 9 | 10 | div.lightbox.hidden { 11 | opacity: 0; 12 | } 13 | 14 | div.lightbox-container { 15 | @extend .theme-light; 16 | margin: 40px auto 0; 17 | 18 | @media (min-width: 38em) { 19 | max-width: 36rem; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /views/lightbox-container/LightboxContainer.ts: -------------------------------------------------------------------------------- 1 | namespace Views { 2 | export class LightboxContainer { 3 | private _lightbox: HTMLDivElement; 4 | private _lightboxContainer: HTMLDivElement; 5 | private _view: Views.View; 6 | 7 | constructor(view: Views.View) { 8 | this._lightbox = document.createElement('div'); 9 | this._lightbox.className = 'lightbox'; 10 | 11 | this._lightboxContainer = document.createElement('div'); 12 | this._lightboxContainer.className = 'lightbox-container'; 13 | this._lightbox.appendChild(this._lightboxContainer); 14 | 15 | this._view = view; 16 | this._view.attach(this._lightboxContainer); 17 | } 18 | 19 | public static showLightbox(view: Views.View, suppressTransitions?: boolean): LightboxContainer { 20 | let lightbox = new LightboxContainer(view); 21 | 22 | if (!suppressTransitions) { 23 | lightbox._lightbox.classList.add('hidden'); 24 | } 25 | 26 | document.body.appendChild(lightbox._lightbox); 27 | 28 | if (!suppressTransitions) { 29 | window.setTimeout(() => { 30 | lightbox._lightbox.classList.remove('hidden'); 31 | lightbox._view.activate(); 32 | }, 20); 33 | } 34 | else { 35 | lightbox._view.activate(); 36 | } 37 | 38 | return lightbox; 39 | } 40 | 41 | public hideLightbox() { 42 | this._view.deactivate(); 43 | 44 | let lightbox = $(this._lightbox); 45 | lightbox.one('transitionend', () => lightbox.remove()); 46 | lightbox.addClass('hidden'); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /views/safety-button/SafetyButton.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | -------------------------------------------------------------------------------- /views/safety-button/SafetyButton.scss: -------------------------------------------------------------------------------- 1 | $view-safety-button-thumb-width: 32px; 2 | 3 | .safety-button { 4 | display: block; 5 | overflow: hidden; 6 | } 7 | 8 | .safety-button > div { 9 | display: block; 10 | float: left; 11 | } 12 | 13 | .safety-button .fa-thumbs-up { 14 | background-color: $colour-positive; 15 | } 16 | .safety-button.danger .fa-thumbs-up { 17 | background-color: $colour-negative; 18 | } 19 | 20 | .safety-button > div.button { 21 | height: 1.8em; 22 | line-height: 1.8em; 23 | vertical-align: middle; 24 | } 25 | .safety-button > div.button:not(:first-child) { 26 | padding-left: 0; 27 | padding-right: 0; 28 | margin-left: 2px; 29 | width: $view-safety-button-thumb-width; 30 | visibility: hidden; 31 | opacity: 0; 32 | transition: visibility 100ms ease-in-out, opacity 100ms ease-in-out; 33 | } 34 | .safety-button.primed > div.button:not(:first-child) { 35 | visibility: visible; 36 | opacity: 1; 37 | } 38 | -------------------------------------------------------------------------------- /views/safety-button/SafetyButton.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class SafetyButton extends Views.View { 4 | private _disabled: boolean; 5 | private _primed: boolean; 6 | 7 | private _primary: HTMLDivElement; 8 | private _confirm: HTMLDivElement; 9 | 10 | public callback: Function; 11 | 12 | constructor(text?: string, dangerous?: boolean) { 13 | super(Templates.cloneTemplate('safety-button')); 14 | this._disabled = false; 15 | this._primed = false; 16 | 17 | this._primary = this.root.children[0]; 18 | this._primary.onclick = this._onPrimaryClick; 19 | this._confirm = this.root.children[2]; 20 | this._confirm.onclick = this._onConfirmClick; 21 | 22 | if (text) this.text = text; 23 | if (dangerous) this.dangerous = dangerous; 24 | 25 | document.body.addEventListener('click', this._onClickCaptured, true); 26 | } 27 | 28 | private _onClickCaptured = (event: MouseEvent) => { 29 | if ((this._primed) && (event.target != this._primary) && (event.target != this._confirm)) { 30 | this.primed = false; 31 | } 32 | } 33 | 34 | private _onPrimaryClick = () => { 35 | if (!this._disabled) { 36 | this.primed = (!this._primed); 37 | } 38 | } 39 | 40 | private _onConfirmClick = () => { 41 | if (this._primed) { 42 | this.primed = false; 43 | 44 | if ((!this._disabled) && (this.callback)) { 45 | this.callback(); 46 | } 47 | } 48 | } 49 | 50 | public get text(): string { 51 | return this._primary.innerText; 52 | } 53 | public set text(text: string) { 54 | this._primary.innerText = text; 55 | this._confirm.title = text; 56 | if (!this._primed) this._primary.title = text; 57 | } 58 | 59 | public get primed(): boolean { 60 | return this._primed; 61 | } 62 | public set primed(value: boolean) { 63 | if (!value) { 64 | this._primed = false; 65 | this.root.classList.remove('primed'); 66 | this._primary.title = this._primary.innerText; 67 | } 68 | else { 69 | this.root.classList.add('primed'); 70 | this._primed = true; 71 | this._primary.title = "cancel"; 72 | } 73 | } 74 | 75 | public get disabled(): boolean { 76 | return this._disabled; 77 | } 78 | public set disabled(value: boolean) { 79 | if (!value) { 80 | this._disabled = false; 81 | this.root.classList.remove('disabled'); 82 | } 83 | else { 84 | this.primed = false; 85 | this._disabled = true; 86 | this.root.classList.add('disabled'); 87 | } 88 | } 89 | 90 | public get dangerous(): boolean { 91 | return this._confirm.classList.contains('negative'); 92 | } 93 | public set dangerous(dangerous: boolean) { 94 | if (dangerous) { 95 | this._confirm.classList.add('negative'); 96 | this._confirm.classList.remove('positive'); 97 | } 98 | else { 99 | this._confirm.classList.add('positive'); 100 | this._confirm.classList.remove('negative'); 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /views/sidebar-menu/SidebarMenu.scss: -------------------------------------------------------------------------------- 1 | ul.sidebar-menu { 2 | @extend .list-none; 3 | margin: 16px $view-sidebar-right-margin 16px -6px; 4 | } 5 | 6 | ul.sidebar-menu > li { 7 | @extend .control-clickable; 8 | position: relative; 9 | padding: 4px 0 4px 6px; 10 | border-left: 6px solid $colour-medium; 11 | white-space: nowrap; 12 | } 13 | 14 | ul.sidebar-menu > li:hover { 15 | border-left-color: darken($colour-medium, 5%); 16 | background-color: $colour-lighter; 17 | } 18 | 19 | ul.sidebar-menu > li.active, 20 | ul.sidebar-menu > li.active:hover { 21 | border-left-color: $colour-negative; 22 | background-color: $colour-lighter; 23 | font-weight: bold; 24 | } 25 | 26 | ul.sidebar-menu > li:before { 27 | width: 1rem; 28 | margin-right: 8px; 29 | text-align: center; 30 | } 31 | 32 | ul.sidebar-menu span.btn-close { 33 | @extend .block-absolute; 34 | left: auto; 35 | padding: 4px; 36 | opacity: 0; 37 | transition: opacity 100ms; 38 | } 39 | ul.sidebar-menu > li:hover span.btn-close { 40 | opacity: 1; 41 | } 42 | ul.sidebar-menu span.btn-close:hover { 43 | opacity: 1; 44 | color: $colour-negative; 45 | } 46 | -------------------------------------------------------------------------------- /views/sidebar-menu/SidebarMenu.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export interface SidebarMenuItem { 4 | id: number, 5 | name: string; 6 | className: string; 7 | closeable: boolean; 8 | } 9 | 10 | export class SidebarMenu extends Views.DataBoundList { 11 | public selectionCallback: (id: number) => void; 12 | public closeCallback: (id: number) => void; 13 | 14 | constructor() { 15 | super(document.createElement('ul')); 16 | this.root.className = 'sidebar-menu'; 17 | } 18 | 19 | protected createChild(key: string, datum: SidebarMenuItem): HTMLLIElement { 20 | let element = document.createElement('li'); 21 | element.appendChild(document.createElement('span')); 22 | element.onclick = () => { if (this.selectionCallback) this.selectionCallback(datum.id); }; 23 | 24 | if (datum.closeable) { 25 | let closeButton = document.createElement('span'); 26 | closeButton.className = "btn-close fa-times"; 27 | closeButton.onclick = (e) => { if (this.closeCallback) this.closeCallback(datum.id); e.stopPropagation(); e.preventDefault(); return false; }; 28 | element.appendChild(closeButton); 29 | } 30 | 31 | this.updateChild(key, datum, element); 32 | return element; 33 | } 34 | 35 | protected updateChild(key: string, datum: SidebarMenuItem, element: HTMLLIElement): void { 36 | element.getElementsByTagName('span')[0].innerText = datum.name; 37 | element.className = datum.className; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /views/sign-in-form/SignInForm.html: -------------------------------------------------------------------------------- 1 |
2 | 13 |
14 | -------------------------------------------------------------------------------- /views/sign-in-form/SignInForm.scss: -------------------------------------------------------------------------------- 1 | .sign-in-form { 2 | overflow: hidden; 3 | max-width: 280px; 4 | padding: 42px; 5 | margin: 42px auto; 6 | } 7 | 8 | .sign-in-form h2 { 9 | margin-bottom: 2rem; 10 | font-size: 2rem; 11 | } 12 | 13 | .sign-in-form input[name=username], form.sign-in-form input[name=password] { 14 | width: 100%; 15 | font-size: 18px; 16 | } 17 | 18 | .sign-in-form .checkbox { 19 | margin-top: 16px; 20 | } 21 | 22 | .sign-in-form .checkbox label { 23 | margin: 0.5rem 0; 24 | } 25 | 26 | .sign-in-form button[type=submit] { 27 | margin: 2rem 0 2rem; 28 | float: right; 29 | } 30 | 31 | .sign-in-form .error-notice, .sign-in-form .remember-me-warning { 32 | color: $colour-negative; 33 | } 34 | .sign-in-form .remember-me-warning { 35 | font-size: small; 36 | font-style: oblique; 37 | } 38 | -------------------------------------------------------------------------------- /views/sign-in-form/SignInForm.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class SignInForm extends Views.View { 4 | private _disabled: boolean; 5 | 6 | private _errorNotice: HTMLParagraphElement; 7 | private _inputUsername: HTMLInputElement; 8 | private _inputPassword: HTMLInputElement; 9 | private _inputRememberMe: HTMLInputElement; 10 | private _inputRememberMeNotice: HTMLParagraphElement; 11 | private _submitButton: HTMLButtonElement; 12 | 13 | public submitCallback: (form: SignInForm) => void; 14 | 15 | constructor() { 16 | super(Views.Templates.cloneTemplate('sign-in-form')); 17 | 18 | this._errorNotice = this.root.querySelector('.error-notice') as HTMLParagraphElement; 19 | this._inputUsername = this.root.querySelector('input[name="username"]') as HTMLInputElement; 20 | this._inputPassword = this.root.querySelector('input[name="password"]') as HTMLInputElement; 21 | this._inputRememberMe = this.root.querySelector('input[name="remember-me"]') as HTMLInputElement; 22 | this._inputRememberMeNotice = this.root.querySelector('.remember-me-warning') as HTMLParagraphElement; 23 | this._submitButton = this.root.querySelector('button[type="submit"]') as HTMLButtonElement; 24 | 25 | $(this._inputRememberMe).click((e) => this.onRememberMeClick(e)); 26 | $(this.root).submit((e) => this.onFormSubmit(e)); 27 | 28 | this._disabled = false; 29 | } 30 | 31 | get disabled(): boolean { 32 | return this._disabled; 33 | } 34 | set disabled(value: boolean) { 35 | if (value != this._disabled) { 36 | $([this._inputUsername, this._inputPassword, this._inputRememberMe, this._submitButton]).prop("disabled", value); 37 | this._disabled = value; 38 | } 39 | } 40 | 41 | public get username() { return this._inputUsername.value; } 42 | public set username(value: string) { this._inputUsername.value = value; } 43 | 44 | public get password() { return this._inputPassword.value; } 45 | public set password(value: string) { this._inputPassword.value = value; } 46 | 47 | public get rememberMe() { return this._inputRememberMe.checked; } 48 | public set rememberMe(value: boolean) { 49 | this._inputRememberMe.checked = value; 50 | if (value) $(this._inputRememberMeNotice).show(); 51 | else $(this._inputRememberMeNotice).hide(); 52 | } 53 | 54 | public focus() { 55 | if ($(this._inputUsername).val()) $(this._inputPassword).focus(); 56 | else $(this._inputUsername).focus(); 57 | } 58 | 59 | public set errorNotice(message: string) { 60 | if (message) { 61 | this._errorNotice.innerText = message; 62 | $(this._errorNotice).slideDown(160); 63 | } 64 | else { 65 | $(this._errorNotice).slideUp(160); 66 | } 67 | } 68 | 69 | private onRememberMeClick(e: JQueryEventObject) { 70 | if (this._inputRememberMe.checked) 71 | $(this._inputRememberMeNotice).slideDown(160); 72 | else 73 | $(this._inputRememberMeNotice).slideUp(160); 74 | } 75 | 76 | private onFormSubmit(e: JQueryEventObject) { 77 | e.preventDefault(); 78 | if ((this._disabled) || (!this._inputUsername.checkValidity()) || (!this._inputPassword.checkValidity())) return; 79 | if (this.submitCallback) this.submitCallback(this); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /views/table-body/TableBody.ts: -------------------------------------------------------------------------------- 1 | /// 2 | namespace Views { 3 | export class TableBody extends Views.DataBoundList { 4 | private _width: number; 5 | 6 | constructor() { 7 | super(document.createElement('tbody')); 8 | } 9 | 10 | public update(items: string[][]) { 11 | this._width = ((items) && (items.length > 0))? items[0].length : 1; 12 | this.bindArray(items); 13 | } 14 | 15 | protected createChild(key: string, datum: string[]): HTMLTableRowElement { 16 | let element = document.createElement('tr'); 17 | for (let i = 0; i < this._width; ++i) { 18 | element.appendChild(document.createElement('td')); 19 | } 20 | 21 | this.updateChild(key, datum, element); 22 | return element; 23 | } 24 | 25 | protected updateChild(key: string, datum: string[], element: HTMLTableRowElement): void { 26 | for (let i = 0; i < element.childElementCount; ++i) { 27 | let d = (i < datum.length)? datum[i] : undefined; 28 | (element.children[i]).innerText = (d != null)? d : ""; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /wgo.js/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Jan Prokop 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 7 | to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or 10 | substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 13 | BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 15 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /wgo.js/wgo.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace WGo { 2 | const B: number; // +1 3 | const W: number; // -1 4 | 5 | interface BoardConfig { 6 | size?: number; 7 | width?: number; 8 | background?: string; 9 | } 10 | 11 | interface Coordinates { 12 | x: number, 13 | y: number 14 | } 15 | interface ColourCoordinates extends Coordinates { 16 | c: number 17 | } 18 | 19 | interface BoardObject extends ColourCoordinates { 20 | // type can be WGo.Board.DrawHandler or name of predefined handler 21 | type?: BoardDrawHandler | string 22 | } 23 | interface BoardRemoveObject extends Coordinates { 24 | // type can be WGo.Board.DrawHandler or name of predefined handler 25 | type?: BoardDrawHandler | string 26 | } 27 | 28 | interface Board extends BoardConfig { 29 | element: HTMLElement; 30 | 31 | setWidth(width: number): void; 32 | setSize(size?: number): void; 33 | 34 | addEventListener(type: string, callback: (x: number, y: number, e: Event) => void): void; 35 | removeEventListener(type: string, callback: Function): boolean; 36 | 37 | addObject(obj: BoardObject | BoardObject[]): void; 38 | removeObject(obj: BoardRemoveObject | BoardRemoveObject[]): void; 39 | removeAllObjects(): void; 40 | 41 | stoneRadius: number; // Size of stone radius in pixels. (read only) 42 | ls: number; // Line-Shift (defined in wgo.js line 1050) 43 | obj_arr: ColourCoordinates[][][]; // Object Structure 44 | 45 | getX(x: number): number; // Returns absolute x-coordinate of file or column x 46 | getY(y: number): number; // Returns absolute y-coordinate of rank or row y 47 | } 48 | var Board: { 49 | prototype: Board; 50 | new (elem: HTMLElement, config: BoardConfig): Board; 51 | } 52 | 53 | export interface BoardDrawHandlerArgs extends ColourCoordinates { 54 | } 55 | export interface BoardDrawObject { 56 | // Both functions are called in "CanvasRenderingContext2D" context of given layer 57 | draw: (this: CanvasRenderingContext2D, args: BoardDrawHandlerArgs, board: Board) => void; // this function should make drawing on the layer 58 | clear?: (this: CanvasRenderingContext2D, args: BoardDrawHandlerArgs, board: Board) => void; // this function should clear drawing produced by draw function (if this function is ommited, default clearing function is used instead) 59 | } 60 | interface BoardDrawHandler { // WGo.Board.DrawHandler 61 | stone?: BoardDrawObject; // Highest default Canvas layer 62 | shadow?: BoardDrawObject; // Middle default Canvas layer 63 | grid?: BoardDrawObject; // Lowest default Canvas layer 64 | } 65 | } 66 | --------------------------------------------------------------------------------