├── .eslintrc ├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── docs ├── app.js ├── favicon.ico └── index.html ├── package-lock.json ├── package.json ├── src └── index.ts ├── tsconfig.json └── webpack ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2017": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 10 | "plugin:import/typescript" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "project": "./tsconfig.json", 15 | "sourceType": "module" 16 | }, 17 | "ignorePatterns": [ 18 | "**/*.d.ts", 19 | "**/*.js", 20 | "**/*.mjs" 21 | ], 22 | "plugins": [ 23 | "@typescript-eslint", 24 | "import" 25 | ], 26 | "rules": { 27 | "@typescript-eslint/no-non-null-assertion": "off", 28 | "import/no-self-import": "warn", 29 | "import/no-cycle": "warn", 30 | "curly": [ 31 | "error", 32 | "multi-line" 33 | ], 34 | "no-template-curly-in-string": "error", 35 | "prefer-const": "error", 36 | "prefer-object-spread": "error", 37 | "radix": "error", 38 | "no-irregular-whitespace": [ 39 | "error", 40 | { 41 | "skipComments": true 42 | } 43 | ], 44 | "no-unused-vars": "off", 45 | "@typescript-eslint/no-unused-vars": [ 46 | "warn", 47 | { 48 | "vars": "all", 49 | "args": "all", 50 | "ignoreRestSiblings": false, 51 | "argsIgnorePattern": "^_" 52 | } 53 | ], 54 | "@typescript-eslint/await-thenable": "error", 55 | "@typescript-eslint/no-floating-promises": "error", 56 | "@typescript-eslint/member-delimiter-style": [ 57 | "error", 58 | { 59 | "multiline": { 60 | "delimiter": "semi", 61 | "requireLast": true 62 | }, 63 | "singleline": { 64 | "delimiter": "semi", 65 | "requireLast": false 66 | } 67 | } 68 | ], 69 | "@typescript-eslint/semi": [ 70 | "error", 71 | "always" 72 | ], 73 | "@typescript-eslint/strict-boolean-expressions": "error" 74 | } 75 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # misc 7 | .DS_Store 8 | npm-debug.log* 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.detectIndentation": false, 4 | "editor.insertSpaces": true, 5 | "editor.tabSize": 2, 6 | "editor.formatOnType": true, 7 | "editor.formatOnSave": true, 8 | "editor.formatOnPaste": true 9 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "lint", 7 | "problemMatcher": [ 8 | "$eslint-stylish" 9 | ], 10 | "label": "npm: lint", 11 | "detail": "eslint --ext .ts ." 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /docs/app.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e={18:(e,t,n)=>{var r=n(178);e.exports=function(e,t,n){var o=[],i=!1,a=-1;function l(){for(a=0;a=0&&(o.splice(i,2),i<=a&&(a-=2),e(t,[])),null!=n&&(o.push(t,n),e(t,r(n),u))},redraw:u}}},223:(e,t,n)=>{var r=n(178),o=n(373),i=n(164),a=n(249),l=n(561),u=n(562),s=n(641),f=n(542),c={};function d(e){try{return decodeURIComponent(e)}catch(t){return e}}e.exports=function(e,t){var n,p,h,v,m,g,y=null==e?null:"function"==typeof e.setImmediate?e.setImmediate:e.setTimeout,w=i.resolve(),b=!1,x=!1,k=0,S=c,j={onbeforeupdate:function(){return!(!(k=k?2:1)||c===S)},onremove:function(){e.removeEventListener("popstate",C,!1),e.removeEventListener("hashchange",A,!1)},view:function(){if(k&&c!==S){var e=[r(h,v.key,v)];return S&&(e=S.render(e[0])),e}}},E=T.SKIP={};function A(){b=!1;var r=e.location.hash;"#"!==T.prefix[0]&&(r=e.location.search+r,"?"!==T.prefix[0]&&"/"!==(r=e.location.pathname+r)[0]&&(r="/"+r));var o=r.concat().replace(/(?:%[a-f89][a-f0-9])+/gim,d).slice(T.prefix.length),i=l(o);function a(e){console.error(e),N(p,null,{replace:!0})}s(i.params,e.history.state),function e(r){for(;r{var r=n(373);r.trust=n(742),r.fragment=n(621),e.exports=r},865:(e,t,n)=>{var r=n(262),o=n(74),i=n(165),a=function(){return r.apply(this,arguments)};a.m=r,a.trust=r.trust,a.fragment=r.fragment,a.Fragment="[",a.mount=i.mount,a.route=n(843),a.render=n(358),a.redraw=i.redraw,a.request=o.request,a.jsonp=o.jsonp,a.parseQueryString=n(874),a.buildQueryString=n(478),a.parsePathname=n(561),a.buildPathname=n(249),a.vnode=n(178),a.PromisePolyfill=n(803),a.censor=n(542),e.exports=a},165:(e,t,n)=>{var r=n(358);e.exports=n(18)(r,"undefined"!=typeof requestAnimationFrame?requestAnimationFrame:null,"undefined"!=typeof console?console:null)},249:(e,t,n)=>{var r=n(478),o=n(641);e.exports=function(e,t){if(/:([^\/\.-]+)(\.{3})?:/.test(e))throw new SyntaxError("Template parameter names must be separated by either a '/', '-', or '.'.");if(null==t)return e;var n=e.indexOf("?"),i=e.indexOf("#"),a=i<0?e.length:i,l=n<0?a:n,u=e.slice(0,l),s={};o(s,t);var f=u.replace(/:([^\/\.-]+)(\.{3})?/g,(function(e,n,r){return delete s[n],null==t[n]?e:r?t[n]:encodeURIComponent(String(t[n]))})),c=f.indexOf("?"),d=f.indexOf("#"),p=d<0?f.length:d,h=c<0?p:c,v=f.slice(0,h);n>=0&&(v+=e.slice(n,a)),c>=0&&(v+=(n<0?"?":"&")+f.slice(c,p));var m=r(s);return m&&(v+=(n<0&&c<0?"?":"&")+m),i>=0&&(v+=e.slice(i)),d>=0&&(v+=(i<0?"":"&")+f.slice(d)),v}},562:(e,t,n)=>{var r=n(561);e.exports=function(e){var t=r(e),n=Object.keys(t.params),o=[],i=new RegExp("^"+t.path.replace(/:([^\/.-]+)(\.{3}|\.(?!\.)|-)?|[\\^$*+.()|\[\]{}]/g,(function(e,t,n){return null==t?"\\"+e:(o.push({k:t,r:"..."===n}),"..."===n?"(.*)":"."===n?"([^/]+)\\.":"([^/]+)"+(n||""))}))+"$");return function(e){for(var r=0;r{var r=n(874);e.exports=function(e){var t=e.indexOf("?"),n=e.indexOf("#"),o=n<0?e.length:n,i=t<0?o:t,a=e.slice(0,i).replace(/\/{2,}/g,"/");return a?("/"!==a[0]&&(a="/"+a),a.length>1&&"/"===a[a.length-1]&&(a=a.slice(0,-1))):a="/",{path:a,params:t<0?{}:r(e.slice(t+1,o))}}},803:e=>{var t=function(e){if(!(this instanceof t))throw new Error("Promise must be called with 'new'.");if("function"!=typeof e)throw new TypeError("executor must be a function.");var n=this,r=[],o=[],i=s(r,!0),a=s(o,!1),l=n._instance={resolvers:r,rejectors:o},u="function"==typeof setImmediate?setImmediate:setTimeout;function s(e,t){return function i(s){var c;try{if(!t||null==s||"object"!=typeof s&&"function"!=typeof s||"function"!=typeof(c=s.then))u((function(){t||0!==e.length||console.error("Possible unhandled promise rejection:",s);for(var n=0;n0||e(n)}}var r=n(a);try{e(n(i),r)}catch(e){r(e)}}f(e)};t.prototype.then=function(e,n){var r,o,i=this._instance;function a(e,t,n,a){t.push((function(t){if("function"!=typeof e)n(t);else try{r(e(t))}catch(e){o&&o(e)}})),"function"==typeof i.retry&&a===i.state&&i.retry()}var l=new t((function(e,t){r=e,o=t}));return a(e,i.resolvers,r,!0),a(n,i.rejectors,o,!1),l},t.prototype.catch=function(e){return this.then(null,e)},t.prototype.finally=function(e){return this.then((function(n){return t.resolve(e()).then((function(){return n}))}),(function(n){return t.resolve(e()).then((function(){return t.reject(n)}))}))},t.resolve=function(e){return e instanceof t?e:new t((function(t){t(e)}))},t.reject=function(e){return new t((function(t,n){n(e)}))},t.all=function(e){return new t((function(t,n){var r=e.length,o=0,i=[];if(0===e.length)t([]);else for(var a=0;a{var r=n(803);"undefined"!=typeof window?(void 0===window.Promise?window.Promise=r:window.Promise.prototype.finally||(window.Promise.prototype.finally=r.prototype.finally),e.exports=window.Promise):void 0!==n.g?(void 0===n.g.Promise?n.g.Promise=r:n.g.Promise.prototype.finally||(n.g.Promise.prototype.finally=r.prototype.finally),e.exports=n.g.Promise):e.exports=r},478:e=>{e.exports=function(e){if("[object Object]"!==Object.prototype.toString.call(e))return"";var t=[];for(var n in e)r(n,e[n]);return t.join("&");function r(e,n){if(Array.isArray(n))for(var o=0;o{function t(e){try{return decodeURIComponent(e)}catch(t){return e}}e.exports=function(e){if(""===e||null==e)return{};"?"===e.charAt(0)&&(e=e.slice(1));for(var n=e.split("&"),r={},o={},i=0;i-1&&s.pop();for(var c=0;c{e.exports=n(452)("undefined"!=typeof window?window:null)},621:(e,t,n)=>{var r=n(178),o=n(359);e.exports=function(){var e=o.apply(0,arguments);return e.tag="[",e.children=r.normalizeChildren(e.children),e}},373:(e,t,n)=>{var r=n(178),o=n(359),i=n(188),a=/(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g,l={};function u(e){for(var t in e)if(i.call(e,t))return!1;return!0}e.exports=function(e){if(null==e||"string"!=typeof e&&"function"!=typeof e&&"function"!=typeof e.view)throw Error("The selector must be either a string or a component.");var t=o.apply(1,arguments);return"string"==typeof e&&(t.children=r.normalizeChildren(t.children),"["!==e)?function(e,t){var n=t.attrs,r=i.call(n,"class"),o=r?n.class:n.className;if(t.tag=e.tag,t.attrs={},!u(e.attrs)&&!u(n)){var a={};for(var l in n)i.call(n,l)&&(a[l]=n[l]);n=a}for(var l in e.attrs)i.call(e.attrs,l)&&"className"!==l&&!i.call(n,l)&&(n[l]=e.attrs[l]);for(var l in null==o&&null==e.attrs.className||(n.className=null!=o?null!=e.attrs.className?String(e.attrs.className)+" "+String(o):o:null!=e.attrs.className?e.attrs.className:null),r&&(n.class=null),n)if(i.call(n,l)&&"key"!==l){t.attrs=n;break}return t}(l[e]||function(e){for(var t,n="div",r=[],o={};t=a.exec(e);){var i=t[1],u=t[2];if(""===i&&""!==u)n=u;else if("#"===i)o.id=u;else if("."===i)r.push(u);else if("["===t[3][0]){var s=t[6];s&&(s=s.replace(/\\(["'])/g,"$1").replace(/\\\\/g,"\\")),"class"===t[4]?r.push(s):o[t[4]]=""===s?s:s||!0}}return r.length>0&&(o.className=r.join(" ")),l[e]={tag:n,attrs:o}}(e),t):(t.tag=e,t)}},359:(e,t,n)=>{var r=n(178);e.exports=function(){var e,t=arguments[this],n=this+1;if(null==t?t={}:("object"!=typeof t||null!=t.tag||Array.isArray(t))&&(t={},n=this),arguments.length===n+1)e=arguments[n],Array.isArray(e)||(e=[e]);else for(e=[];n{var r=n(178);e.exports=function(e){var t,n=e&&e.document,o={svg:"http://www.w3.org/2000/svg",math:"http://www.w3.org/1998/Math/MathML"};function i(e){return e.attrs&&e.attrs.xmlns||o[e.tag]}function a(e,t){if(e.state!==t)throw new Error("'vnode.state' must not be modified.")}function l(e){var t=e.state;try{return this.apply(t,arguments)}finally{a(e,t)}}function u(){try{return n.activeElement}catch(e){return null}}function s(e,t,n,r,o,i,a){for(var l=n;l'+t.children+"",a=a.firstChild):a.innerHTML=t.children,t.dom=a.firstChild,t.domSize=a.childNodes.length,t.instance=[];for(var l,u=n.createDocumentFragment();l=a.firstChild;)t.instance.push(l),u.appendChild(l);b(e,u,o)}function p(e,t,n,r,o,i){if(t!==n&&(null!=t||null!=n))if(null==t||0===t.length)s(e,n,0,n.length,r,o,i);else if(null==n||0===n.length)k(e,t,0,t.length);else{var a=null!=t[0]&&null!=t[0].key,l=null!=n[0]&&null!=n[0].key,u=0,c=0;if(!a)for(;c=c&&E>=u&&(w=t[j],b=n[E],w.key===b.key);)w!==b&&h(e,w,b,r,o,i),null!=b.dom&&(o=b.dom),j--,E--;for(;j>=c&&E>=u&&(d=t[c],p=n[u],d.key===p.key);)c++,u++,d!==p&&h(e,d,p,r,g(t,c,o),i);for(;j>=c&&E>=u&&u!==E&&d.key===b.key&&w.key===p.key;)y(e,w,x=g(t,c,o)),w!==p&&h(e,w,p,r,x,i),++u<=--E&&y(e,d,o),d!==b&&h(e,d,b,r,o,i),null!=b.dom&&(o=b.dom),c++,w=t[--j],b=n[E],d=t[c],p=n[u];for(;j>=c&&E>=u&&w.key===b.key;)w!==b&&h(e,w,b,r,o,i),null!=b.dom&&(o=b.dom),E--,w=t[--j],b=n[E];if(u>E)k(e,t,c,j+1);else if(c>j)s(e,n,u,E+1,r,o,i);else{var A,C,N=o,T=E-u+1,z=new Array(T),O=0,P=0,_=2147483647,I=0;for(P=0;P=u;P--){null==A&&(A=v(t,c,j+1));var L=A[(b=n[P]).key];null!=L&&(_=L<_?L:-1,z[P-u]=L,w=t[L],t[L]=null,w!==b&&h(e,w,b,r,o,i),null!=b.dom&&(o=b.dom),I++)}if(o=N,I!==j-c+1&&k(e,t,c,j+1),0===I)s(e,n,u,E+1,r,o,i);else if(-1===_)for(C=function(e){var t=[0],n=0,r=0,o=0,i=m.length=e.length;for(o=0;o>>1)+(r>>>1)+(n&r&1);e[t[l]]0&&(m[o]=t[n-1]),t[n]=o)}}for(r=t[(n=t.length)-1];n-- >0;)t[n]=r,r=m[r];return m.length=0,t}(z),O=C.length-1,P=E;P>=u;P--)p=n[P],-1===z[P-u]?f(e,p,r,i,o):C[O]===P-u?O--:y(e,p,o),null!=p.dom&&(o=n[P].dom);else for(P=E;P>=u;P--)p=n[P],-1===z[P-u]&&f(e,p,r,i,o),null!=p.dom&&(o=n[P].dom)}}else{var R=t.lengthR&&k(e,t,u,t.length),n.length>R&&s(e,n,u,n.length,r,o,i)}}}function h(e,t,n,o,a,u){var s=t.tag;if(s===n.tag){if(n.state=t.state,n.events=t.events,function(e,t){do{var n;if(null!=e.attrs&&"function"==typeof e.attrs.onbeforeupdate&&void 0!==(n=l.call(e.attrs.onbeforeupdate,e,t))&&!n)break;if("string"!=typeof e.tag&&"function"==typeof e.state.onbeforeupdate&&void 0!==(n=l.call(e.state.onbeforeupdate,e,t))&&!n)break;return!1}while(0);return e.dom=t.dom,e.domSize=t.domSize,e.instance=t.instance,e.attrs=t.attrs,e.children=t.children,e.text=t.text,!0}(n,t))return;if("string"==typeof s)switch(null!=n.attrs&&D(n.attrs,n,o),s){case"#":!function(e,t){e.children.toString()!==t.children.toString()&&(e.dom.nodeValue=t.children),t.dom=e.dom}(t,n);break;case"<":!function(e,t,n,r,o){t.children!==n.children?(j(e,t),d(e,n,r,o)):(n.dom=t.dom,n.domSize=t.domSize,n.instance=t.instance)}(e,t,n,u,a);break;case"[":!function(e,t,n,r,o,i){p(e,t.children,n.children,r,o,i);var a=0,l=n.children;if(n.dom=null,null!=l){for(var u=0;u-1||null!=e.attrs&&e.attrs.is||"href"!==t&&"list"!==t&&"form"!==t&&"width"!==t&&"height"!==t)&&t in e.dom}var O,P=/[A-Z]/g;function _(e){return"-"+e.toLowerCase()}function I(e){return"-"===e[0]&&"-"===e[1]?e:"cssFloat"===e?"float":e.replace(P,_)}function L(e,t,n){if(t===n);else if(null==n)e.style.cssText="";else if("object"!=typeof n)e.style.cssText=n;else if(null==t||"object"!=typeof t)for(var r in e.style.cssText="",n)null!=(o=n[r])&&e.style.setProperty(I(r),String(o));else{for(var r in n){var o;null!=(o=n[r])&&(o=String(o))!==String(t[r])&&e.style.setProperty(I(r),o)}for(var r in t)null!=t[r]&&null==n[r]&&e.style.removeProperty(I(r))}}function R(){this._=t}function $(e,n,r){if(null!=e.events){if(e.events._=t,e.events[n]===r)return;null==r||"function"!=typeof r&&"object"!=typeof r?(null!=e.events[n]&&e.dom.removeEventListener(n.slice(2),e.events,!1),e.events[n]=void 0):(null==e.events[n]&&e.dom.addEventListener(n.slice(2),e.events,!1),e.events[n]=r)}else null==r||"function"!=typeof r&&"object"!=typeof r||(e.events=new R,e.dom.addEventListener(n.slice(2),e.events,!1),e.events[n]=r)}function M(e,t,n){"function"==typeof e.oninit&&l.call(e.oninit,t),"function"==typeof e.oncreate&&n.push(l.bind(e.oncreate,t))}function D(e,t,n){"function"==typeof e.onupdate&&n.push(l.bind(e.onupdate,t))}return R.prototype=Object.create(null),R.prototype.handleEvent=function(e){var t,n=this["on"+e.type];"function"==typeof n?t=n.call(e.currentTarget,e):"function"==typeof n.handleEvent&&n.handleEvent(e),this._&&!1!==e.redraw&&(0,this._)(),!1===t&&(e.preventDefault(),e.stopPropagation())},function(e,n,o){if(!e)throw new TypeError("DOM element being rendered to does not exist.");if(null!=O&&e.contains(O))throw new TypeError("Node is currently being rendered to and thus is locked.");var i=t,a=O,l=[],s=u(),f=e.namespaceURI;O=e,t="function"==typeof o?o:void 0;try{null==e.vnodes&&(e.textContent=""),n=r.normalizeChildren(Array.isArray(n)?n:[n]),p(e,e.vnodes,n,l,null,"http://www.w3.org/1999/xhtml"===f?void 0:f),e.vnodes=n,null!=s&&u()!==s&&"function"==typeof s.focus&&s.focus();for(var c=0;c{var r=n(178);e.exports=function(e){return null==e&&(e=""),r("<",void 0,void 0,e,void 0,void 0)}},178:e=>{function t(e,t,n,r,o,i){return{tag:e,key:t,attrs:n,children:r,text:o,dom:i,domSize:void 0,state:void 0,events:void 0,instance:void 0}}t.normalize=function(e){return Array.isArray(e)?t("[",void 0,void 0,t.normalizeChildren(e),void 0,void 0):null==e||"boolean"==typeof e?null:"object"==typeof e?e:t("#",void 0,void 0,String(e),void 0,void 0)},t.normalizeChildren=function(e){var n=[];if(e.length){for(var r=null!=e[0]&&null!=e[0].key,o=1;o{var r=n(164),o=n(165);e.exports=n(775)("undefined"!=typeof window?window:null,r,o.redraw)},775:(e,t,n)=>{var r=n(249),o=n(188);e.exports=function(e,t,n){var i=0;function a(e){return new t(e)}function l(e){return function(o,i){"string"!=typeof o?(i=o,o=o.url):null==i&&(i={});var l=new t((function(t,n){e(r(o,i.params),i,(function(e){if("function"==typeof i.type)if(Array.isArray(e))for(var n=0;n=200&&e.target.status<300||304===e.target.status||/^file:\/\//i.test(t),l=e.target.response;if("json"===c){if(!e.target.responseType&&"function"!=typeof n.extract)try{l=JSON.parse(e.target.responseText)}catch(e){l=null}}else c&&"text"!==c||null==l&&(l=e.target.responseText);if("function"==typeof n.extract?(l=n.extract(e.target,n),a=!0):"function"==typeof n.deserialize&&(l=n.deserialize(l)),a)r(l);else{var u=function(){try{o=e.target.responseText}catch(e){o=l}var t=new Error(o);t.code=e.target.status,t.response=l,i(t)};0===d.status?setTimeout((function(){h||u()})):u()}}catch(e){i(e)}},d.ontimeout=function(e){h=!0;var t=new Error("Request timed out");t.code=e.target.status,i(t)},"function"==typeof n.config&&(d=n.config(d,n,t)||d)!==v&&(a=d.abort,d.abort=function(){p=!0,a.call(this)}),null==s?d.send():"function"==typeof n.serialize?d.send(n.serialize(s)):s instanceof e.FormData||s instanceof e.URLSearchParams?d.send(s):d.send(JSON.stringify(s))})),jsonp:l((function(t,n,r,o){var a=n.callbackName||"_mithril_"+Math.round(1e16*Math.random())+"_"+i++,l=e.document.createElement("script");e[a]=function(t){delete e[a],l.parentNode.removeChild(l),r(t)},l.onerror=function(){delete e[a],l.parentNode.removeChild(l),o(new Error("JSONP request failed"))},l.src=t+(t.indexOf("?")<0?"?":"&")+encodeURIComponent(n.callbackKey||"callback")+"="+encodeURIComponent(a),e.document.documentElement.appendChild(l)}))}}},843:(e,t,n)=>{var r=n(165);e.exports=n(223)("undefined"!=typeof window?window:null,r)},641:(e,t,n)=>{var r=n(188);e.exports=Object.assign||function(e,t){for(var n in t)r.call(t,n)&&(e[n]=t[n])}},542:(e,t,n)=>{var r=n(188),o=new RegExp("^(?:key|oninit|oncreate|onbeforeupdate|onupdate|onbeforeremove|onremove)$");e.exports=function(e,t){var n={};if(null!=t)for(var i in e)r.call(e,i)&&!o.test(i)&&t.indexOf(i)<0&&(n[i]=e[i]);else for(var i in e)r.call(e,i)&&!o.test(i)&&(n[i]=e[i]);return n}},188:e=>{e.exports={}.hasOwnProperty},607:function(e,t,n){var r,o=this&&this.__assign||function(){return o=Object.assign||function(e){for(var t,n=1,r=arguments.length;n0)&&!(r=i.next()).done;)a.push(r.value)}catch(e){o={error:e}}finally{try{r&&!r.done&&(n=i.return)&&n.call(i)}finally{if(o)throw o.error}}return a},a=this&&this.__spreadArray||function(e,t,n){if(n||2===arguments.length)for(var r,o=0,i=t.length;o=64))return{downwards:Math.floor(e/8),rightwards:e%8}}function S(e){if(!(e.downwards<0||e.downwards>=8||e.rightwards<0||e.rightwards>=8))return 8*e.downwards+e.rightwards}function j(e,t){e.rightwards+=t.rightwards,e.downwards+=t.downwards}function E(e,t,n){var r=1-n;return g.map((function(i){for(var a=0,l=o({},t);;){j(l,i);var u=S(l);if(e[u]!==r)return e[u]===n?a:0;a+=1}}))}function A(e,t,n){if(e[t]===s){var r=k(t),l=E(e,r,n);if(0!==l.reduce((function(e,t){return e+t}))){var u=a([],i(e),!1);return u[t]=n,function(e,t,n){for(var r=0;r0}))}function T(e,t,n,r,o){return void 0===n&&(n=12),void 0===r&&(r=4),void 0===o&&(o=1),e.reduce((function(e,i,a){return e+(i!==t?0:0===a||7===a||56===a||63===a?n:a<=7||a>=56||a%8==0||a%8==7?r:o)}),0)}function z(){var e,t,n;function r(t){e=void 0,void 0!==n&&clearTimeout(n),n=void 0;var r=t.attrs,o=r.boardStr,i=r.turnStr,a=r.flagsCode,l=Number(i),u=x(a);if(0===l&&u.ai0||1===l&&u.ai1){var s=w(o),f=function(e,t){for(var n=1-t,r=-1/0,o=[],i=0;i<64;i++){var a=A(e,i,t);if(Array.isArray(a)){for(var l=(T(a,t)-T(a,n))/100,u=1/0,s=0;s<64;s++){var f=A(a,s,n);if(Array.isArray(f)){var c=T(f,t)-T(f,n)+l;cr&&(r=u,o=[i])}}return o}(s,l),c=f[Math.floor(Math.random()*f.length)];void 0!==c&&(n=setTimeout((function(){return C(s,c,l,t)}),2e3))}}return{oncreate:r,onupdate:r,view:function(r){var i=r.attrs,a=i.boardStr,l=i.turnStr,f=i.lastPieceStr,c=i.flagsCode,d=x(c),v=w(a),g=Number(l),y="-"===f?void 0:Number(f),S=k(null!=y?y:-1),j=function(e){return e.reduce((function(e,t){return e[t]+=1,e}),[0,0,0])}(v),E=j[s],A=void 0===t||E===t-1,T=N(v,g),z=1-g,O=!T&&N(v,z),P=!T&&!O,_=j[0]>j[1]?0:j[1]>j[0]?1:void 0;return t=E,(0,u.default)(".game",{style:{width:"760px",margin:"0 auto"}},m.map((function(e,t){return(0,u.default)(".player",{style:{position:"relative",height:"40px",width:"380px",borderRadius:"20px",background:P||g!==t||T?!P&&t===g&&0===t||P&&t===_&&0===t?"#000":!P&&t===g&&1===t||P&&t===_&&1===t?"#fff":"transparent":"#fc0",color:P||g!==t||T?!P&&t===g&&0===t||P&&0===_&&0===t?"#fff":!P&&t===g&&1===t||P&&_&&1===t?"#000":"inherit":"#000",margin:"0 0 15px",float:"left",transition:"background-color .2s .8s, color .2s .8s"}},(0,u.default)(".piece",{style:{width:"16px",height:"16px",borderRadius:"16px",border:"1px solid #999",position:"absolute",top:"11px",left:"15px",background:e.colour}}),(0,u.default)(".playerText",{style:{position:"absolute",left:"40px",top:"6.5px"}},e.name," (",j[t],") ",t===g&&T&&" to play ",P&&_===t&&(0,u.default)("b"," wins "),P&&void 0===_&&(0,u.default)("b"," draws "),t===g&&!T&&O&&[(0,u.default)("b"," can’t play ")," — ",(0,u.default)(u.default.route.Link,{href:h,params:o(o({},r.attrs),{turnStr:z})},"Pass")]),(0,u.default)("label",{style:{position:"absolute",right:"18px",top:"6.5px"}},(0,u.default)("input[type=checkbox]",{checked:d[0===t?"ai0":"ai1"],onchange:function(){var e,n=b(o(o({},d),((e={})["ai".concat(t)]=!d[0===t?"ai0":"ai1"],e)));u.default.route.set(h,o(o({},r.attrs),{flagsCode:n}))}})," AI"))})),(0,u.default)(".board",{style:{background:"#372",width:"720px",height:"720px",borderRadius:"55px",margin:"10px 0 25px",padding:"20px",clear:"left"}},v.map((function(t,i){var a={width:"80px",height:"80px",borderRadius:"80px"},l=k(i),f=void 0===S?1:Math.sqrt(Math.pow(l.rightwards-S.rightwards,2)+Math.pow(l.downwards-S.downwards,2)),c=o(o({},a),{position:"absolute",backfaceVisibility:"hidden",top:t===s?"-20px":"0",zIndex:10,transition:i===y?"top .25s":"transform .5s ".concat(.15*(1+f*(A?1:0)),"s")});return(0,u.default)("div",{style:o(o({},a),{position:"relative",float:"left",margin:"5px",transition:"box-shadow .25s .25s, background .5s",background:i===e?"#f60":0===g?"rgba(0, 0, 0, .075)":"rgba(255, 255, 255, .075)",boxShadow:i===y?"0 0 12px #fff":"none",cursor:2===t?"pointer":"default",color:"#372",fontSize:"36px",textAlign:"center",lineHeight:"77px",fontWeight:"bold"}),onclick:function(){void 0===n&&(C(v,i,g,r)||v[i]!==s||(e=i,setTimeout(u.default.redraw,750)))}},d.gridNos&&[["A","B","C","D","E","F","G","H"][l.rightwards],l.downwards+1],(0,u.default)(".piece0",{style:o(o({},c),{transform:0===t?"rotateY(0deg)":1===t?"rotateY(180deg)":"rotateY(90deg)",background:t!==s?m[0].colour:"transparent"})}),(0,u.default)(".piece1",{style:o(o({},c),{transform:0===t?"rotateY(180deg)":1===t?"rotateY(0deg)":"rotateY(90deg)",background:t!==s?m[1].colour:"transparent"})}))}))),(0,u.default)("label",{style:{float:"right"}},(0,u.default)("input[type=checkbox]",{checked:d.gridNos,onchange:function(){var e=b(o(o({},d),{gridNos:!d.gridNos}));u.default.route.set(h,o(o({},r.attrs),{flagsCode:e}))}})," Named cells"),(0,u.default)(u.default.route.Link,{href:"/:flagsCode/".concat(p,"/-/0"),params:{flagsCode:c},style:{fontWeight:"bold"}},"Start again"),u.default.trust("   "),(0,u.default)("a",{href:"https://www.worldothello.org/about/about-othello/othello-rules/official-rules/english"},"How to play"),u.default.trust("   "),(0,u.default)("a",{href:"https://github.com/jawj/fliptiles",style:{color:"#bbb"}},"Code on GitHub"))}}}t.Fliptiles=z,u.default.route(document.body,v,((r={})[h]=z,r))}},t={};function n(r){var o=t[r];if(void 0!==o)return o.exports;var i=t[r]={exports:{}};return e[r].call(i.exports,i,i.exports,n),i.exports}n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),n(607)})(); -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jawj/fliptiles/f4b82be947089716c7ee99b6123316e2e840e005/docs/favicon.ico -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Fliptiles 14 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fliptiles", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "webpack.common.js", 6 | "scripts": { 7 | "start": "webpack serve --config webpack/webpack.dev.js --host 0.0.0.0 --progress --color", 8 | "build": "webpack --config webpack/webpack.prod.js" 9 | }, 10 | "author": "George MacKerron", 11 | "license": "ISC", 12 | "dependencies": { 13 | "mithril": "^2.0.4" 14 | }, 15 | "devDependencies": { 16 | "@types/mithril": "^2.0.6", 17 | "@typescript-eslint/eslint-plugin": "^5.30.0", 18 | "@typescript-eslint/parser": "^5.30.0", 19 | "eslint": "^8.18.0", 20 | "eslint-plugin-import": "^2.26.0", 21 | "ts-loader": "^9.3.1", 22 | "typescript": "^4.7.4", 23 | "webpack": "^5.16.0", 24 | "webpack-cli": "^4.10.0", 25 | "webpack-dev-server": "^4.9.3", 26 | "webpack-merge": "^5.7.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import m from 'mithril'; 2 | 3 | interface FliptilesAttrs { 4 | boardStr: string; 5 | lastPieceStr: string; 6 | turnStr: string; 7 | flagsCode: number; 8 | } 9 | 10 | interface Flags { 11 | gridNos: boolean; 12 | ai0: boolean; 13 | ai1: boolean; 14 | } 15 | 16 | interface Position { // can also represent a vector movement 17 | rightwards: number; 18 | downwards: number; 19 | } 20 | 21 | type Board = (0 | 1 | typeof x)[]; 22 | 23 | const 24 | x = 2, // signifies blank square: **MUST BE 2** because we use base 3 to serialize 25 | initialBoard: Board = [ 26 | x, x, x, x, x, x, x, x, 27 | x, x, x, x, x, x, x, x, 28 | x, x, x, x, x, x, x, x, 29 | x, x, x, 1, 0, x, x, x, 30 | x, x, x, 0, 1, x, x, x, 31 | x, x, x, x, x, x, x, x, 32 | x, x, x, x, x, x, x, x, 33 | x, x, x, x, x, x, x, x, 34 | ], 35 | codeChars = `234567bcdfghjkmnpqrstvwxyz-`, 36 | codeCharHash = Object.fromEntries(codeChars.split('').map((x, i) => [x, i])), 37 | initialBoardStr = stringFromBoard(initialBoard), 38 | routeTemplate = '/:flagsCode/:boardStr/:lastPieceStr/:turnStr', 39 | defaultRoute = `/0/${initialBoardStr}/-/0`, // = no cell names, initial board, no previous piece, black to start 40 | players = [ 41 | { name: 'Black', colour: '#000' }, 42 | { name: 'White', colour: '#fff' }, 43 | ], 44 | directions = [-1, 0, 1].flatMap(d => (d === 0 ? [-1, 1] : [-1, 0, 1]).map(r => ({ downwards: d, rightwards: r }))); 45 | 46 | function stringFromBoard(board: Board) { 47 | return (board.join('') + '22').match(/.{1,3}/g)!.map(x => codeChars.charAt(parseInt(x, 3))).join(''); 48 | } 49 | 50 | function boardFromString(s: string) { 51 | return s.split('').flatMap(x => (27 + codeCharHash[x]).toString(3).slice(-3).split('').map(Number)).slice(0, 64) as Board; 52 | } 53 | 54 | function encodeFlags(flags: Flags) { 55 | return (flags.gridNos ? 1 : 0) * 0b001 + (flags.ai0 ? 1 : 0) * 0b010 + (flags.ai1 ? 1 : 0) * 0b100; 56 | } 57 | 58 | function decodeFlags(flags: number): Flags { 59 | return { gridNos: !!(flags & 0b001), ai0: !!(flags & 0b010), ai1: !!(flags & 0b100) }; 60 | } 61 | 62 | function positionFromPieceIndex(pieceIndex: number): Position | undefined { 63 | if (pieceIndex < 0 || pieceIndex >= 64) return; 64 | return { downwards: Math.floor(pieceIndex / 8), rightwards: pieceIndex % 8 }; 65 | } 66 | 67 | function pieceIndexFromPosition(position: Position): number | undefined { 68 | if (position.downwards < 0 || position.downwards >= 8 || position.rightwards < 0 || position.rightwards >= 8) return; 69 | return position.downwards * 8 + position.rightwards; 70 | } 71 | 72 | function addPosition(p1: Position, p2: Position) { 73 | p1.rightwards += p2.rightwards; 74 | p1.downwards += p2.downwards; 75 | } 76 | 77 | function flippableOpponentPiecesByDirection(board: Board, position: Position, player: 0 | 1) { 78 | const opponent = 1 - player; 79 | 80 | return directions.map(direction => { 81 | let opponentPieces = 0; 82 | const currentPosition = { ...position }; 83 | 84 | for (; ;) { 85 | addPosition(currentPosition, direction); 86 | const currentIndex = pieceIndexFromPosition(currentPosition)!; 87 | if (board[currentIndex] === opponent) opponentPieces += 1; 88 | else if (board[currentIndex] === player) return opponentPieces; 89 | else return 0; 90 | } 91 | }); 92 | } 93 | 94 | function flipPiecesByDirections(board: Board, position: Position, pieceCounts: number[]) { 95 | for (let i = 0; i < directions.length; i++) { 96 | const 97 | direction = directions[i], 98 | currentPosition = { ...position }; 99 | 100 | for (let j = 0; j < pieceCounts[i]; j++) { 101 | addPosition(currentPosition, direction); 102 | const pieceIndex = pieceIndexFromPosition(currentPosition)!; 103 | board[pieceIndex] = 1 - board[pieceIndex] as 0 | 1; 104 | } 105 | } 106 | } 107 | 108 | function boardByPlayingPieceAtIndex(board: Board, pieceIndex: number, player: 0 | 1) { 109 | const currentPiece = board[pieceIndex]; 110 | if (currentPiece !== x) return; // can't play where there's already a piece 111 | 112 | const 113 | position = positionFromPieceIndex(pieceIndex)!, 114 | flippablesByDirection = flippableOpponentPiecesByDirection(board, position, player), 115 | flippablesCount = flippablesByDirection.reduce((memo, n) => memo + n); 116 | 117 | if (flippablesCount === 0) return; // can't play if nothing gets flipped 118 | 119 | const newBoard = [...board]; 120 | newBoard[pieceIndex] = player; 121 | flipPiecesByDirections(newBoard, position, flippablesByDirection); 122 | 123 | return newBoard; 124 | } 125 | 126 | function playAtPieceIndex(board: Board, pieceIndex: number, player: 0 | 1, vnode: m.Vnode) { 127 | const newBoard = boardByPlayingPieceAtIndex(board, pieceIndex, player); 128 | if (!Array.isArray(newBoard)) return false; 129 | 130 | m.route.set(routeTemplate, { ...vnode.attrs, boardStr: stringFromBoard(newBoard), lastPieceStr: pieceIndex, turnStr: 1 - player }); 131 | return true; 132 | } 133 | 134 | function playerCanPlay(board: Board, player: 0 | 1) { 135 | return board.some((piece, pieceIndex) => 136 | piece === x ? flippableOpponentPiecesByDirection(board, positionFromPieceIndex(pieceIndex)!, player).reduce((memo, n) => memo + n) > 0 : false); 137 | } 138 | 139 | function piecesByPlayer(board: Board) { 140 | return board.reduce((memo, piece) => { memo[piece] += 1; return memo; }, [0, 0, 0]); 141 | } 142 | 143 | function boardScoreForPlayer(board: Board, player: 0 | 1, cornerScore = 12, edgeScore = 4, otherScore = 1) { 144 | return board.reduce((memo: number, piece, i) => 145 | memo + (piece !== player ? 0 : 146 | i === 0 || i === 7 || i === 56 || i === 63 ? cornerScore : 147 | i <= 7 || i >= 56 || i % 8 === 0 || i % 8 === 7 ? edgeScore : otherScore), 0); 148 | } 149 | 150 | function suggestMoves(board: Board, player: 0 | 1) { 151 | const opponent = 1 - player as 0 | 1; 152 | let 153 | bestWorstCaseScore = -Infinity, 154 | bestMoves: number[] = []; 155 | 156 | for (let i = 0; i < 64; i++) { 157 | const board1 = boardByPlayingPieceAtIndex(board, i, player); 158 | if (!Array.isArray(board1)) continue; 159 | 160 | // the tie-break score represents how good the board is for us straight away 161 | const tieBreakScore = (boardScoreForPlayer(board1, player) - boardScoreForPlayer(board1, opponent)) / 100; 162 | 163 | let worstCaseScore = Infinity; 164 | for (let j = 0; j < 64; j++) { 165 | const board2 = boardByPlayingPieceAtIndex(board1, j, opponent); 166 | if (!Array.isArray(board2)) continue; 167 | // subtracting opponent score isn't redundant, because of edge and corner boosts 168 | const score = boardScoreForPlayer(board2, player) - boardScoreForPlayer(board2, opponent) + tieBreakScore; 169 | if (score < worstCaseScore) worstCaseScore = score; 170 | } 171 | 172 | if (worstCaseScore === bestWorstCaseScore) bestMoves.push(i); 173 | else if (worstCaseScore > bestWorstCaseScore) { 174 | bestWorstCaseScore = worstCaseScore; 175 | bestMoves = [i]; 176 | } 177 | } 178 | 179 | return bestMoves; 180 | } 181 | 182 | export function Fliptiles() { 183 | let 184 | errorIndex: number | undefined, 185 | prevBlanks: number | undefined, 186 | aiTimeout: ReturnType | undefined; 187 | 188 | function afterDraw(vnode: m.Vnode) { 189 | errorIndex = undefined; // clear error appearance on next redraw 190 | 191 | if (aiTimeout !== undefined) clearTimeout(aiTimeout); // don't stack up AI moves 192 | aiTimeout = undefined; 193 | 194 | const 195 | { boardStr, turnStr, flagsCode } = vnode.attrs, 196 | turnForPlayer = Number(turnStr) as 0 | 1, 197 | flags = decodeFlags(flagsCode); 198 | 199 | if ((turnForPlayer === 0 && flags.ai0) || (turnForPlayer === 1 && flags.ai1)) { 200 | const 201 | board = boardFromString(boardStr), 202 | suggestedMoves = suggestMoves(board, turnForPlayer), 203 | suggestedMove = suggestedMoves[Math.floor(Math.random() * suggestedMoves.length)]; 204 | 205 | if (suggestedMove !== undefined) aiTimeout = setTimeout(() => playAtPieceIndex(board, suggestedMove, turnForPlayer, vnode), 2000); 206 | } 207 | } 208 | 209 | return { 210 | oncreate: afterDraw, 211 | onupdate: afterDraw, 212 | view: (vnode: m.Vnode) => { 213 | const 214 | { boardStr, turnStr, lastPieceStr, flagsCode } = vnode.attrs, 215 | flags = decodeFlags(flagsCode), 216 | board = boardFromString(boardStr), 217 | turnForPlayer = Number(turnStr) as 0 | 1, 218 | lastPieceIndex = lastPieceStr === '-' ? undefined : Number(lastPieceStr), 219 | lastPiecePosition = positionFromPieceIndex(lastPieceIndex ?? -1), 220 | piecesPerPlayer = piecesByPlayer(board), 221 | blanks = piecesPerPlayer[x], 222 | ordinaryMove = prevBlanks === undefined || blanks === prevBlanks - 1, // for animation purposes 223 | canPlay = playerCanPlay(board, turnForPlayer), 224 | opponent = 1 - turnForPlayer as 0 | 1, 225 | opponentCanPlay = !canPlay && playerCanPlay(board, opponent), 226 | gameOver = !canPlay && !opponentCanPlay, 227 | winning = piecesPerPlayer[0] > piecesPerPlayer[1] ? 0 : 228 | piecesPerPlayer[1] > piecesPerPlayer[0] ? 1 : undefined; 229 | 230 | prevBlanks = blanks; 231 | 232 | return m('.game', 233 | { style: { width: '760px', margin: '0 auto' } }, 234 | players.map((player, playerIndex) => m('.player', 235 | { 236 | style: { 237 | position: 'relative', 238 | height: '40px', 239 | width: '380px', 240 | borderRadius: '20px', 241 | background: !gameOver && turnForPlayer === playerIndex && !canPlay ? '#fc0' : 242 | (!gameOver && playerIndex === turnForPlayer && playerIndex === 0) || (gameOver && playerIndex === winning && playerIndex === 0) ? '#000' : 243 | (!gameOver && playerIndex === turnForPlayer && playerIndex === 1) || (gameOver && playerIndex === winning && playerIndex === 1) ? '#fff' : 'transparent', 244 | color: !gameOver && turnForPlayer === playerIndex && !canPlay ? '#000' : 245 | (!gameOver && playerIndex === turnForPlayer && playerIndex === 0) || (gameOver && winning === 0 && playerIndex === 0) ? '#fff' : 246 | (!gameOver && playerIndex === turnForPlayer && playerIndex === 1) || (gameOver && winning && playerIndex === 1) ? '#000' : 'inherit', 247 | margin: '0 0 15px', 248 | float: 'left', 249 | transition: 'background-color .2s .8s, color .2s .8s', 250 | } 251 | }, 252 | m('.piece', { 253 | style: { 254 | width: '16px', height: '16px', 255 | borderRadius: '16px', 256 | border: '1px solid #999', 257 | position: 'absolute', 258 | top: '11px', 259 | left: '15px', 260 | background: player.colour 261 | } 262 | }), 263 | m('.playerText', 264 | { style: { position: 'absolute', left: '40px', top: '6.5px', } }, 265 | player.name, ' (', piecesPerPlayer[playerIndex], ') ', 266 | playerIndex === turnForPlayer && canPlay && ` to play `, 267 | gameOver && winning === playerIndex && m('b', ' wins '), 268 | gameOver && winning === undefined && m('b', ' draws '), 269 | playerIndex === turnForPlayer && !canPlay && opponentCanPlay && [ 270 | m('b', ` can’t play `), ' — ', 271 | m(m.route.Link, { 272 | href: routeTemplate, 273 | params: { ...vnode.attrs, turnStr: opponent }, 274 | }, `Pass`)], 275 | ), 276 | m('label', 277 | { 278 | style: { 279 | position: 'absolute', 280 | right: '18px', 281 | top: '6.5px', 282 | } 283 | }, 284 | m('input[type=checkbox]', { 285 | checked: flags[playerIndex === 0 ? 'ai0' : 'ai1'], 286 | onchange: () => { 287 | const newFlagsCode = encodeFlags({ 288 | ...flags, 289 | [`ai${playerIndex}`]: !flags[playerIndex === 0 ? 'ai0' : 'ai1'], 290 | }); 291 | m.route.set(routeTemplate, { ...vnode.attrs, flagsCode: newFlagsCode }); 292 | } 293 | }), 294 | ' AI' 295 | ), 296 | )), 297 | m('.board', 298 | { 299 | style: { 300 | background: '#372', 301 | width: '720px', height: '720px', 302 | borderRadius: '55px', 303 | margin: '10px 0 25px', 304 | padding: '20px', 305 | clear: 'left', 306 | } 307 | }, 308 | board.map((playerIndex, pieceIndex) => { 309 | const 310 | commonPieceSizeStyle = { width: '80px', height: '80px', borderRadius: '80px' }, 311 | piecePosition = positionFromPieceIndex(pieceIndex)!, 312 | distanceFromLastPlayed = lastPiecePosition === undefined ? 1 : Math.sqrt( 313 | Math.pow(piecePosition.rightwards - lastPiecePosition.rightwards, 2) + 314 | Math.pow(piecePosition.downwards - lastPiecePosition.downwards, 2) 315 | ), 316 | commonPieceStyle = { 317 | ...commonPieceSizeStyle, position: 'absolute', backfaceVisibility: 'hidden', 318 | top: playerIndex === x ? '-20px' : '0', zIndex: 10, 319 | transition: pieceIndex === lastPieceIndex ? 'top .25s' : 320 | `transform .5s ${.15 * (1 + distanceFromLastPlayed * (ordinaryMove ? 1 : 0))}s` 321 | }; 322 | 323 | return m('div', 324 | { 325 | style: { 326 | ...commonPieceSizeStyle, 327 | position: 'relative', 328 | float: 'left', 329 | margin: '5px', 330 | transition: `box-shadow .25s .25s, background .5s`, 331 | background: pieceIndex === errorIndex ? '#f60' : 332 | turnForPlayer === 0 ? 'rgba(0, 0, 0, .075)' : 'rgba(255, 255, 255, .075)', 333 | boxShadow: pieceIndex === lastPieceIndex ? '0 0 12px #fff' : 'none', 334 | cursor: playerIndex === 2 ? 'pointer' : 'default', 335 | color: '#372', 336 | fontSize: '36px', 337 | textAlign: 'center', 338 | lineHeight: '77px', 339 | fontWeight: 'bold', 340 | }, 341 | onclick: () => { 342 | if (aiTimeout !== undefined) return; 343 | const success = playAtPieceIndex(board, pieceIndex, turnForPlayer, vnode); 344 | if (!success && board[pieceIndex] === x) { 345 | errorIndex = pieceIndex; 346 | setTimeout(m.redraw, 750); 347 | } 348 | } 349 | }, 350 | flags.gridNos && [ 351 | ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'][piecePosition.rightwards], 352 | piecePosition.downwards + 1, 353 | ], 354 | m('.piece0', { 355 | style: { 356 | ...commonPieceStyle, 357 | transform: playerIndex === 0 ? 'rotateY(0deg)' : playerIndex === 1 ? 'rotateY(180deg)' : 'rotateY(90deg)', 358 | background: playerIndex !== x ? players[0].colour : 'transparent', 359 | } 360 | }), 361 | m('.piece1', { 362 | style: { 363 | ...commonPieceStyle, 364 | transform: playerIndex === 0 ? 'rotateY(180deg)' : playerIndex === 1 ? 'rotateY(0deg)' : 'rotateY(90deg)', 365 | background: playerIndex !== x ? players[1].colour : 'transparent', 366 | } 367 | }), 368 | ); 369 | }) 370 | ), 371 | m('label', 372 | { style: { float: 'right' } }, 373 | m('input[type=checkbox]', { 374 | checked: flags.gridNos, 375 | onchange: () => { 376 | const newFlagsCode = encodeFlags({ ...flags, gridNos: !flags.gridNos }); 377 | m.route.set(routeTemplate, { ...vnode.attrs, flagsCode: newFlagsCode }); 378 | } 379 | }), 380 | ' Named cells' 381 | ), 382 | m(m.route.Link, { href: `/:flagsCode/${initialBoardStr}/-/0`, params: { flagsCode }, style: { fontWeight: 'bold' } }, 'Start again'), 383 | m.trust('   '), 384 | m('a', { href: 'https://www.worldothello.org/about/about-othello/othello-rules/official-rules/english' }, 'How to play'), 385 | m.trust('   '), 386 | m('a', { href: 'https://github.com/jawj/fliptiles', style: { color: '#bbb' } }, 'Code on GitHub'), 387 | ); 388 | } 389 | }; 390 | } 391 | 392 | m.route(document.body, defaultRoute, { [routeTemplate]: Fliptiles }); 393 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ], 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": [ 8 | "es2019", 9 | "dom" 10 | ], 11 | "strict": true, 12 | "sourceMap": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "esModuleInterop": true, 15 | "downlevelIteration": true 16 | } 17 | } -------------------------------------------------------------------------------- /webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/index.ts', 5 | module: { 6 | rules: [{ test: /\.tsx?$/, loader: 'ts-loader' }] 7 | }, 8 | resolve: { 9 | extensions: ['.ts', '.tsx', '.js'] 10 | }, 11 | output: { 12 | filename: 'app.js', 13 | path: path.resolve(__dirname, '..', 'docs') 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | 2 | const 3 | path = require('path'), 4 | { merge } = require('webpack-merge'), 5 | common = require('./webpack.common.js'); 6 | 7 | module.exports = merge(common, { 8 | mode: 'development', 9 | devtool: 'inline-source-map', 10 | devServer: { 11 | static: { 12 | directory: path.join(__dirname, '..', 'docs'), 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const 2 | { merge } = require('webpack-merge'), 3 | common = require('./webpack.common.js'); 4 | 5 | module.exports = merge(common, { 6 | mode: 'production' 7 | }); 8 | --------------------------------------------------------------------------------