├── .browserslistrc
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── README.md
├── babel.config.js
├── jsconfig.json
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── css
│ └── font-awesome.min.css
├── favicon.png
├── fonts
│ ├── fontawesome-webfont.eot
│ ├── fontawesome-webfont.svg
│ ├── fontawesome-webfont.ttf
│ ├── fontawesome-webfont.woff
│ └── fontawesome-webfont.woff2
├── icon.png
├── index.html
└── lib
│ ├── gif.js
│ ├── gif.js.map
│ ├── gif.worker.js
│ └── gif.worker.js.map
├── src
├── App.vue
├── ai
│ ├── engine-warpper.worker.js
│ └── engine.js
├── components
│ ├── Bestline.vue
│ └── Board.vue
├── i18n.js
├── locales
│ ├── en.json
│ ├── ja.json
│ ├── ko.json
│ ├── ru.json
│ ├── vi.json
│ ├── zh-CN.json
│ └── zh-TW.json
├── main.js
├── router.js
├── store
│ ├── index.js
│ └── modules
│ │ ├── ai.js
│ │ ├── position.js
│ │ └── settings.js
├── theme.less
└── views
│ ├── About-en.md
│ ├── About-zh-CN.md
│ ├── About.vue
│ ├── Game.vue
│ ├── Home.vue
│ └── Settings.vue
└── vue.config.js
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | browser: true
6 | },
7 | extends: ['plugin:vue/essential', 'eslint:recommended'],
8 | rules: {
9 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
10 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
11 | },
12 | parserOptions: {
13 | parser: 'babel-eslint',
14 | ecmaVersion: 6
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
4 |
5 | # Do not store the built data
6 | public/build
7 |
8 | # local env files
9 | .env.local
10 | .env.*.local
11 |
12 | # Log files
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
17 | # Editor directories and files
18 | .idea
19 | .vscode
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "printWidth": 100,
4 | "endOfLine": "auto",
5 | "semi": false,
6 | "singleQuote": true
7 | }
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gomoku-Calculator
2 |
3 | ## Project setup
4 | ```
5 | npm install
6 | ```
7 |
8 | ### Compiles and hot-reloads for development
9 | ```
10 | npm run serve
11 | ```
12 |
13 | ### Compiles and minifies for production
14 | ```
15 | npm run build
16 | ```
17 |
18 | ### Lints and fixes files
19 | ```
20 | npm run lint
21 | ```
22 |
23 | ### Prettify source files
24 | ```
25 | npm run pretty
26 | ```
27 |
28 | ### Customize configuration
29 | See [Configuration Reference](https://cli.vuejs.org/config/).
30 |
31 |
32 | ## Deployment
33 | After running `npm run build`, just copy all files under 'dist' to your static file server.
34 |
35 | Since browsers require stricter environment when enabling SharedArrayBuffer feature at this moment, [COEP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy) is required if multi-threading is needed. Add these two headers to the whole served site (including 'index.html' and javascript files under 'dist/build' directory):
36 | ```
37 | Cross-Origin-Embedder-Policy: require-corp
38 | Cross-Origin-Opener-Policy: same-origin
39 | ```
40 | It is also recommended to set correct MIME type (`application/wasm`) for the wasm files under 'dist/build' directory.
41 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@vue/app']
3 | }
4 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["./src/**/*"]
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gomoku-calculator",
3 | "version": "0.30.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint",
9 | "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'",
10 | "pretty": "prettier --write \"./src/**/*.{js,vue,json}\" \"./public/index.html\""
11 | },
12 | "dependencies": {
13 | "core-js": "^2.6.5",
14 | "dynamic-import": "^0.1.1",
15 | "es6-promise": "^4.2.8",
16 | "github-markdown-css": "^3.0.1",
17 | "register-service-worker": "^1.7.1",
18 | "throttle-debounce": "^2.3.0",
19 | "vue": "^2.6.10",
20 | "vue-i18n": "^8.0.0",
21 | "vue-router": "^3.0.3",
22 | "vuex": "^3.1.1",
23 | "vux": "^2.10.1",
24 | "wasm-feature-detect": "^1.8.0"
25 | },
26 | "devDependencies": {
27 | "@vue/cli-plugin-babel": "^3.8.0",
28 | "@vue/cli-plugin-eslint": "^3.8.0",
29 | "@vue/cli-plugin-pwa": "~4.5.7",
30 | "@vue/cli-service": "^3.8.0",
31 | "babel-eslint": "^10.0.1",
32 | "eslint": "^5.16.0",
33 | "eslint-plugin-vue": "^5.2.3",
34 | "less": "^3.9.0",
35 | "less-loader": "^5.0.0",
36 | "vue-cli-plugin-i18n": "^0.6.0",
37 | "vue-loader": "^14.2.2",
38 | "vue-template-compiler": "^2.6.10",
39 | "vux-loader": "^1.2.9",
40 | "worker-loader": "^3.0.8",
41 | "yaml-loader": "^0.5.0"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhbloo/gomoku-calculator/99572508c2fcb7bb4310d64bf9e6687677b6e4cb/public/favicon.png
--------------------------------------------------------------------------------
/public/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhbloo/gomoku-calculator/99572508c2fcb7bb4310d64bf9e6687677b6e4cb/public/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/public/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhbloo/gomoku-calculator/99572508c2fcb7bb4310d64bf9e6687677b6e4cb/public/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/public/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhbloo/gomoku-calculator/99572508c2fcb7bb4310d64bf9e6687677b6e4cb/public/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/public/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhbloo/gomoku-calculator/99572508c2fcb7bb4310d64bf9e6687677b6e4cb/public/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dhbloo/gomoku-calculator/99572508c2fcb7bb4310d64bf9e6687677b6e4cb/public/icon.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | 五子棋计算器 Gomoku Calculator
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/public/lib/gif.js:
--------------------------------------------------------------------------------
1 | // gif.js 0.2.0 - https://github.com/jnordberg/gif.js
2 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.GIF=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o0&&this._events[type].length>m){this._events[type].warned=true;console.error("(node) warning: possible EventEmitter memory "+"leak detected. %d listeners added. "+"Use emitter.setMaxListeners() to increase limit.",this._events[type].length);if(typeof console.trace==="function"){console.trace()}}}return this};EventEmitter.prototype.on=EventEmitter.prototype.addListener;EventEmitter.prototype.once=function(type,listener){if(!isFunction(listener))throw TypeError("listener must be a function");var fired=false;function g(){this.removeListener(type,g);if(!fired){fired=true;listener.apply(this,arguments)}}g.listener=listener;this.on(type,g);return this};EventEmitter.prototype.removeListener=function(type,listener){var list,position,length,i;if(!isFunction(listener))throw TypeError("listener must be a function");if(!this._events||!this._events[type])return this;list=this._events[type];length=list.length;position=-1;if(list===listener||isFunction(list.listener)&&list.listener===listener){delete this._events[type];if(this._events.removeListener)this.emit("removeListener",type,listener)}else if(isObject(list)){for(i=length;i-- >0;){if(list[i]===listener||list[i].listener&&list[i].listener===listener){position=i;break}}if(position<0)return this;if(list.length===1){list.length=0;delete this._events[type]}else{list.splice(position,1)}if(this._events.removeListener)this.emit("removeListener",type,listener)}return this};EventEmitter.prototype.removeAllListeners=function(type){var key,listeners;if(!this._events)return this;if(!this._events.removeListener){if(arguments.length===0)this._events={};else if(this._events[type])delete this._events[type];return this}if(arguments.length===0){for(key in this._events){if(key==="removeListener")continue;this.removeAllListeners(key)}this.removeAllListeners("removeListener");this._events={};return this}listeners=this._events[type];if(isFunction(listeners)){this.removeListener(type,listeners)}else if(listeners){while(listeners.length)this.removeListener(type,listeners[listeners.length-1])}delete this._events[type];return this};EventEmitter.prototype.listeners=function(type){var ret;if(!this._events||!this._events[type])ret=[];else if(isFunction(this._events[type]))ret=[this._events[type]];else ret=this._events[type].slice();return ret};EventEmitter.prototype.listenerCount=function(type){if(this._events){var evlistener=this._events[type];if(isFunction(evlistener))return 1;else if(evlistener)return evlistener.length}return 0};EventEmitter.listenerCount=function(emitter,type){return emitter.listenerCount(type)};function isFunction(arg){return typeof arg==="function"}function isNumber(arg){return typeof arg==="number"}function isObject(arg){return typeof arg==="object"&&arg!==null}function isUndefined(arg){return arg===void 0}},{}],2:[function(require,module,exports){var UA,browser,mode,platform,ua;ua=navigator.userAgent.toLowerCase();platform=navigator.platform.toLowerCase();UA=ua.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/)||[null,"unknown",0];mode=UA[1]==="ie"&&document.documentMode;browser={name:UA[1]==="version"?UA[3]:UA[1],version:mode||parseFloat(UA[1]==="opera"&&UA[4]?UA[4]:UA[2]),platform:{name:ua.match(/ip(?:ad|od|hone)/)?"ios":(ua.match(/(?:webos|android)/)||platform.match(/mac|win|linux/)||["other"])[0]}};browser[browser.name]=true;browser[browser.name+parseInt(browser.version,10)]=true;browser.platform[browser.platform.name]=true;module.exports=browser},{}],3:[function(require,module,exports){var EventEmitter,GIF,browser,extend=function(child,parent){for(var key in parent){if(hasProp.call(parent,key))child[key]=parent[key]}function ctor(){this.constructor=child}ctor.prototype=parent.prototype;child.prototype=new ctor;child.__super__=parent.prototype;return child},hasProp={}.hasOwnProperty,indexOf=[].indexOf||function(item){for(var i=0,l=this.length;iref;i=0<=ref?++j:--j){results.push(null)}return results}.call(this);numWorkers=this.spawnWorkers();if(this.options.globalPalette===true){this.renderNextFrame()}else{for(i=j=0,ref=numWorkers;0<=ref?jref;i=0<=ref?++j:--j){this.renderNextFrame()}}this.emit("start");return this.emit("progress",0)};GIF.prototype.abort=function(){var worker;while(true){worker=this.activeWorkers.shift();if(worker==null){break}this.log("killing active worker");worker.terminate()}this.running=false;return this.emit("abort")};GIF.prototype.spawnWorkers=function(){var j,numWorkers,ref,results;numWorkers=Math.min(this.options.workers,this.frames.length);(function(){results=[];for(var j=ref=this.freeWorkers.length;ref<=numWorkers?jnumWorkers;ref<=numWorkers?j++:j--){results.push(j)}return results}).apply(this).forEach(function(_this){return function(i){var worker;_this.log("spawning worker "+i);worker=new Worker(_this.options.workerScript);worker.onmessage=function(event){_this.activeWorkers.splice(_this.activeWorkers.indexOf(worker),1);_this.freeWorkers.push(worker);return _this.frameFinished(event.data)};return _this.freeWorkers.push(worker)}}(this));return numWorkers};GIF.prototype.frameFinished=function(frame){var i,j,ref;this.log("frame "+frame.index+" finished - "+this.activeWorkers.length+" active");this.finishedFrames++;this.emit("progress",this.finishedFrames/this.frames.length);this.imageParts[frame.index]=frame;if(this.options.globalPalette===true){this.options.globalPalette=frame.globalPalette;this.log("global palette analyzed");if(this.frames.length>2){for(i=j=1,ref=this.freeWorkers.length;1<=ref?jref;i=1<=ref?++j:--j){this.renderNextFrame()}}}if(indexOf.call(this.imageParts,null)>=0){return this.renderNextFrame()}else{return this.finishRendering()}};GIF.prototype.finishRendering=function(){var data,frame,i,image,j,k,l,len,len1,len2,len3,offset,page,ref,ref1,ref2;len=0;ref=this.imageParts;for(j=0,len1=ref.length;j=this.frames.length){return}frame=this.frames[this.nextFrame++];worker=this.freeWorkers.shift();task=this.getTask(frame);this.log("starting frame "+(task.index+1)+" of "+this.frames.length);this.activeWorkers.push(worker);return worker.postMessage(task)};GIF.prototype.getContextData=function(ctx){return ctx.getImageData(0,0,this.options.width,this.options.height).data};GIF.prototype.getImageData=function(image){var ctx;if(this._canvas==null){this._canvas=document.createElement("canvas");this._canvas.width=this.options.width;this._canvas.height=this.options.height}ctx=this._canvas.getContext("2d");ctx.setFill=this.options.background;ctx.fillRect(0,0,this.options.width,this.options.height);ctx.drawImage(image,0,0);return this.getContextData(ctx)};GIF.prototype.getTask=function(frame){var index,task;index=this.frames.indexOf(frame);task={index:index,last:index===this.frames.length-1,delay:frame.delay,transparent:frame.transparent,width:this.options.width,height:this.options.height,quality:this.options.quality,dither:this.options.dither,globalPalette:this.options.globalPalette,repeat:this.options.repeat,canTransfer:browser.name==="chrome"};if(frame.data!=null){task.data=frame.data}else if(frame.context!=null){task.data=this.getContextData(frame.context)}else if(frame.image!=null){task.data=this.getImageData(frame.image)}else{throw new Error("Invalid frame")}return task};GIF.prototype.log=function(){var args;args=1<=arguments.length?slice.call(arguments,0):[];if(!this.options.debug){return}return console.log.apply(console,args)};return GIF}(EventEmitter);module.exports=GIF},{"./browser.coffee":2,events:1}]},{},[3])(3)});
3 | //# sourceMappingURL=gif.js.map
4 |
--------------------------------------------------------------------------------
/public/lib/gif.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["node_modules/browser-pack/_prelude.js","node_modules/events/events.js","src/browser.coffee","src/gif.coffee"],"names":["f","exports","module","define","amd","g","window","global","self","this","GIF","e","t","n","r","s","o","u","a","require","i","Error","code","l","call","length","1","EventEmitter","_events","_maxListeners","undefined","prototype","defaultMaxListeners","setMaxListeners","isNumber","isNaN","TypeError","emit","type","er","handler","len","args","listeners","error","isObject","arguments","err","context","isUndefined","isFunction","Array","slice","apply","addListener","listener","m","newListener","push","warned","console","trace","on","once","fired","removeListener","list","position","splice","removeAllListeners","key","ret","listenerCount","evlistener","emitter","arg","UA","browser","mode","platform","ua","navigator","userAgent","toLowerCase","match","document","documentMode","name","version","parseFloat","parseInt","extend","child","parent","hasProp","ctor","constructor","__super__","superClass","defaults","frameDefaults","workerScript","workers","repeat","background","quality","width","height","transparent","debug","dither","delay","copy","options","base","value","running","frames","freeWorkers","activeWorkers","setOptions","setOption","_canvas","results","addFrame","image","frame","ImageData","data","CanvasRenderingContext2D","WebGLRenderingContext","getContextData","childNodes","getImageData","render","j","numWorkers","ref","nextFrame","finishedFrames","imageParts","spawnWorkers","globalPalette","renderNextFrame","abort","worker","shift","log","terminate","Math","min","forEach","_this","Worker","onmessage","event","indexOf","frameFinished","index","finishRendering","k","len1","len2","len3","offset","page","ref1","ref2","pageSize","cursor","round","Uint8Array","set","Blob","task","getTask","postMessage","ctx","createElement","getContext","setFill","fillRect","drawImage","last","canTransfer"],"mappings":"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","sourceRoot":"","sourcesContent":["(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require==\"function\"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error(\"Cannot find module '\"+o+\"'\");throw f.code=\"MODULE_NOT_FOUND\",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require==\"function\"&&require;for(var o=0;o 0 && this._events[type].length > m) {\n this._events[type].warned = true;\n console.error('(node) warning: possible EventEmitter memory ' +\n 'leak detected. %d listeners added. ' +\n 'Use emitter.setMaxListeners() to increase limit.',\n this._events[type].length);\n if (typeof console.trace === 'function') {\n // not supported in IE 10\n console.trace();\n }\n }\n }\n\n return this;\n};\n\nEventEmitter.prototype.on = EventEmitter.prototype.addListener;\n\nEventEmitter.prototype.once = function(type, listener) {\n if (!isFunction(listener))\n throw TypeError('listener must be a function');\n\n var fired = false;\n\n function g() {\n this.removeListener(type, g);\n\n if (!fired) {\n fired = true;\n listener.apply(this, arguments);\n }\n }\n\n g.listener = listener;\n this.on(type, g);\n\n return this;\n};\n\n// emits a 'removeListener' event iff the listener was removed\nEventEmitter.prototype.removeListener = function(type, listener) {\n var list, position, length, i;\n\n if (!isFunction(listener))\n throw TypeError('listener must be a function');\n\n if (!this._events || !this._events[type])\n return this;\n\n list = this._events[type];\n length = list.length;\n position = -1;\n\n if (list === listener ||\n (isFunction(list.listener) && list.listener === listener)) {\n delete this._events[type];\n if (this._events.removeListener)\n this.emit('removeListener', type, listener);\n\n } else if (isObject(list)) {\n for (i = length; i-- > 0;) {\n if (list[i] === listener ||\n (list[i].listener && list[i].listener === listener)) {\n position = i;\n break;\n }\n }\n\n if (position < 0)\n return this;\n\n if (list.length === 1) {\n list.length = 0;\n delete this._events[type];\n } else {\n list.splice(position, 1);\n }\n\n if (this._events.removeListener)\n this.emit('removeListener', type, listener);\n }\n\n return this;\n};\n\nEventEmitter.prototype.removeAllListeners = function(type) {\n var key, listeners;\n\n if (!this._events)\n return this;\n\n // not listening for removeListener, no need to emit\n if (!this._events.removeListener) {\n if (arguments.length === 0)\n this._events = {};\n else if (this._events[type])\n delete this._events[type];\n return this;\n }\n\n // emit removeListener for all listeners on all events\n if (arguments.length === 0) {\n for (key in this._events) {\n if (key === 'removeListener') continue;\n this.removeAllListeners(key);\n }\n this.removeAllListeners('removeListener');\n this._events = {};\n return this;\n }\n\n listeners = this._events[type];\n\n if (isFunction(listeners)) {\n this.removeListener(type, listeners);\n } else if (listeners) {\n // LIFO order\n while (listeners.length)\n this.removeListener(type, listeners[listeners.length - 1]);\n }\n delete this._events[type];\n\n return this;\n};\n\nEventEmitter.prototype.listeners = function(type) {\n var ret;\n if (!this._events || !this._events[type])\n ret = [];\n else if (isFunction(this._events[type]))\n ret = [this._events[type]];\n else\n ret = this._events[type].slice();\n return ret;\n};\n\nEventEmitter.prototype.listenerCount = function(type) {\n if (this._events) {\n var evlistener = this._events[type];\n\n if (isFunction(evlistener))\n return 1;\n else if (evlistener)\n return evlistener.length;\n }\n return 0;\n};\n\nEventEmitter.listenerCount = function(emitter, type) {\n return emitter.listenerCount(type);\n};\n\nfunction isFunction(arg) {\n return typeof arg === 'function';\n}\n\nfunction isNumber(arg) {\n return typeof arg === 'number';\n}\n\nfunction isObject(arg) {\n return typeof arg === 'object' && arg !== null;\n}\n\nfunction isUndefined(arg) {\n return arg === void 0;\n}\n","### CoffeeScript version of the browser detection from MooTools ###\n\nua = navigator.userAgent.toLowerCase()\nplatform = navigator.platform.toLowerCase()\nUA = ua.match(/(opera|ie|firefox|chrome|version)[\\s\\/:]([\\w\\d\\.]+)?.*?(safari|version[\\s\\/:]([\\w\\d\\.]+)|$)/) or [null, 'unknown', 0]\nmode = UA[1] == 'ie' && document.documentMode\n\nbrowser =\n name: if UA[1] is 'version' then UA[3] else UA[1]\n version: mode or parseFloat(if UA[1] is 'opera' && UA[4] then UA[4] else UA[2])\n\n platform:\n name: if ua.match(/ip(?:ad|od|hone)/) then 'ios' else (ua.match(/(?:webos|android)/) or platform.match(/mac|win|linux/) or ['other'])[0]\n\nbrowser[browser.name] = true\nbrowser[browser.name + parseInt(browser.version, 10)] = true\nbrowser.platform[browser.platform.name] = true\n\nmodule.exports = browser\n","{EventEmitter} = require 'events'\nbrowser = require './browser.coffee'\n\nclass GIF extends EventEmitter\n\n defaults =\n workerScript: 'gif.worker.js'\n workers: 2\n repeat: 0 # repeat forever, -1 = repeat once\n background: '#fff'\n quality: 10 # pixel sample interval, lower is better\n width: null # size derermined from first frame if possible\n height: null\n transparent: null\n debug: false\n dither: false # see GIFEncoder.js for dithering options\n\n frameDefaults =\n delay: 500 # ms\n copy: false\n\n constructor: (options) ->\n @running = false\n\n @options = {}\n @frames = []\n\n @freeWorkers = []\n @activeWorkers = []\n\n @setOptions options\n for key, value of defaults\n @options[key] ?= value\n\n setOption: (key, value) ->\n @options[key] = value\n if @_canvas? and key in ['width', 'height']\n @_canvas[key] = value\n\n setOptions: (options) ->\n @setOption key, value for own key, value of options\n\n addFrame: (image, options={}) ->\n frame = {}\n frame.transparent = @options.transparent\n for key of frameDefaults\n frame[key] = options[key] or frameDefaults[key]\n\n # use the images width and height for options unless already set\n @setOption 'width', image.width unless @options.width?\n @setOption 'height', image.height unless @options.height?\n\n if ImageData? and image instanceof ImageData\n frame.data = image.data\n else if (CanvasRenderingContext2D? and image instanceof CanvasRenderingContext2D) or (WebGLRenderingContext? and image instanceof WebGLRenderingContext)\n if options.copy\n frame.data = @getContextData image\n else\n frame.context = image\n else if image.childNodes?\n if options.copy\n frame.data = @getImageData image\n else\n frame.image = image\n else\n throw new Error 'Invalid image'\n\n @frames.push frame\n\n render: ->\n throw new Error 'Already running' if @running\n\n if not @options.width? or not @options.height?\n throw new Error 'Width and height must be set prior to rendering'\n\n @running = true\n @nextFrame = 0\n @finishedFrames = 0\n\n @imageParts = (null for i in [0...@frames.length])\n numWorkers = @spawnWorkers()\n # we need to wait for the palette\n if @options.globalPalette == true\n @renderNextFrame()\n else\n @renderNextFrame() for i in [0...numWorkers]\n\n @emit 'start'\n @emit 'progress', 0\n\n abort: ->\n loop\n worker = @activeWorkers.shift()\n break unless worker?\n @log 'killing active worker'\n worker.terminate()\n @running = false\n @emit 'abort'\n\n # private\n\n spawnWorkers: ->\n numWorkers = Math.min(@options.workers, @frames.length)\n [@freeWorkers.length...numWorkers].forEach (i) =>\n @log \"spawning worker #{ i }\"\n worker = new Worker @options.workerScript\n worker.onmessage = (event) =>\n @activeWorkers.splice @activeWorkers.indexOf(worker), 1\n @freeWorkers.push worker\n @frameFinished event.data\n @freeWorkers.push worker\n return numWorkers\n\n frameFinished: (frame) ->\n @log \"frame #{ frame.index } finished - #{ @activeWorkers.length } active\"\n @finishedFrames++\n @emit 'progress', @finishedFrames / @frames.length\n @imageParts[frame.index] = frame\n # remember calculated palette, spawn the rest of the workers\n if @options.globalPalette == true\n @options.globalPalette = frame.globalPalette\n @log 'global palette analyzed'\n @renderNextFrame() for i in [1...@freeWorkers.length] if @frames.length > 2\n if null in @imageParts\n @renderNextFrame()\n else\n @finishRendering()\n\n finishRendering: ->\n len = 0\n for frame in @imageParts\n len += (frame.data.length - 1) * frame.pageSize + frame.cursor\n len += frame.pageSize - frame.cursor\n @log \"rendering finished - filesize #{ Math.round(len / 1000) }kb\"\n data = new Uint8Array len\n offset = 0\n for frame in @imageParts\n for page, i in frame.data\n data.set page, offset\n if i is frame.data.length - 1\n offset += frame.cursor\n else\n offset += frame.pageSize\n\n image = new Blob [data],\n type: 'image/gif'\n\n @emit 'finished', image, data\n\n renderNextFrame: ->\n throw new Error 'No free workers' if @freeWorkers.length is 0\n return if @nextFrame >= @frames.length # no new frame to render\n\n frame = @frames[@nextFrame++]\n worker = @freeWorkers.shift()\n task = @getTask frame\n\n @log \"starting frame #{ task.index + 1 } of #{ @frames.length }\"\n @activeWorkers.push worker\n worker.postMessage task#, [task.data.buffer]\n\n getContextData: (ctx) ->\n return ctx.getImageData(0, 0, @options.width, @options.height).data\n\n getImageData: (image) ->\n if not @_canvas?\n @_canvas = document.createElement 'canvas'\n @_canvas.width = @options.width\n @_canvas.height = @options.height\n\n ctx = @_canvas.getContext '2d'\n ctx.setFill = @options.background\n ctx.fillRect 0, 0, @options.width, @options.height\n ctx.drawImage image, 0, 0\n\n return @getContextData ctx\n\n getTask: (frame) ->\n index = @frames.indexOf frame\n task =\n index: index\n last: index is (@frames.length - 1)\n delay: frame.delay\n transparent: frame.transparent\n width: @options.width\n height: @options.height\n quality: @options.quality\n dither: @options.dither\n globalPalette: @options.globalPalette\n repeat: @options.repeat\n canTransfer: (browser.name is 'chrome')\n\n if frame.data?\n task.data = frame.data\n else if frame.context?\n task.data = @getContextData frame.context\n else if frame.image?\n task.data = @getImageData frame.image\n else\n throw new Error 'Invalid frame'\n\n return task\n\n log: (args...) ->\n return unless @options.debug\n console.log args...\n\n\nmodule.exports = GIF\n"]}
--------------------------------------------------------------------------------
/public/lib/gif.worker.js:
--------------------------------------------------------------------------------
1 | // gif.worker.js 0.2.0 - https://github.com/jnordberg/gif.js
2 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o=ByteArray.pageSize)this.newPage();this.pages[this.page][this.cursor++]=val};ByteArray.prototype.writeUTFBytes=function(string){for(var l=string.length,i=0;i=0)this.dispose=disposalCode};GIFEncoder.prototype.setRepeat=function(repeat){this.repeat=repeat};GIFEncoder.prototype.setTransparent=function(color){this.transparent=color};GIFEncoder.prototype.addFrame=function(imageData){this.image=imageData;this.colorTab=this.globalPalette&&this.globalPalette.slice?this.globalPalette:null;this.getImagePixels();this.analyzePixels();if(this.globalPalette===true)this.globalPalette=this.colorTab;if(this.firstFrame){this.writeLSD();this.writePalette();if(this.repeat>=0){this.writeNetscapeExt()}}this.writeGraphicCtrlExt();this.writeImageDesc();if(!this.firstFrame&&!this.globalPalette)this.writePalette();this.writePixels();this.firstFrame=false};GIFEncoder.prototype.finish=function(){this.out.writeByte(59)};GIFEncoder.prototype.setQuality=function(quality){if(quality<1)quality=1;this.sample=quality};GIFEncoder.prototype.setDither=function(dither){if(dither===true)dither="FloydSteinberg";this.dither=dither};GIFEncoder.prototype.setGlobalPalette=function(palette){this.globalPalette=palette};GIFEncoder.prototype.getGlobalPalette=function(){return this.globalPalette&&this.globalPalette.slice&&this.globalPalette.slice(0)||this.globalPalette};GIFEncoder.prototype.writeHeader=function(){this.out.writeUTFBytes("GIF89a")};GIFEncoder.prototype.analyzePixels=function(){if(!this.colorTab){this.neuQuant=new NeuQuant(this.pixels,this.sample);this.neuQuant.buildColormap();this.colorTab=this.neuQuant.getColormap()}if(this.dither){this.ditherPixels(this.dither.replace("-serpentine",""),this.dither.match(/-serpentine/)!==null)}else{this.indexPixels()}this.pixels=null;this.colorDepth=8;this.palSize=7;if(this.transparent!==null){this.transIndex=this.findClosest(this.transparent,true)}};GIFEncoder.prototype.indexPixels=function(imgq){var nPix=this.pixels.length/3;this.indexedPixels=new Uint8Array(nPix);var k=0;for(var j=0;j=0&&x1+x=0&&y1+y>16,(c&65280)>>8,c&255,used)};GIFEncoder.prototype.findClosestRGB=function(r,g,b,used){if(this.colorTab===null)return-1;if(this.neuQuant&&!used){return this.neuQuant.lookupRGB(r,g,b)}var c=b|g<<8|r<<16;var minpos=0;var dmin=256*256*256;var len=this.colorTab.length;for(var i=0,index=0;i=0){disp=dispose&7}disp<<=2;this.out.writeByte(0|disp|0|transp);this.writeShort(this.delay);this.out.writeByte(this.transIndex);this.out.writeByte(0)};GIFEncoder.prototype.writeImageDesc=function(){this.out.writeByte(44);this.writeShort(0);this.writeShort(0);this.writeShort(this.width);this.writeShort(this.height);if(this.firstFrame||this.globalPalette){this.out.writeByte(0)}else{this.out.writeByte(128|0|0|0|this.palSize)}};GIFEncoder.prototype.writeLSD=function(){this.writeShort(this.width);this.writeShort(this.height);this.out.writeByte(128|112|0|this.palSize);this.out.writeByte(0);this.out.writeByte(0)};GIFEncoder.prototype.writeNetscapeExt=function(){this.out.writeByte(33);this.out.writeByte(255);this.out.writeByte(11);this.out.writeUTFBytes("NETSCAPE2.0");this.out.writeByte(3);this.out.writeByte(1);this.writeShort(this.repeat);this.out.writeByte(0)};GIFEncoder.prototype.writePalette=function(){this.out.writeBytes(this.colorTab);var n=3*256-this.colorTab.length;for(var i=0;i>8&255)};GIFEncoder.prototype.writePixels=function(){var enc=new LZWEncoder(this.width,this.height,this.indexedPixels,this.colorDepth);enc.encode(this.out)};GIFEncoder.prototype.stream=function(){return this.out};module.exports=GIFEncoder},{"./LZWEncoder.js":2,"./TypedNeuQuant.js":3}],2:[function(require,module,exports){var EOF=-1;var BITS=12;var HSIZE=5003;var masks=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function LZWEncoder(width,height,pixels,colorDepth){var initCodeSize=Math.max(2,colorDepth);var accum=new Uint8Array(256);var htab=new Int32Array(HSIZE);var codetab=new Int32Array(HSIZE);var cur_accum,cur_bits=0;var a_count;var free_ent=0;var maxcode;var clear_flg=false;var g_init_bits,ClearCode,EOFCode;function char_out(c,outs){accum[a_count++]=c;if(a_count>=254)flush_char(outs)}function cl_block(outs){cl_hash(HSIZE);free_ent=ClearCode+2;clear_flg=true;output(ClearCode,outs)}function cl_hash(hsize){for(var i=0;i=0){disp=hsize_reg-i;if(i===0)disp=1;do{if((i-=disp)<0)i+=hsize_reg;if(htab[i]===fcode){ent=codetab[i];continue outer_loop}}while(htab[i]>=0)}output(ent,outs);ent=c;if(free_ent<1<0){outs.writeByte(a_count);outs.writeBytes(accum,0,a_count);a_count=0}}function MAXCODE(n_bits){return(1<0)cur_accum|=code<=8){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}if(free_ent>maxcode||clear_flg){if(clear_flg){maxcode=MAXCODE(n_bits=g_init_bits);clear_flg=false}else{++n_bits;if(n_bits==BITS)maxcode=1<0){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}flush_char(outs)}}this.encode=encode}module.exports=LZWEncoder},{}],3:[function(require,module,exports){var ncycles=100;var netsize=256;var maxnetpos=netsize-1;var netbiasshift=4;var intbiasshift=16;var intbias=1<>betashift;var betagamma=intbias<>3;var radiusbiasshift=6;var radiusbias=1<>3);var i,v;for(i=0;i>=netbiasshift;network[i][1]>>=netbiasshift;network[i][2]>>=netbiasshift;network[i][3]=i}}function altersingle(alpha,i,b,g,r){network[i][0]-=alpha*(network[i][0]-b)/initalpha;network[i][1]-=alpha*(network[i][1]-g)/initalpha;network[i][2]-=alpha*(network[i][2]-r)/initalpha}function alterneigh(radius,i,b,g,r){var lo=Math.abs(i-radius);var hi=Math.min(i+radius,netsize);var j=i+1;var k=i-1;var m=1;var p,a;while(jlo){a=radpower[m++];if(jlo){p=network[k--];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}}}function contest(b,g,r){var bestd=~(1<<31);var bestbiasd=bestd;var bestpos=-1;var bestbiaspos=bestpos;var i,n,dist,biasdist,betafreq;for(i=0;i>intbiasshift-netbiasshift);if(biasdist>betashift;freq[i]-=betafreq;bias[i]+=betafreq<>1;for(j=previouscol+1;j>1;for(j=previouscol+1;j<256;j++)netindex[j]=maxnetpos}function inxsearch(b,g,r){var a,p,dist;var bestd=1e3;var best=-1;var i=netindex[g];var j=i-1;while(i=0){if(i=bestd)i=netsize;else{i++;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist=0){p=network[j];dist=g-p[1];if(dist>=bestd)j=-1;else{j--;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist>radiusbiasshift;if(rad<=1)rad=0;for(i=0;i=lengthcount)pix-=lengthcount;i++;if(delta===0)delta=1;if(i%delta===0){alpha-=alpha/alphadec;radius-=radius/radiusdec;rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(j=0;j
2 |
3 |
8 |
9 |
14 |
15 |
16 |
17 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {{ $t('tabbar.game') }}
37 |
38 |
39 |
40 |
41 | {{ $t('tabbar.settings') }}
42 |
43 |
44 |
45 |
46 | {{ $t('tabbar.about') }}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
213 |
214 |
292 |
--------------------------------------------------------------------------------
/src/ai/engine-warpper.worker.js:
--------------------------------------------------------------------------------
1 | var EngineInstance = null
2 |
3 | function locateFile(url, engineDirURL) {
4 | // Redirect 'rapfi.*\.data' to 'rapfi.data'
5 | if (/^rapfi.*\.data$/.test(url)) url = 'rapfi.data'
6 | return engineDirURL + url
7 | }
8 |
9 | self.onmessage = function (e) {
10 | const { type, data } = e.data
11 | if (type == 'command') {
12 | EngineInstance.sendCommand(data)
13 | } else if (type == 'engineScriptURL') {
14 | const { engineURL, memoryArgs } = data
15 | const engineDirURL = engineURL.substring(0, engineURL.lastIndexOf('/') + 1)
16 | self.importScripts(engineURL)
17 |
18 | self['Rapfi']({
19 | locateFile: (url) => locateFile(url, engineDirURL),
20 | onReceiveStdout: (o) => self.postMessage({ type: 'stdout', data: o }),
21 | onReceiveStderr: (o) => self.postMessage({ type: 'stderr', data: o }),
22 | onExit: (c) => self.postMessage({ type: 'exit', data: c }),
23 | setStatus: (s) => self.postMessage({ type: 'status', data: s }),
24 | wasmMemory: memoryArgs ? new WebAssembly.Memory(memoryArgs) : undefined,
25 | }).then((instance) => ((EngineInstance = instance), self.postMessage({ type: 'ready' })))
26 | } else {
27 | console.error('worker received unknown payload: ' + e.data)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/ai/engine.js:
--------------------------------------------------------------------------------
1 | import Worker from './engine-warpper.worker.js'
2 | import { script } from '@/../node_modules/dynamic-import/dist/import.js'
3 | import { threads, simd, relaxedSimd } from 'wasm-feature-detect'
4 |
5 | var callback, engineInstance, supportThreads, dataLoaded
6 |
7 | function locateFile(url, engineDirURL) {
8 | // Redirect 'rapfi.*\.data' to 'rapfi.data'
9 | if (/^rapfi.*\.data$/.test(url)) url = 'rapfi.data'
10 | return engineDirURL + url
11 | }
12 |
13 | function getWasmMemoryArguments(isShared, maximum_memory_mb = 2048) {
14 | return {
15 | initial: 64 * ((1024 * 1024) / 65536), // 64MB
16 | maximum: maximum_memory_mb * ((1024 * 1024) / 65536),
17 | shared: isShared,
18 | }
19 | }
20 |
21 | function instantiateSharedWasmMemory() {
22 | let maximum_memory_mb = 2048
23 | // Find the maximum memory size that can be allocated
24 | while (maximum_memory_mb > 512) {
25 | try {
26 | const memory = new WebAssembly.Memory(getWasmMemoryArguments(true, maximum_memory_mb))
27 | memory.grow(1)
28 | return memory
29 | } catch (e) {
30 | maximum_memory_mb /= 2
31 | }
32 | }
33 | return new WebAssembly.Memory(getWasmMemoryArguments(true, maximum_memory_mb))
34 | }
35 |
36 | // Init engine and setup callback function for receiving engine output
37 | async function init(callbackFn_) {
38 | callback = callbackFn_
39 | dataLoaded = false
40 |
41 | supportThreads = await threads()
42 | const supportSIMD = await simd()
43 | const supportRelaxedSIMD = supportThreads && (await relaxedSimd())
44 |
45 | const engineFlags =
46 | (supportThreads ? '-multi' : '-single') +
47 | (supportSIMD ? '-simd128' : '') +
48 | (supportRelaxedSIMD ? '-relaxed' : '')
49 | const engineURL = process.env.BASE_URL + `build/rapfi${engineFlags}.js`
50 |
51 | if (supportThreads) {
52 | await script.import(/* webpackIgnore: true */ engineURL)
53 |
54 | const engineDirURL = engineURL.substring(0, engineURL.lastIndexOf('/') + 1)
55 |
56 | engineInstance = await self['Rapfi']({
57 | locateFile: (url) => locateFile(url, engineDirURL),
58 | onReceiveStdout: (o) => onEngineStdout(o),
59 | onReceiveStderr: (o) => onEngineStderr(o),
60 | onExit: (c) => onEngineExit(c),
61 | setStatus: (s) => onEngineStatus(s),
62 | wasmMemory: instantiateSharedWasmMemory(),
63 | })
64 | dataLoaded = true
65 | callback({ ok: true })
66 | } else {
67 | engineInstance = new Worker()
68 |
69 | engineInstance.onmessage = (e) => {
70 | const { type, data } = e.data
71 | if (type === 'stdout') onEngineStdout(data)
72 | else if (type === 'stderr') onEngineStderr(data)
73 | else if (type === 'exit') onEngineExit(data)
74 | else if (type === 'status') onEngineStatus(data)
75 | else if (type === 'ready') (dataLoaded = true), callback({ ok: true })
76 | else console.error('received unknown message from worker: ', e.data)
77 | }
78 |
79 | engineInstance.onerror = (err) => {
80 | console.error('worker error: ' + err.message + '\nretrying after 0.5s...')
81 | engineInstance.terminate()
82 | setTimeout(() => init(callback), 500)
83 | }
84 |
85 | engineInstance.postMessage({
86 | type: 'engineScriptURL',
87 | data: {
88 | engineURL: engineURL,
89 | memoryArgs: getWasmMemoryArguments(false),
90 | },
91 | })
92 | }
93 |
94 | return engineURL
95 | }
96 |
97 | // Stop current engine's thinking process
98 | // Returns true if force stoped, otherwise returns false
99 | function stopThinking() {
100 | if (supportThreads) {
101 | sendCommand('YXSTOP')
102 | return false
103 | } else {
104 | engineInstance.terminate()
105 | init(callback) // Use previous callback function
106 | return true
107 | }
108 | }
109 |
110 | // Send a command to engine
111 | function sendCommand(cmd) {
112 | if (typeof cmd !== 'string' || cmd.length == 0) return
113 |
114 | if (supportThreads) engineInstance.sendCommand(cmd)
115 | else engineInstance.postMessage({ type: 'command', data: cmd })
116 | }
117 |
118 | // process output from engine and call callback function
119 | function onEngineStdout(output) {
120 | let i = output.indexOf(' ')
121 |
122 | if (i == -1) {
123 | if (output == 'OK') return
124 | else if (output == 'SWAP') callback({ swap: true })
125 | else {
126 | const coord = output.split(',')
127 | callback({ pos: [+coord[0], +coord[1]] })
128 | }
129 | return
130 | }
131 |
132 | let head = output.substring(0, i)
133 | let tail = output.substring(i + 1)
134 |
135 | if (head == 'MESSAGE') {
136 | if (tail.startsWith('REALTIME')) {
137 | let r = tail.split(' ')
138 | if (r.length < 3) {
139 | callback({
140 | realtime: {
141 | type: r[1],
142 | },
143 | })
144 | } else {
145 | let coord = r[2].split(',')
146 | callback({
147 | realtime: {
148 | type: r[1],
149 | pos: [+coord[0], +coord[1]],
150 | },
151 | })
152 | }
153 | } else {
154 | callback({ msg: tail })
155 | }
156 | } else if (head == 'INFO') {
157 | i = tail.indexOf(' ')
158 | head = tail.substring(0, i)
159 | tail = tail.substring(i + 1)
160 |
161 | if (head == 'PV') callback({ multipv: tail })
162 | else if (head == 'NUMPV') callback({ numpv: +tail })
163 | else if (head == 'DEPTH') callback({ depth: +tail })
164 | else if (head == 'SELDEPTH') callback({ seldepth: +tail })
165 | else if (head == 'NODES') callback({ nodes: +tail })
166 | else if (head == 'TOTALNODES') callback({ totalnodes: +tail })
167 | else if (head == 'TOTALTIME') callback({ totaltime: +tail })
168 | else if (head == 'SPEED') callback({ speed: +tail })
169 | else if (head == 'EVAL') callback({ eval: tail })
170 | else if (head == 'WINRATE') callback({ winrate: parseFloat(tail) })
171 | else if (head == 'BESTLINE')
172 | callback({ bestline: tail.match(/\d+,\d+/g).map((s) => s.split(',').map(Number)) })
173 | } else if (head == 'FORBID') {
174 | callback({
175 | forbid: (tail.match(/.{4}/g) || []).map((s) => {
176 | let coord = s.match(/([0-9][0-9])([0-9][0-9])/)
177 | let x = +coord[1]
178 | let y = +coord[2]
179 | return [x, y]
180 | }),
181 | })
182 | } else if (head == 'ERROR') {
183 | callback({ error: tail })
184 | } else if (head.indexOf(',') != -1) {
185 | const coord1 = head.split(',')
186 | const coord2 = tail.split(',')
187 | callback({
188 | pos: [+coord1[0], +coord1[1]],
189 | pos2: [+coord2[0], +coord2[1]],
190 | })
191 | } else {
192 | callback({ unknown: tail })
193 | }
194 | }
195 |
196 | function onEngineStderr(output) {
197 | console.error('[Engine Error] ' + output)
198 | }
199 |
200 | function onEngineExit(code) {
201 | console.log('[Engine Exit] ' + code)
202 | }
203 |
204 | function onEngineStatus(status) {
205 | if (dataLoaded) return
206 |
207 | if (status === 'Running...' || status === '') {
208 | dataLoaded = true
209 | callback({
210 | loading: {
211 | progress: 1.0,
212 | },
213 | })
214 | }
215 |
216 | const match = status.match(/\((\d+)\/(\d+)\)/)
217 | if (match) {
218 | const loadedBytes = parseInt(match[1], 10)
219 | const totalBytes = parseInt(match[2], 10)
220 | if (loadedBytes == totalBytes) dataLoaded = true
221 | callback({
222 | loading: {
223 | progress: loadedBytes / totalBytes,
224 | loadedBytes: loadedBytes,
225 | totalBytes: totalBytes,
226 | },
227 | })
228 | }
229 | }
230 |
231 | export { init, sendCommand, stopThinking }
232 |
--------------------------------------------------------------------------------
/src/components/Bestline.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
mouseoverPV(index)" @mouseleave="mouseleavePV"
5 | @dblclick="() => dblclickPV(index)">
6 | {{ String.fromCharCode('A'.charCodeAt(0) + pos[0]) + (boardSize - pos[1]).toString() }}
7 |
8 |
9 |
10 |
11 |
12 |
58 |
59 |
79 |
--------------------------------------------------------------------------------
/src/components/Board.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
687 |
688 |
722 |
--------------------------------------------------------------------------------
/src/i18n.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueI18n from 'vue-i18n'
3 |
4 | Vue.use(VueI18n)
5 |
6 | const LOCALES = ['zh-CN', 'zh-TW', 'en', 'ko', 'ja', 'vi', 'ru']
7 |
8 | function loadLocaleMessages() {
9 | const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
10 | const messages = {}
11 | locales.keys().forEach((key) => {
12 | const matched = key.match(/([A-Za-z0-9-_]+)\./i)
13 | if (matched && matched.length > 1) {
14 | const locale = matched[1]
15 | messages[locale] = locales(key)
16 | }
17 | })
18 | return messages
19 | }
20 |
21 | function getDefaultLanguage() {
22 | // Iterate through the user's preferred languages
23 | for (const lang of navigator.languages) {
24 | if (LOCALES.includes(lang)) {
25 | return lang // Exact match
26 | }
27 |
28 | // Check for partial match (e.g., "en-US" matching "en")
29 | const baseLang = lang.split('-')[0]
30 | if (LOCALES.includes(baseLang)) {
31 | return baseLang
32 | }
33 | }
34 |
35 | // Default to English if no match is found
36 | return 'en'
37 | }
38 |
39 | export default new VueI18n({
40 | locale: getDefaultLanguage(),
41 | fallbackLocale: 'en',
42 | messages: loadLocaleMessages(),
43 | })
44 |
--------------------------------------------------------------------------------
/src/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": "Gomoku Calculator",
3 | "common": {
4 | "ok": "OK",
5 | "confirm": "Confirm",
6 | "cancel": "Cancel"
7 | },
8 | "tabbar": {
9 | "game": "Board",
10 | "settings": "Settings",
11 | "about": "About"
12 | },
13 | "game": {
14 | "balance1": "Find One-step Balanced Move",
15 | "balance2": "Find Two-step Balanced Move",
16 | "flip": "Flip",
17 | "move": "Move",
18 | "info": {
19 | "depth": "Depth",
20 | "eval": "Eval",
21 | "speed": "Speed",
22 | "nodes": "Nodes",
23 | "time": "Time",
24 | "bestline": "Bestline",
25 | "nbestIndex": "Idx"
26 | },
27 | "interruptThinking": {
28 | "title": "AI is thinking",
29 | "msg": "Stop thinking now?"
30 | },
31 | "swap": {
32 | "title": "SWAP",
33 | "msg": "AI chose to swap.",
34 | "questionTitle": "SWAP?",
35 | "questionMsg": "Do you want to swap?"
36 | },
37 | "forbid": {
38 | "title": "Forbid",
39 | "msg": "This move is forbidden."
40 | },
41 | "shotJpg": "Take ScreenShot as JPG",
42 | "shotGif": "Take ScreenShot as GIF",
43 | "gifDelay": "Enter the delay(ms) of each frame",
44 | "gifStart": "Enter the start index",
45 | "gifLoading": "Gif is generating...",
46 | "saveScreenshot": "Save screenshot to device",
47 | "saveScreenshotIOS": "Long press to save screenshot",
48 | "currentPos": "Position",
49 | "evalChart": "Evaluation Chart",
50 | "black": "BLACK",
51 | "white": "WHITE",
52 | "engineLoading": "Downloading engine data...",
53 | "engineLoadingError": "Engine loading failed, please refresh and try again",
54 | "copiedToClipboard": "Copied to clipboard"
55 | },
56 | "setting": {
57 | "language": "Language",
58 | "thinking": {
59 | "timeTitle": "Thinking Time",
60 | "turnTime": "Turn time(s)",
61 | "matchTime": "Match time(s)",
62 | "maxDepth": "Max Depth",
63 | "maxNode": "Max Node(M)",
64 | "fast": "Fast game(~3 min)",
65 | "slow": "Slow game(~15 min)",
66 | "analysis": "Analysis(Unlimited Time)",
67 | "custom": "Custom",
68 | "handicap": "Handicap",
69 | "nbest": "Multi PV",
70 | "thinkTitle": "Thinking",
71 | "threads": "Thread Num",
72 | "pondering": "Pondering",
73 | "hashSize": "Transposition Table Size",
74 | "config": {
75 | "title": "Engine Model",
76 | "mix9lite": "Mix9Lite NNUE",
77 | "220723": "Classic(2022/07/23)",
78 | "210901": "Classic(2021/09/01)"
79 | },
80 | "candrange": {
81 | "title": "Candidate Range",
82 | "square2": "Square2",
83 | "square2line3": "Square2 + Line3",
84 | "square3": "Square3",
85 | "square3line4": "Square3 + Line4 (Recommended)",
86 | "square4": "Square4",
87 | "fullboard": "Full Board"
88 | }
89 | },
90 | "board": {
91 | "title": "Board",
92 | "size": "Board Size",
93 | "rule": {
94 | "title": "Rule",
95 | "gomoku": "Gomoku",
96 | "standard": "Standard",
97 | "renju": "Renju",
98 | "swap1": "Gomoku - SWAP1"
99 | },
100 | "aiThinkBlack": "AI Plays Black",
101 | "aiThinkWhite": "AI Plays White",
102 | "clickCheck": {
103 | "title": "Click Behaviour",
104 | "direct": "Direct Move",
105 | "confirm": "Secondary Confirmation",
106 | "slide": "Slide To Move"
107 | },
108 | "showTitle": "Board Display",
109 | "showCoord": "Show Coord",
110 | "showAnalysis": "Show Analysis",
111 | "showDetail": "Show Detail",
112 | "showIndex": "Show Index",
113 | "showLastStep": "Highlight Last Step",
114 | "showWinline": "Show Winning Line",
115 | "showForbid": "Show Forbid",
116 | "showPvEval": {
117 | "title": "Show PV Evaluation",
118 | "none": "Disable",
119 | "eval": "Eval",
120 | "winrate": "Winrate"
121 | },
122 | "colorTitle": "Color Scheme",
123 | "boardColor": "Board Color",
124 | "lastStepColor": "Last Step Color",
125 | "winlineColor": "Winning Line Color",
126 | "bestMoveColor": "Analysis - Best Move",
127 | "thoughtMoveColor": "Analysis - Searched Move",
128 | "lostMoveColor": "Analysis - Lost Move"
129 | },
130 | "browser": {
131 | "capability": "Browser Capability",
132 | "simd": "SIMD",
133 | "relaxedSimd": "(Enhanced)",
134 | "threads": "Multi Threading",
135 | "maxthreads": "Max Number of Threads"
136 | },
137 | "reset": "Reset all settings"
138 | },
139 | "update": {
140 | "title": "New Version Found",
141 | "msg": "A new version has been detected. Please refresh the page to update."
142 | },
143 | "install": {
144 | "title": "Just One Step Away!",
145 | "msg": "Would you like to add 'Gomoku Calculator' to your home screen? Once installed, you can easily enjoy a seamless board experience anytime, even offline!"
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/locales/ja.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": "五目並べ計算機",
3 | "common": {
4 | "ok": "確認",
5 | "confirm": "確認",
6 | "cancel": "キャンセル"
7 | },
8 | "tabbar": {
9 | "game": "ゲーム盤",
10 | "settings": "設定",
11 | "about": "情報"
12 | },
13 | "game": {
14 | "balance1": "1手バランス点を探す",
15 | "balance2": "2手バランス点を探す",
16 | "flip": "反転",
17 | "move": "移動",
18 | "info": {
19 | "depth": "深さ",
20 | "eval": "評価",
21 | "speed": "速度",
22 | "nodes": "ノード数",
23 | "time": "所要時間",
24 | "bestline": "最良のルート",
25 | "nbestIndex": "番号"
26 | },
27 | "interruptThinking": {
28 | "title": "AI思考中",
29 | "msg": "今すぐ思考を中止しますか?"
30 | },
31 | "swap": {
32 | "title": "交替",
33 | "msg": "AIが黒白交替を選択しました。",
34 | "questionTitle": "交替しますか?",
35 | "questionMsg": "交替を選択しますか?"
36 | },
37 | "forbid": {
38 | "title": "禁手",
39 | "msg": "この場所は禁手です。"
40 | },
41 | "shotJpg": "静的スクリーンショット",
42 | "shotGif": "GIFスクリーンショット",
43 | "gifDelay": "フレームごとの時間を入力(単位:ミリ秒)",
44 | "gifStart": "開始番号を入力",
45 | "gifLoading": "GIFを生成中...",
46 | "saveScreenshot": "スクリーンショットをローカルに保存",
47 | "saveScreenshotIOS": "画像を長押ししてローカルに保存",
48 | "currentPos": "局面コード",
49 | "evalChart": "評価グラフ",
50 | "black": "黒石",
51 | "white": "白石",
52 | "engineLoading": "エンジンデータをダウンロード中...",
53 | "engineLoadingError": "エンジンの読み込みに失敗しました。リフレッシュして再試行してください",
54 | "copiedToClipboard": "クリップボードにコピーしました"
55 | },
56 | "setting": {
57 | "language": "言語",
58 | "thinking": {
59 | "timeTitle": "思考時間",
60 | "turnTime": "1手の時間(秒)",
61 | "matchTime": "ゲーム時間(秒)",
62 | "maxDepth": "最大深さ",
63 | "maxNode": "最大ノード数(M)",
64 | "fast": "早指し(1ゲーム約3分)",
65 | "slow": "長考(1ゲーム約15分)",
66 | "analysis": "分析モード(時間無制限)",
67 | "custom": "カスタム",
68 | "handicap": "棋力制限",
69 | "nbest": "複数点分析",
70 | "thinkTitle": "思考",
71 | "threads": "スレッド数",
72 | "pondering": "バックグラウンド思考",
73 | "hashSize": "ハッシュテーブルのサイズ",
74 | "config": {
75 | "title": "エンジンの重み付け",
76 | "mix9lite": "Mix9Lite NNUE",
77 | "220723": "従来型(2022/07/23)",
78 | "210901": "従来型(2021/09/01)"
79 | },
80 | "candrange": {
81 | "title": "分析範囲",
82 | "square2": "2マス",
83 | "square2line3": "2.5マス",
84 | "square3": "3マス",
85 | "square3line4": "3.5マス(推奨)",
86 | "square4": "4マス",
87 | "fullboard": "全盤"
88 | }
89 | },
90 | "board": {
91 | "title": "ゲーム盤",
92 | "size": "盤サイズ",
93 | "rule": {
94 | "title": "ルール",
95 | "gomoku": "禁手なし",
96 | "standard": "禁手なし・長連不可",
97 | "renju": "禁手あり",
98 | "swap1": "禁手なし1手交替"
99 | },
100 | "aiThinkBlack": "AIが黒石",
101 | "aiThinkWhite": "AIが白石",
102 | "clickCheck": {
103 | "title": "着手方法",
104 | "direct": "直接着手",
105 | "confirm": "確認後着手",
106 | "slide": "スライド着手"
107 | },
108 | "showTitle": "ゲーム盤表示",
109 | "showCoord": "座標を表示",
110 | "showAnalysis": "分析を表示",
111 | "showDetail": "詳細を表示",
112 | "showIndex": "番号を表示",
113 | "showLastStep": "最後の手を強調表示",
114 | "showWinline": "勝利ラインを表示",
115 | "showForbid": "禁手を表示",
116 | "showPvEval": {
117 | "title": "分析評価を表示",
118 | "none": "表示しない",
119 | "eval": "評価を表示",
120 | "winrate": "勝率を表示"
121 | },
122 | "colorTitle": "色",
123 | "boardColor": "盤の色",
124 | "lastStepColor": "最後の手の色",
125 | "winlineColor": "勝利ラインの色",
126 | "bestMoveColor": "分析 - 最良の着手",
127 | "thoughtMoveColor": "分析 - 検索済みの着手",
128 | "lostMoveColor": "分析 - 敗着"
129 | },
130 | "browser": {
131 | "capability": "ブラウザ機能",
132 | "simd": "SIMD加速",
133 | "relaxedSimd": "(強化版)",
134 | "threads": "マルチスレッド",
135 | "maxthreads": "最大スレッド数"
136 | },
137 | "reset": "全設定をリセット"
138 | },
139 | "update": {
140 | "title": "新しいバージョンを発見",
141 | "msg": "新しいバージョンが検出されました。更新するにはページをリフレッシュしてください。"
142 | },
143 | "install": {
144 | "title": "あと一歩!",
145 | "msg": "「五目並べ計算機」をホーム画面に追加しますか?インストールすれば、ネットがなくてもいつでも快適に棋局を楽しめます!"
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/locales/ko.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": "오목 계산기",
3 | "common": {
4 | "ok": "확인",
5 | "confirm": "확인",
6 | "cancel": "취소"
7 | },
8 | "tabbar": {
9 | "game": "게임판",
10 | "settings": "설정",
11 | "about": "정보"
12 | },
13 | "game": {
14 | "balance1": "한 단계 균형점 찾기",
15 | "balance2": "두 단계 균형점 찾기",
16 | "flip": "뒤집기",
17 | "move": "이동",
18 | "info": {
19 | "depth": "깊이",
20 | "eval": "평가",
21 | "speed": "속도",
22 | "nodes": "노드 수",
23 | "time": "소요 시간",
24 | "bestline": "최적 경로",
25 | "nbestIndex": "번호"
26 | },
27 | "interruptThinking": {
28 | "title": "AI가 생각 중",
29 | "msg": "즉시 중단하시겠습니까?"
30 | },
31 | "swap": {
32 | "title": "교환",
33 | "msg": "AI가 흑백 교환을 선택했습니다.",
34 | "questionTitle": "교환?",
35 | "questionMsg": "교환을 선택하시겠습니까?"
36 | },
37 | "forbid": {
38 | "title": "금지 수",
39 | "msg": "해당 위치는 금지 수입니다."
40 | },
41 | "shotJpg": "정적 스크린샷",
42 | "shotGif": "GIF 스크린샷",
43 | "gifDelay": "프레임당 시간 입력 (단위: 밀리초)",
44 | "gifStart": "시작 번호 입력",
45 | "gifLoading": "GIF 생성 중...",
46 | "saveScreenshot": "스크린샷을 로컬에 저장",
47 | "saveScreenshotIOS": "이미지를 길게 눌러 로컬에 저장",
48 | "currentPos": "게임 상태 코드",
49 | "evalChart": "평가 그래프",
50 | "black": "흑돌",
51 | "white": "백돌",
52 | "engineLoading": "엔진 데이터 다운로드 중...",
53 | "engineLoadingError": "엔진 로드 실패, 새로고침 후 다시 시도하세요",
54 | "copiedToClipboard": "클립보드에 복사되었습니다"
55 | },
56 | "setting": {
57 | "language": "언어",
58 | "thinking": {
59 | "timeTitle": "생각 시간",
60 | "turnTime": "한 수당 시간 (초)",
61 | "matchTime": "게임 시간 (초)",
62 | "maxDepth": "최대 깊이",
63 | "maxNode": "최대 노드 수 (M)",
64 | "fast": "속기 (게임당 약 3분)",
65 | "slow": "완기 (게임당 약 15분)",
66 | "analysis": "분석 모드 (시간 제한 없음)",
67 | "custom": "사용자 정의",
68 | "handicap": "기력 제한",
69 | "nbest": "다중 분석",
70 | "thinkTitle": "생각",
71 | "threads": "스레드 수",
72 | "pondering": "백그라운드 생각",
73 | "hashSize": "해시 테이블 크기",
74 | "config": {
75 | "title": "엔진 가중치",
76 | "mix9lite": "Mix9Lite NNUE",
77 | "220723": "전통 (2022/07/23)",
78 | "210901": "전통 (2021/09/01)"
79 | },
80 | "candrange": {
81 | "title": "분석 범위",
82 | "square2": "2칸",
83 | "square2line3": "2칸 반",
84 | "square3": "3칸",
85 | "square3line4": "3칸 반 (추천)",
86 | "square4": "4칸",
87 | "fullboard": "전체 판"
88 | }
89 | },
90 | "board": {
91 | "title": "게임판",
92 | "size": "판 크기",
93 | "rule": {
94 | "title": "규칙",
95 | "gomoku": "금수 없음",
96 | "standard": "금수 없음, 5목 이상 승리 불가",
97 | "renju": "금수 있음",
98 | "swap1": "금수 없음, 한 수 교환"
99 | },
100 | "aiThinkBlack": "AI가 흑돌",
101 | "aiThinkWhite": "AI가 백돌",
102 | "clickCheck": {
103 | "title": "착수 방식",
104 | "direct": "바로 착수",
105 | "confirm": "재확인",
106 | "slide": "슬라이드 착수"
107 | },
108 | "showTitle": "게임판 표시",
109 | "showCoord": "좌표 표시",
110 | "showAnalysis": "분석 표시",
111 | "showDetail": "세부 사항 표시",
112 | "showIndex": "번호 표시",
113 | "showLastStep": "마지막 수 강조",
114 | "showWinline": "승리선 표시",
115 | "showForbid": "금수 표시",
116 | "showPvEval": {
117 | "title": "분석 평가 표시",
118 | "none": "표시 안 함",
119 | "eval": "평가 표시",
120 | "winrate": "승률 표시"
121 | },
122 | "colorTitle": "색상",
123 | "boardColor": "게임판 색상",
124 | "lastStepColor": "마지막 수 색상",
125 | "winlineColor": "승리선 색상",
126 | "bestMoveColor": "분석 - 최적 수",
127 | "thoughtMoveColor": "분석 - 탐색 완료 수",
128 | "lostMoveColor": "분석 - 패착"
129 | },
130 | "browser": {
131 | "capability": "브라우저 기능",
132 | "simd": "SIMD 가속",
133 | "relaxedSimd": "(강화됨)",
134 | "threads": "멀티스레드",
135 | "maxthreads": "최대 스레드 수"
136 | },
137 | "reset": "모든 설정 초기화"
138 | },
139 | "update": {
140 | "title": "새 버전 발견",
141 | "msg": "새 버전이 감지되었습니다. 업데이트하려면 페이지를 새로 고침하세요."
142 | },
143 | "install": {
144 | "title": "한 걸음 남았습니다!",
145 | "msg": "'오목 계산기'를 홈 화면에 추가하시겠습니까? 설치하면 언제든지 간편하게 바둑판 경험을 즐길 수 있으며, 인터넷 없이도 쉽게 접근할 수 있습니다!"
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/locales/ru.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": "Калькулятор Гомоку",
3 | "common": {
4 | "ok": "ОК",
5 | "confirm": "Подтвердить",
6 | "cancel": "Отмена"
7 | },
8 | "tabbar": {
9 | "game": "Игровое поле",
10 | "settings": "Настройки",
11 | "about": "О программе"
12 | },
13 | "game": {
14 | "balance1": "Найти точку равновесия за 1 ход",
15 | "balance2": "Найти точку равновесия за 2 хода",
16 | "flip": "Перевернуть",
17 | "move": "Переместить",
18 | "info": {
19 | "depth": "Глубина",
20 | "eval": "Оценка",
21 | "speed": "Скорость",
22 | "nodes": "Количество узлов",
23 | "time": "Время",
24 | "bestline": "Лучший ход",
25 | "nbestIndex": "Номер"
26 | },
27 | "interruptThinking": {
28 | "title": "ИИ размышляет",
29 | "msg": "Прервать размышление немедленно?"
30 | },
31 | "swap": {
32 | "title": "Обмен",
33 | "msg": "ИИ выбрал обмен черными и белыми.",
34 | "questionTitle": "Обмен?",
35 | "questionMsg": "Вы хотите обменяться?"
36 | },
37 | "forbid": {
38 | "title": "Запрещённый ход",
39 | "msg": "Эта позиция является запрещённой."
40 | },
41 | "shotJpg": "Снимок экрана",
42 | "shotGif": "GIF-снимок",
43 | "gifDelay": "Введите время на кадр (в миллисекундах)",
44 | "gifStart": "Введите начальный номер",
45 | "gifLoading": "Создание GIF...",
46 | "saveScreenshot": "Сохранить снимок на устройство",
47 | "saveScreenshotIOS": "Нажмите и удерживайте изображение, чтобы сохранить",
48 | "currentPos": "Код текущей позиции",
49 | "evalChart": "График оценки",
50 | "black": "Чёрные",
51 | "white": "Белые",
52 | "engineLoading": "Загрузка данных движка...",
53 | "engineLoadingError": "Ошибка загрузки движка, обновите страницу и попробуйте снова",
54 | "copiedToClipboard": "Скопировано в буфер обмена"
55 | },
56 | "setting": {
57 | "language": "Язык",
58 | "thinking": {
59 | "timeTitle": "Время размышления",
60 | "turnTime": "Время на ход (секунды)",
61 | "matchTime": "Время на матч (секунды)",
62 | "maxDepth": "Максимальная глубина",
63 | "maxNode": "Максимальное количество узлов (М)",
64 | "fast": "Быстрая игра (около 3 минут на матч)",
65 | "slow": "Медленная игра (около 15 минут на матч)",
66 | "analysis": "Режим анализа (без ограничения времени)",
67 | "custom": "Настраиваемый",
68 | "handicap": "Ограничение уровня игры",
69 | "nbest": "Множественный анализ",
70 | "thinkTitle": "Размышление",
71 | "threads": "Количество потоков",
72 | "pondering": "Фоновое размышление",
73 | "hashSize": "Размер хеш-таблицы",
74 | "config": {
75 | "title": "Весовые коэффициенты движка",
76 | "mix9lite": "Mix9Lite NNUE",
77 | "220723": "Классический (2022/07/23)",
78 | "210901": "Классический (2021/09/01)"
79 | },
80 | "candrange": {
81 | "title": "Диапазон анализа",
82 | "square2": "2 круга",
83 | "square2line3": "2,5 круга",
84 | "square3": "3 круга",
85 | "square3line4": "3,5 круга (рекомендуется)",
86 | "square4": "4 круга",
87 | "fullboard": "Всё поле"
88 | }
89 | },
90 | "board": {
91 | "title": "Игровое поле",
92 | "size": "Размер поля",
93 | "rule": {
94 | "title": "Правила",
95 | "gomoku": "Без запрещённых ходов",
96 | "standard": "Без запрещённых ходов и длинные ряды не побеждают",
97 | "renju": "С запрещёнными ходами",
98 | "swap1": "Без запрещённых ходов с обменом первого хода"
99 | },
100 | "aiThinkBlack": "ИИ за чёрных",
101 | "aiThinkWhite": "ИИ за белых",
102 | "clickCheck": {
103 | "title": "Способ хода",
104 | "direct": "Прямой ход",
105 | "confirm": "Подтверждение",
106 | "slide": "Свайп для хода"
107 | },
108 | "showTitle": "Отображение на поле",
109 | "showCoord": "Показать координаты",
110 | "showAnalysis": "Показать анализ",
111 | "showDetail": "Показать детали размышлений",
112 | "showIndex": "Показать номера",
113 | "showLastStep": "Выделить последний ход",
114 | "showWinline": "Показать линию победы",
115 | "showForbid": "Показать запрещённые ходы",
116 | "showPvEval": {
117 | "title": "Показать оценку анализа",
118 | "none": "Не показывать",
119 | "eval": "Показать оценку",
120 | "winrate": "Показать вероятность победы"
121 | },
122 | "colorTitle": "Цвета",
123 | "boardColor": "Цвет игрового поля",
124 | "lastStepColor": "Цвет последнего хода",
125 | "winlineColor": "Цвет линии победы",
126 | "bestMoveColor": "Анализ - лучший ход",
127 | "thoughtMoveColor": "Анализ - проанализированный ход",
128 | "lostMoveColor": "Анализ - проигрышный ход"
129 | },
130 | "browser": {
131 | "capability": "Возможности браузера",
132 | "simd": "Ускорение SIMD",
133 | "relaxedSimd": "(Улучшенное)",
134 | "threads": "Многопоточность",
135 | "maxthreads": "Максимальное количество потоков"
136 | },
137 | "reset": "Сбросить все настройки"
138 | },
139 | "update": {
140 | "title": "Обнаружена новая версия",
141 | "msg": "Обнаружена новая версия. Пожалуйста, обновите страницу для обновления."
142 | },
143 | "install": {
144 | "title": "Всего один шаг!",
145 | "msg": "Хотите добавить «Калькулятор Гомоку» на главный экран? После установки вы сможете наслаждаться удобным опытом игры в любое время, даже без интернета!"
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/locales/vi.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": "Gomoku Calculator",
3 | "common": {
4 | "ok": "Xác nhận",
5 | "confirm": "Xác nhận",
6 | "cancel": "Hủy"
7 | },
8 | "tabbar": {
9 | "game": "Bàn cờ",
10 | "settings": "Cài đặt",
11 | "about": "Giới thiệu"
12 | },
13 | "game": {
14 | "balance1": "Tìm điểm cân bằng bằng một nước",
15 | "balance2": "Tìm điểm cân bằng bằng hai nước",
16 | "flip": "Lật",
17 | "move": "Nước đi",
18 | "info": {
19 | "depth": "Độ sâu",
20 | "eval": "Đánh giá",
21 | "speed": "Tốc độ",
22 | "nodes": "Số nút",
23 | "time": "Thời gian",
24 | "bestline": "Đường đi tốt nhất",
25 | "nbestIndex": "Thứ tự"
26 | },
27 | "interruptThinking": {
28 | "title": "AI đang suy nghĩ",
29 | "msg": "Bạn có muốn dừng suy nghĩ ngay lập tức không?"
30 | },
31 | "swap": {
32 | "title": "Đổi bên",
33 | "msg": "AI chọn đổi bên",
34 | "questionTitle": "Đổi bên?",
35 | "questionMsg": "Bạn có muốn đổi bên?"
36 | },
37 | "forbid": {
38 | "title": "Nước cấm",
39 | "msg": "Vị trí này là nước cấm."
40 | },
41 | "shotJpg": "Chụp màn hình định dạng file JPG",
42 | "shotGif": "Chụp màn hình định dạng file GIF",
43 | "gifDelay": "Nhập độ trễ (mili giây) cho mỗi khung hình",
44 | "gifStart": "Nhập chỉ số bắt đầu",
45 | "gifLoading": "Đang tạo GIF...",
46 | "saveScreenshot": "Lưu ảnh chụp",
47 | "saveScreenshotIOS": "Nhấn giữ để chụp",
48 | "currentPos": "Vị trí",
49 | "evalChart": "Biểu đồ đánh giá",
50 | "black": "Quân đen",
51 | "white": "Quân trắng",
52 | "engineLoading": "Đang tải dữ liệu engine...",
53 | "engineLoadingError": "Tải động cơ thất bại, vui lòng làm mới và thử lại",
54 | "copiedToClipboard": "Đã sao chép vào clipboard"
55 | },
56 | "setting": {
57 | "language": "Ngôn ngữ",
58 | "thinking": {
59 | "timeTitle": "Thời gian suy nghĩ",
60 | "turnTime": "Thời gian mỗi lượt (giây)",
61 | "matchTime": "Thời gian trận đấu (giây)",
62 | "maxDepth": "Đồ sâu tối đa",
63 | "maxNode": "Số nút tối đa (M)",
64 | "fast": "Cờ chớp (~3 phút)",
65 | "slow": "Chậm (~15 phút)",
66 | "analysis": "Chế độ phân tích (Không giới hạn thời gian)",
67 | "custom": "Tùy chỉnh",
68 | "handicap": "Chấp",
69 | "nbest": "Phân tích nhiều vị trí",
70 | "thinkTitle": "Suy nghĩ",
71 | "threads": "Số luồng",
72 | "pondering": "Suy nghĩ nền",
73 | "hashSize": "Kích thước bảng băm",
74 | "config": {
75 | "title": "Mô hình Engine",
76 | "mix9lite": "Mix9Lite NNUE",
77 | "220723": "Đánh giá cổ điển (2022/07/23)",
78 | "210901": "Đánh giá cổ điển (2021/09/01)"
79 | },
80 | "candrange": {
81 | "title": "Phạm vi",
82 | "square2": "Square2",
83 | "square2line3": "Square2 + Line3",
84 | "square3": "Square3",
85 | "square3line4": "Square3 + Line4 (Khuyến nghị)",
86 | "square4": "Square4",
87 | "fullboard": "Toàn bàn cờ"
88 | }
89 | },
90 | "board": {
91 | "title": "Bàn cờ",
92 | "size": "Kích thước bàn cờ",
93 | "rule": {
94 | "title": "Luật",
95 | "gomoku": "Tự do",
96 | "standard": "Tiêu chuẩn",
97 | "renju": "Renju",
98 | "swap1": "Tự do - SWAP1"
99 | },
100 | "aiThinkBlack": "AI cầm quân đen",
101 | "aiThinkWhite": "AI cầm quân trắng",
102 | "clickCheck": {
103 | "title": "Cách đặt quân",
104 | "direct": "Đặt quân trực tiếp",
105 | "confirm": "Xác nhận trước khi đặt",
106 | "slide": "Kéo thả để đặt quân"
107 | },
108 | "showTitle": "Hiển thị bàn cờ",
109 | "showCoord": "Hiển thị tọa độ",
110 | "showAnalysis": "Hiển thị phân tích",
111 | "showDetail": "Hiển thị chi tiết",
112 | "showIndex": "Hiển thị thứ tự",
113 | "showLastStep": "Làm nổi bật nước đi cuối",
114 | "showWinline": "Hiển thị đường thắng",
115 | "showForbid": "Hiển thị nước cấm",
116 | "showPvEval": {
117 | "title": "Hiển thị đánh giá phân tích",
118 | "none": "Không hiển thị",
119 | "eval": "Hiển thị đánh giá (centipawn)",
120 | "winrate": "Hiển thị tỉ lệ thắng (%)"
121 | },
122 | "colorTitle": "Màu sắc",
123 | "boardColor": "Màu bàn cờ",
124 | "lastStepColor": "Màu nước đi cuối",
125 | "winlineColor": "Màu đường thắng",
126 | "bestMoveColor": "Phân tích - Nước đi tốt nhất",
127 | "thoughtMoveColor": "Phân tích - Nước đi đã suy nghĩ",
128 | "lostMoveColor": "Phân tích - Nước thua"
129 | },
130 | "browser": {
131 | "capability": "Khả năng trình duyệt",
132 | "simd": "SIMD",
133 | "relaxedSimd": "(Tăng cường)",
134 | "threads": "Multi Threading",
135 | "maxthreads": "Số luồng tối đa"
136 | },
137 | "reset": "Đặt lại tất cả cài đặt"
138 | },
139 | "update": {
140 | "title": "Đã phát hiện phiên bản mới",
141 | "msg": "Đã phát hiện phiên bản mới. Vui lòng làm mới trang để cập nhật."
142 | },
143 | "install": {
144 | "title": "Chỉ còn một bước nữa!",
145 | "msg": "Bạn có muốn thêm 'Máy tính Cờ caro' vào màn hình chính không? Sau khi cài đặt, bạn có thể dễ dàng tận hưởng trải nghiệm ván cờ mọi lúc, ngay cả khi không có mạng!"
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/locales/zh-CN.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": "五子棋计算器",
3 | "common": {
4 | "ok": "确定",
5 | "confirm": "确认",
6 | "cancel": "取消"
7 | },
8 | "tabbar": {
9 | "game": "棋盘",
10 | "settings": "设置",
11 | "about": "关于"
12 | },
13 | "game": {
14 | "balance1": "找一步平衡点",
15 | "balance2": "找二步平衡点",
16 | "flip": "翻转",
17 | "move": "移动",
18 | "info": {
19 | "depth": "深度",
20 | "eval": "评估",
21 | "speed": "速度",
22 | "nodes": "节点数",
23 | "time": "用时",
24 | "bestline": "路线",
25 | "nbestIndex": "序号"
26 | },
27 | "interruptThinking": {
28 | "title": "AI 正在思考",
29 | "msg": "要立即停止思考吗?"
30 | },
31 | "swap": {
32 | "title": "交换",
33 | "msg": "AI 选择黑白交换。",
34 | "questionTitle": "交换?",
35 | "questionMsg": "你选择交换吗?"
36 | },
37 | "forbid": {
38 | "title": "禁手",
39 | "msg": "该位置为禁手。"
40 | },
41 | "shotJpg": "静态截图",
42 | "shotGif": "GIF截图",
43 | "gifDelay": "输入每帧时长(单位:毫秒)",
44 | "gifStart": "输入起始序号",
45 | "gifLoading": "生成GIF中...",
46 | "saveScreenshot": "保存截图到本地",
47 | "saveScreenshotIOS": "长按图片保存到本地",
48 | "currentPos": "局面代码",
49 | "evalChart": "评估曲线",
50 | "black": "黑棋",
51 | "white": "白棋",
52 | "engineLoading": "下载引擎数据中...",
53 | "engineLoadingError": "加载引擎失败,请刷新重试",
54 | "copiedToClipboard": "已复制到剪贴板"
55 | },
56 | "setting": {
57 | "language": "语言",
58 | "thinking": {
59 | "timeTitle": "思考时间",
60 | "turnTime": "步时(秒)",
61 | "matchTime": "局时(秒)",
62 | "maxDepth": "最大深度",
63 | "maxNode": "最大节点数(M)",
64 | "fast": "快棋(一局大约 3 分钟)",
65 | "slow": "慢棋(一局大约 15 分钟)",
66 | "analysis": "分析模式(不限时)",
67 | "custom": "自定义",
68 | "handicap": "棋力限制",
69 | "nbest": "多点分析",
70 | "thinkTitle": "思考",
71 | "threads": "线程数",
72 | "pondering": "后台思考",
73 | "hashSize": "置换表大小",
74 | "config": {
75 | "title": "引擎权重",
76 | "mix9lite": "Mix9Lite NNUE",
77 | "220723": "传统(2022/07/23)",
78 | "210901": "传统(2021/09/01)"
79 | },
80 | "candrange": {
81 | "title": "选点范围",
82 | "square2": "两圈",
83 | "square2line3": "两圈半",
84 | "square3": "三圈",
85 | "square3line4": "三圈半 (推荐)",
86 | "square4": "四圈",
87 | "fullboard": "全盘"
88 | }
89 | },
90 | "board": {
91 | "title": "棋盘",
92 | "size": "棋盘大小",
93 | "rule": {
94 | "title": "规则",
95 | "gomoku": "无禁手",
96 | "standard": "无禁手长连不赢",
97 | "renju": "有禁手",
98 | "swap1": "无禁手一手交换"
99 | },
100 | "aiThinkBlack": "AI执黑",
101 | "aiThinkWhite": "AI执白",
102 | "clickCheck": {
103 | "title": "落子方式",
104 | "direct": "直接落子",
105 | "confirm": "二次确认",
106 | "slide": "滑动落子"
107 | },
108 | "showTitle": "棋盘显示",
109 | "showCoord": "显示坐标",
110 | "showAnalysis": "显示分析",
111 | "showDetail": "显示思考细节",
112 | "showIndex": "显示序号",
113 | "showLastStep": "高亮最后一步",
114 | "showWinline": "显示胜利线",
115 | "showForbid": "显示禁手",
116 | "showPvEval": {
117 | "title": "显示分析估值",
118 | "none": "不显示",
119 | "eval": "显示估值",
120 | "winrate": "显示胜率"
121 | },
122 | "colorTitle": "颜色",
123 | "boardColor": "棋盘颜色",
124 | "lastStepColor": "最后一步颜色",
125 | "winlineColor": "胜利线颜色",
126 | "bestMoveColor": "分析 - 最佳着法",
127 | "thoughtMoveColor": "分析 - 已搜索着法",
128 | "lostMoveColor": "分析 - 必败着法"
129 | },
130 | "browser": {
131 | "capability": "浏览器能力",
132 | "simd": "SIMD加速",
133 | "relaxedSimd": "(增强)",
134 | "threads": "多线程",
135 | "maxthreads": "最大线程数"
136 | },
137 | "reset": "重置所有设置"
138 | },
139 | "update": {
140 | "title": "发现新版本",
141 | "msg": "检测到新版本,请刷新页面以进行更新。"
142 | },
143 | "install": {
144 | "title": "一步之遥!",
145 | "msg": "您是否希望把“五子棋计算器”添加到主屏幕?安装后可随时享受便捷的棋局体验,即使没网也能轻松访问!"
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/locales/zh-TW.json:
--------------------------------------------------------------------------------
1 | {
2 | "appName": "五子棋計算器",
3 | "common": {
4 | "ok": "確定",
5 | "confirm": "確認",
6 | "cancel": "取消"
7 | },
8 | "tabbar": {
9 | "game": "棋盤",
10 | "settings": "設定",
11 | "about": "關於"
12 | },
13 | "game": {
14 | "balance1": "找一步平衡點",
15 | "balance2": "找兩步平衡點",
16 | "flip": "翻轉",
17 | "move": "移動",
18 | "info": {
19 | "depth": "深度",
20 | "eval": "評估",
21 | "speed": "速度",
22 | "nodes": "節點數",
23 | "time": "用時",
24 | "bestline": "路線",
25 | "nbestIndex": "序號"
26 | },
27 | "interruptThinking": {
28 | "title": "AI 正在思考",
29 | "msg": "是否立即停止思考?"
30 | },
31 | "swap": {
32 | "title": "交換",
33 | "msg": "AI 選擇黑白交換。",
34 | "questionTitle": "交換?",
35 | "questionMsg": "您選擇交換嗎?"
36 | },
37 | "forbid": {
38 | "title": "禁手",
39 | "msg": "該位置為禁手。"
40 | },
41 | "shotJpg": "靜態截圖",
42 | "shotGif": "GIF 截圖",
43 | "gifDelay": "輸入每幀時長(單位:毫秒)",
44 | "gifStart": "輸入起始序號",
45 | "gifLoading": "生成 GIF 中...",
46 | "saveScreenshot": "保存截圖到本地",
47 | "saveScreenshotIOS": "長按圖片保存到本地",
48 | "currentPos": "局面代碼",
49 | "evalChart": "評估曲線",
50 | "black": "黑棋",
51 | "white": "白棋",
52 | "engineLoading": "下載引擎數據中...",
53 | "engineLoadingError": "加載引擎失敗,請刷新重試",
54 | "copiedToClipboard": "已複製到剪貼板"
55 | },
56 | "setting": {
57 | "language": "語言",
58 | "thinking": {
59 | "timeTitle": "思考時間",
60 | "turnTime": "每步時間(秒)",
61 | "matchTime": "局時(秒)",
62 | "maxDepth": "最大深度",
63 | "maxNode": "最大節點數(M)",
64 | "fast": "快棋(一局約 3 分鐘)",
65 | "slow": "慢棋(一局約 15 分鐘)",
66 | "analysis": "分析模式(不限時)",
67 | "custom": "自訂",
68 | "handicap": "棋力限制",
69 | "nbest": "多點分析",
70 | "thinkTitle": "思考",
71 | "threads": "線程數",
72 | "pondering": "後台思考",
73 | "hashSize": "置換表大小",
74 | "config": {
75 | "title": "引擎權重",
76 | "mix9lite": "Mix9Lite NNUE",
77 | "220723": "傳統(2022/07/23)",
78 | "210901": "傳統(2021/09/01)"
79 | },
80 | "candrange": {
81 | "title": "選點範圍",
82 | "square2": "兩圈",
83 | "square2line3": "兩圈半",
84 | "square3": "三圈",
85 | "square3line4": "三圈半(推薦)",
86 | "square4": "四圈",
87 | "fullboard": "全盤"
88 | }
89 | },
90 | "board": {
91 | "title": "棋盤",
92 | "size": "棋盤大小",
93 | "rule": {
94 | "title": "規則",
95 | "gomoku": "無禁手",
96 | "standard": "無禁手長連不贏",
97 | "renju": "有禁手",
98 | "swap1": "無禁手一手交換"
99 | },
100 | "aiThinkBlack": "AI 执黑",
101 | "aiThinkWhite": "AI 执白",
102 | "clickCheck": {
103 | "title": "落子方式",
104 | "direct": "直接落子",
105 | "confirm": "二次確認",
106 | "slide": "滑動落子"
107 | },
108 | "showTitle": "棋盤顯示",
109 | "showCoord": "顯示座標",
110 | "showAnalysis": "顯示分析",
111 | "showDetail": "顯示思考細節",
112 | "showIndex": "顯示序號",
113 | "showLastStep": "高亮最後一步",
114 | "showWinline": "顯示勝利線",
115 | "showForbid": "顯示禁手",
116 | "showPvEval": {
117 | "title": "顯示分析估值",
118 | "none": "不顯示",
119 | "eval": "顯示估值",
120 | "winrate": "顯示勝率"
121 | },
122 | "colorTitle": "顏色",
123 | "boardColor": "棋盤顏色",
124 | "lastStepColor": "最後一步顏色",
125 | "winlineColor": "勝利線顏色",
126 | "bestMoveColor": "分析 - 最佳着法",
127 | "thoughtMoveColor": "分析 - 已搜索着法",
128 | "lostMoveColor": "分析 - 必敗着法"
129 | },
130 | "browser": {
131 | "capability": "瀏覽器功能",
132 | "simd": "SIMD 加速",
133 | "relaxedSimd": "(增強)",
134 | "threads": "多線程",
135 | "maxthreads": "最大線程數"
136 | },
137 | "reset": "重置所有設定"
138 | },
139 | "update": {
140 | "title": "發現新版本",
141 | "msg": "檢測到新版本,請重新整理頁面以進行更新。"
142 | },
143 | "install": {
144 | "title": "一步之遙!",
145 | "msg": "您是否希望將「五子棋計算器」添加到主畫面?安裝後可隨時享受便捷的棋局體驗,即使沒網也能輕鬆訪問!"
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 | import router from './router'
4 | import i18n from './i18n'
5 | import store from './store/index.js'
6 |
7 | // Redirect to https pages
8 | if (process.env.NODE_ENV != 'development') {
9 | let loc = window.location.href + ''
10 | if (loc.indexOf('http://') == 0) {
11 | window.location.href = loc.replace('http://', 'https://')
12 | }
13 | }
14 |
15 | // Fix error of redundant navigation to current location
16 | const originalPush = router.push
17 | router.push = function push(location) {
18 | return originalPush.call(this, location).catch((err) => err)
19 | }
20 |
21 | require('es6-promise').polyfill()
22 | require('fastclick').attach(document.body)
23 |
24 | import ConfirmPlugin from 'vux/src/plugins/confirm'
25 | import AlertPlugin from 'vux/src/plugins/alert'
26 | import ToastPlugin from 'vux/src/plugins/toast'
27 | Vue.use(ConfirmPlugin)
28 | Vue.use(AlertPlugin)
29 | Vue.use(ToastPlugin)
30 |
31 | Vue.config.productionTip = process.env.NODE_ENV == 'development'
32 |
33 | new Vue({
34 | router,
35 | store,
36 | i18n,
37 | render: (h) => h(App),
38 | }).$mount('#app')
39 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import Game from './views/Game.vue'
4 | import Settings from './views/Settings.vue'
5 |
6 | Vue.use(Router)
7 |
8 | export default new Router({
9 | routes: [
10 | {
11 | path: '/settings',
12 | name: 'settings',
13 | component: Settings,
14 | },
15 | {
16 | path: '/about',
17 | name: 'about',
18 | // route level code-splitting
19 | // this generates a separate chunk (about.[hash].js) for this route
20 | // which is lazy-loaded when the route is visited.
21 | component: () =>
22 | import(/* webpackChunkName: "about" */ /* webpackPrefetch: true */ './views/About.vue'),
23 | },
24 | {
25 | path: '/:pos?',
26 | name: 'game',
27 | component: Game,
28 | },
29 | ],
30 | })
31 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import ai from './modules/ai'
4 | import position from './modules/position'
5 | import settings from './modules/settings'
6 | import { threads, simd, relaxedSimd } from 'wasm-feature-detect'
7 |
8 | Vue.use(Vuex)
9 |
10 | export default new Vuex.Store({
11 | modules: {
12 | ai,
13 | position,
14 | settings,
15 | },
16 | state: {
17 | screenWidth: document.documentElement.clientWidth, // 屏幕宽度
18 | screenHeight: document.documentElement.clientHeight, // 屏幕高度
19 | isOnIOSBrowser:
20 | (/iPad|iPhone|iPod/.test(navigator.platform) ||
21 | (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) &&
22 | !window.MSStream,
23 | maxThreads: 1,
24 | supportThreads: false,
25 | supportSimd: false,
26 | supportRelaxedSimd: false,
27 | },
28 | getters: {
29 | boardCanvasWidth(state) {
30 | const MinBoardWidth = 300
31 | const BottomPadding = 200
32 | return Math.max(
33 | Math.min(state.screenWidth, state.screenHeight - BottomPadding),
34 | MinBoardWidth
35 | )
36 | },
37 | },
38 | mutations: {
39 | setScreenSize(state, payload) {
40 | state.screenWidth = payload.width
41 | state.screenHeight = payload.height
42 | },
43 | setBrowserCapability(state, { maxThreads, supportThreads, supportSimd, supportRelaxedSimd }) {
44 | state.maxThreads = maxThreads
45 | state.supportThreads = supportThreads
46 | state.supportSimd = supportSimd
47 | state.supportRelaxedSimd = supportRelaxedSimd
48 | },
49 | },
50 | actions: {
51 | async getBrowserCapabilities({ commit, rootState }) {
52 | const supportThreads = await threads()
53 | const supportSimd = await simd()
54 | const supportRelaxedSimd = await relaxedSimd()
55 | const maxThreads = supportThreads ? navigator.hardwareConcurrency : 1
56 | commit('setBrowserCapability', {
57 | maxThreads,
58 | supportThreads,
59 | supportSimd,
60 | supportRelaxedSimd,
61 | })
62 | if (rootState.settings.threads === null) {
63 | commit('settings/setValue', {
64 | key: 'threads',
65 | value: Math.max(1, maxThreads / 2),
66 | })
67 | }
68 | },
69 | },
70 | strict: process.env.NODE_ENV !== 'production',
71 | })
72 |
--------------------------------------------------------------------------------
/src/store/modules/ai.js:
--------------------------------------------------------------------------------
1 | import * as engine from '@/ai/engine'
2 | import { RENJU, CONFIGS } from './settings'
3 |
4 | const state = {
5 | loadingProgress: 0.0,
6 | ready: false,
7 | startSize: 0,
8 | restart: false,
9 | thinking: false,
10 | timeUsed: 0,
11 | lastThinkTime: 0,
12 | lastThinkPosition: [],
13 | currentConfig: null,
14 | hashSize: null,
15 | outputs: {
16 | pos: null,
17 | swap: null,
18 | currentPV: 0,
19 | pv: [
20 | {
21 | depth: 0,
22 | seldepth: 0,
23 | nodes: 0,
24 | eval: '-',
25 | winrate: 0.0,
26 | bestline: [],
27 | },
28 | ],
29 | nodes: 0,
30 | speed: 0,
31 | time: 0,
32 | msg: null,
33 | realtime: {
34 | best: [],
35 | lost: [],
36 | },
37 | forbid: [],
38 | error: null,
39 | },
40 | messages: [],
41 | posCallback: null,
42 | }
43 |
44 | const getters = {
45 | bestlineStr: (state) => (pvIdx) => {
46 | if (!pvIdx) pvIdx = 0
47 | let posStrs = []
48 | for (let p of state.outputs.pv[pvIdx].bestline) {
49 | const coordX = String.fromCharCode('A'.charCodeAt(0) + p[0])
50 | const coordY = (state.startSize - p[1]).toString()
51 | posStrs.push(coordX + coordY)
52 | }
53 | return posStrs.join(' ')
54 | },
55 | }
56 |
57 | const mutations = {
58 | setLoadingProgress(state, progress) {
59 | state.loadingProgress = progress
60 | },
61 | setReady(state, ready) {
62 | state.ready = ready
63 | },
64 | setThinkingState(state, thinking) {
65 | state.thinking = thinking
66 | },
67 | setStartSize(state, size) {
68 | state.startSize = size
69 | },
70 | setRestart(state, enabled = true) {
71 | state.restart = enabled
72 | },
73 | clearUsedTime(state) {
74 | state.timeUsed = 0
75 | },
76 | addUsedTime(state) {
77 | state.timeUsed += Date.now() - state.lastThinkTime
78 | },
79 | setThinkStartTime(state) {
80 | state.lastThinkTime = Date.now()
81 | },
82 | setCurrentConfig(state, config) {
83 | state.currentConfig = config
84 | },
85 | setHashSize(state, hashSize) {
86 | state.hashSize = hashSize
87 | },
88 | addMessage(state, msg) {
89 | state.messages.push(msg)
90 | },
91 | clearMessage(state) {
92 | state.messages = []
93 | },
94 | setOutput(state, output) {
95 | state.outputs[output.key] = output.value
96 | },
97 | setCurrentPV(state, pvIdx) {
98 | state.outputs.currentPV = pvIdx
99 | },
100 | setPVOutput(state, output) {
101 | let pv = state.outputs.pv[state.outputs.currentPV]
102 | if (!pv) {
103 | state.outputs.pv[state.outputs.currentPV] = {}
104 | pv = state.outputs.pv[state.outputs.currentPV]
105 | }
106 | pv[output.key] = output.value
107 | },
108 | clearOutput(state) {
109 | state.outputs.pv = [
110 | {
111 | depth: 0,
112 | seldepth: 0,
113 | nodes: 0,
114 | eval: '-',
115 | winrate: 0.0,
116 | bestline: [],
117 | },
118 | ]
119 | state.outputs.pos = null
120 | state.outputs.nodes = 0
121 | state.outputs.speed = 0
122 | state.outputs.forbid = []
123 | },
124 | addRealtime(state, rt) {
125 | state.outputs.realtime[rt.type].push(rt.pos)
126 | },
127 | clearRealtime(state, type) {
128 | state.outputs.realtime[type] = []
129 | },
130 | setPosCallback(state, callback) {
131 | state.posCallback = callback
132 | },
133 | sortPV(state) {
134 | let isPosEqual = (m) =>
135 | state.outputs.pos ? m[0] == state.outputs.pos[0] && m[1] == state.outputs.pos[1] : false
136 | let evalStrToEval = (e) => {
137 | let val = +e
138 | if (isNaN(val)) {
139 | if (e.startsWith('+M')) val = 40000 - +e.substring(2)
140 | else if (e.startsWith('-M')) val = -40000 + +e.substring(2)
141 | else val = -80000
142 | }
143 | return val
144 | }
145 |
146 | state.outputs.pv.sort((a, b) => {
147 | if (isPosEqual(a.bestline[0])) return -1
148 | else if (isPosEqual(b.bestline[0])) return 1
149 | return evalStrToEval(b.eval) - evalStrToEval(a.eval)
150 | })
151 | },
152 | setLastThinkPosition(state, position) {
153 | state.lastThinkPosition = [...position]
154 | },
155 | }
156 |
157 | const actions = {
158 | async initEngine({ commit, dispatch, state }) {
159 | commit('setLoadingProgress', 0.0)
160 | commit('setReady', false)
161 | const callback = (r) => {
162 | if (r.realtime) {
163 | switch (r.realtime.type) {
164 | case 'LOST':
165 | commit('addRealtime', { type: 'lost', pos: r.realtime.pos })
166 | break
167 | case 'BEST':
168 | commit('clearRealtime', 'best')
169 | commit('addRealtime', { type: 'best', pos: r.realtime.pos })
170 | break
171 | default:
172 | break
173 | }
174 | } else if (r.msg) {
175 | commit('setOutput', { key: 'msg', value: r.msg })
176 | commit('addMessage', r.msg)
177 | } else if (r.multipv) {
178 | if (r.multipv == 'DONE') commit('setCurrentPV', 0)
179 | else commit('setCurrentPV', +r.multipv)
180 | } else if (r.depth) {
181 | commit('setPVOutput', { key: 'depth', value: r.depth })
182 | } else if (r.seldepth) {
183 | commit('setPVOutput', { key: 'seldepth', value: r.seldepth })
184 | } else if (r.nodes) {
185 | commit('setPVOutput', { key: 'nodes', value: r.nodes })
186 | } else if (r.totalnodes) {
187 | commit('setOutput', { key: 'nodes', value: r.totalnodes })
188 | } else if (r.totaltime) {
189 | commit('setOutput', { key: 'time', value: r.totaltime })
190 | } else if (r.speed) {
191 | commit('setOutput', { key: 'speed', value: r.speed })
192 | } else if (r.eval) {
193 | commit('setPVOutput', { key: 'eval', value: r.eval })
194 | } else if (r.winrate) {
195 | commit('setPVOutput', { key: 'winrate', value: r.winrate })
196 | } else if (r.bestline) {
197 | commit('setPVOutput', { key: 'bestline', value: r.bestline })
198 | } else if (r.pos) {
199 | commit('setOutput', { key: 'pos', value: r.pos })
200 | commit('addUsedTime')
201 | commit('clearRealtime', 'best')
202 | commit('clearRealtime', 'lost')
203 | commit('sortPV')
204 | commit('setThinkingState', false)
205 | if (state.posCallback) state.posCallback(r.pos)
206 | } else if (r.swap) {
207 | commit('setOutput', { key: 'swap', value: r.swap })
208 | } else if (r.forbid) {
209 | commit('setOutput', { key: 'forbid', value: r.forbid })
210 | } else if (r.error) {
211 | commit('setOutput', { key: 'error', value: r.error })
212 | commit('addMessage', 'Error: ' + r.error)
213 | } else if (r.ok) {
214 | commit('addMessage', 'Engine ready.')
215 | commit('setReady', true)
216 | dispatch('checkForbid')
217 | } else if (r.loading) {
218 | commit('setLoadingProgress', r.loading.progress)
219 | }
220 | }
221 | const engineURL = await engine.init(callback)
222 | commit('addMessage', 'Engine: ' + engineURL)
223 | },
224 | sendInfo({ rootState, rootGetters }) {
225 | engine.sendCommand('INFO RULE ' + rootState.settings.rule)
226 | engine.sendCommand('INFO THREAD_NUM ' + (rootState.settings.threads || 1))
227 | engine.sendCommand('INFO CAUTION_FACTOR ' + rootState.settings.candRange)
228 | engine.sendCommand('INFO STRENGTH ' + rootState.settings.strength)
229 | engine.sendCommand('INFO TIMEOUT_TURN ' + rootGetters['settings/turnTime'])
230 | engine.sendCommand('INFO TIMEOUT_MATCH ' + rootGetters['settings/matchTime'])
231 | engine.sendCommand('INFO MAX_DEPTH ' + rootGetters['settings/depth'])
232 | engine.sendCommand('INFO MAX_NODE ' + rootGetters['settings/nodes'])
233 | engine.sendCommand('INFO SHOW_DETAIL ' + (rootState.settings.showDetail ? 3 : 2))
234 | engine.sendCommand('INFO PONDERING ' + (rootState.settings.pondering ? 1 : 0))
235 | engine.sendCommand('INFO SWAPABLE ' + (rootState.position.swaped ? 0 : 1))
236 | },
237 | sendBoard({ rootState }, immediateThink) {
238 | let position = rootState.position.position
239 |
240 | let command = immediateThink ? 'BOARD' : 'YXBOARD'
241 | let side = position.length % 2 == 0 ? 1 : 2
242 | for (let pos of position) {
243 | command += ' ' + pos[0] + ',' + pos[1] + ',' + side
244 | side = 3 - side
245 | }
246 | command += ' DONE'
247 | engine.sendCommand(command)
248 | },
249 | think({ commit, dispatch, state, rootState, rootGetters }, args) {
250 | if (!state.ready) {
251 | commit('addMessage', 'Engine is not ready!')
252 | return Promise.reject(new Error('Engine is not ready!'))
253 | }
254 | commit('setThinkingState', true)
255 | commit('setOutput', { key: 'swap', value: false })
256 | commit('clearMessage')
257 |
258 | dispatch('reloadConfig')
259 | dispatch('updateHashSize')
260 | dispatch('sendInfo')
261 |
262 | if (state.restart || state.startSize != rootState.position.size) {
263 | engine.sendCommand('START ' + rootState.position.size)
264 | commit('setRestart', false)
265 | commit('setStartSize', rootState.position.size)
266 | commit('clearUsedTime')
267 | }
268 |
269 | let timeLeft = Math.max(rootGetters['settings/matchTime'] - state.timeUsed, 1)
270 | engine.sendCommand('INFO TIME_LEFT ' + timeLeft)
271 |
272 | dispatch('sendBoard', false)
273 | commit('setThinkStartTime')
274 | commit('setLastThinkPosition', rootState.position.position)
275 | commit('clearOutput')
276 |
277 | if (args && args.balanceMode) {
278 | engine.sendCommand(
279 | (args.balanceMode == 2 ? 'YXBALANCETWO ' : 'YXBALANCEONE ') + (args.balanceBias || 0)
280 | )
281 | } else {
282 | engine.sendCommand('YXNBEST ' + rootState.settings.nbest)
283 | }
284 |
285 | return new Promise((resolve) => {
286 | commit('setPosCallback', resolve)
287 | })
288 | },
289 | stop({ commit, state }) {
290 | if (!state.thinking) return
291 |
292 | if (engine.stopThinking()) {
293 | commit('setReady', false)
294 | commit('clearRealtime', 'best')
295 | commit('clearRealtime', 'lost')
296 | commit('addUsedTime')
297 | commit('sortPV')
298 | commit('setThinkingState', false)
299 | let pos = state.outputs.pv[0].bestline[0]
300 | if (pos) {
301 | commit('setOutput', { key: 'pos', value: pos })
302 | if (state.posCallback) state.posCallback(pos)
303 | }
304 | commit('setRestart')
305 | // Reset to initial state
306 | commit('setCurrentConfig', null)
307 | commit('setHashSize', null)
308 | return true
309 | }
310 |
311 | return false
312 | },
313 | restart({ commit }) {
314 | commit('setRestart')
315 | commit('clearUsedTime')
316 | commit('clearOutput')
317 | },
318 | reloadConfig({ commit, state, rootState }) {
319 | if (rootState.settings.configIndex == state.currentConfig) return
320 | commit('setCurrentConfig', rootState.settings.configIndex)
321 |
322 | engine.sendCommand('RELOADCONFIG ' + (CONFIGS[state.currentConfig] || ''))
323 | },
324 | updateHashSize({ commit, state, rootState }) {
325 | if (rootState.settings.hashSize == state.hashSize) return
326 | commit('setHashSize', rootState.settings.hashSize)
327 |
328 | engine.sendCommand('INFO HASH_SIZE ' + state.hashSize * 1024)
329 | commit('addMessage', `Hash size reset to ${state.hashSize} MB.`)
330 | },
331 | checkForbid({ commit, state, dispatch, rootState, rootGetters }) {
332 | commit('setOutput', { key: 'forbid', value: [] })
333 | if (!state.ready) return
334 |
335 | if (rootGetters['settings/gameRule'] == RENJU) {
336 | dispatch('sendInfo')
337 | if (state.restart || state.startSize != rootState.position.size) {
338 | engine.sendCommand('START ' + rootState.position.size)
339 | commit('setRestart', false)
340 | commit('setStartSize', rootState.position.size)
341 | }
342 | dispatch('sendBoard', false)
343 | engine.sendCommand('YXSHOWFORBID')
344 | }
345 | },
346 | }
347 |
348 | export default {
349 | namespaced: true,
350 | state,
351 | getters,
352 | actions,
353 | mutations,
354 | }
355 |
--------------------------------------------------------------------------------
/src/store/modules/position.js:
--------------------------------------------------------------------------------
1 | import { STANDARD, RENJU } from './settings'
2 |
3 | const EMPTY = 0,
4 | BLACK = 1,
5 | WHITE = 2
6 |
7 | function toIndex(p, size) {
8 | if (p[0] < 0 || p[1] < 0 || p[0] >= size || p[1] >= size) return -1
9 | else return p[1] * size + p[0]
10 | }
11 |
12 | function checkLine(board, pos, delta, size, exactFive = false) {
13 | let piece = board[toIndex(pos, size)]
14 | let count = 1,
15 | i,
16 | j
17 | for (i = 1; ; i++) {
18 | let x = pos[0] + delta[0] * i
19 | let y = pos[1] + delta[1] * i
20 | let index = toIndex([x, y], size)
21 | if (index == -1) break
22 | if (board[index] == piece) count++
23 | else break
24 | }
25 | for (j = 1; ; j++) {
26 | let x = pos[0] - delta[0] * j
27 | let y = pos[1] - delta[1] * j
28 | let index = toIndex([x, y], size)
29 | if (index == -1) break
30 | if (board[index] == piece) count++
31 | else break
32 | }
33 | if (exactFive ? count == 5 : count >= 5) return [1 - j, i - 1]
34 | }
35 |
36 | const state = {
37 | size: 15,
38 | board: null,
39 | position: [],
40 | /* 棋子序列数组,每个元素为一个表示坐标的二维向量[x, y] */
41 | lastPosition: [],
42 | winline: [],
43 | swaped: false,
44 | }
45 |
46 | const getters = {
47 | posStr: (state) => {
48 | let posStrs = []
49 | for (let p of state.position) {
50 | posStrs.push(String.fromCharCode('a'.charCodeAt(0) + p[0]))
51 | posStrs.push(state.size - p[1])
52 | }
53 | return posStrs.join('')
54 | },
55 | get: (state) => {
56 | return (pos) => {
57 | switch (state.board[toIndex(pos, state.size)]) {
58 | case BLACK:
59 | return 'BLACK'
60 | case WHITE:
61 | return 'WHITE'
62 | case EMPTY:
63 | return 'EMPTY'
64 | default:
65 | return 'ERROR'
66 | }
67 | }
68 | },
69 | isEmpty: (state) => {
70 | return (pos) => {
71 | return state.board[toIndex(pos, state.size)] == EMPTY
72 | }
73 | },
74 | isInBoard: (state) => {
75 | return (pos) => {
76 | return toIndex(pos, state.size) != -1
77 | }
78 | },
79 | playerToMove: (state) => {
80 | return state.position.length % 2 == 0 ? 'BLACK' : 'WHITE'
81 | },
82 | moveLeftCount: (state) => {
83 | return state.size * state.size - state.position.length
84 | },
85 | marchPosition: (state) => {
86 | return (position) => {
87 | let len = Math.min(position.length, state.position)
88 | let i = 0
89 | for (; i < len; i++) {
90 | let pos1 = position[i]
91 | let pos2 = state.position[i]
92 | if (pos1[0] != pos2[0] || pos1[1] != pos2[1]) break
93 | }
94 | return i
95 | }
96 | },
97 | }
98 |
99 | const mutations = {
100 | new(state, size) {
101 | state.size = size
102 | state.board = new Uint8Array(state.size * state.size).fill(EMPTY)
103 | state.lastPosition = []
104 | state.position = []
105 | state.winline = []
106 | state.swaped = false
107 | },
108 | move(state, pos) {
109 | state.board[pos[1] * state.size + pos[0]] = state.position.length % 2 == 0 ? BLACK : WHITE
110 | state.position.push(pos)
111 |
112 | let lastPos = state.lastPosition[state.position.length - 1]
113 | if (!lastPos || lastPos[0] != pos[0] || lastPos[1] != pos[1]) {
114 | state.lastPosition = [...state.position] // 走向不同的分支
115 | }
116 | },
117 | undo(state) {
118 | let pos = state.position.pop()
119 | state.board[pos[1] * state.size + pos[0]] = EMPTY
120 | state.winline = []
121 | },
122 | checkWin(state, checkOverline) {
123 | if (state.position.length < 9) return
124 |
125 | let lastPos = state.position[state.position.length - 1]
126 | const dirs = [
127 | [1, 0],
128 | [0, 1],
129 | [1, 1],
130 | [1, -1],
131 | ]
132 | for (let dir of dirs) {
133 | let ret = checkLine(state.board, lastPos, dir, state.size, checkOverline)
134 | if (ret) {
135 | return (state.winline = [
136 | [lastPos[0] + ret[0] * dir[0], lastPos[1] + ret[0] * dir[1]],
137 | [lastPos[0] + ret[1] * dir[0], lastPos[1] + ret[1] * dir[1]],
138 | ])
139 | }
140 | }
141 | },
142 | setSwaped(state) {
143 | state.swaped = true
144 | },
145 | }
146 |
147 | const actions = {
148 | setPosStr({ commit, dispatch, getters, state }, str) {
149 | str = str.trim().toLowerCase()
150 | let posArray = str.match(/([a-z])(\d+)/g)
151 | if (getters.posStr == str) return
152 |
153 | commit('new', state.size)
154 | if (!posArray) return
155 | for (let p of posArray) {
156 | let x = p.match(/[a-z]/)[0].charCodeAt(0) - 'a'.charCodeAt(0)
157 | let y = state.size - +p.match(/\d+/)[0]
158 | dispatch('makeMove', [x, y])
159 | if (state.winline.length > 0) break
160 | }
161 | },
162 | makeMove({ commit, dispatch, getters, rootGetters }, pos) {
163 | if (!getters.isEmpty(pos)) return false
164 | let checkOverline =
165 | rootGetters['settings/gameRule'] == STANDARD ||
166 | (rootGetters['settings/gameRule'] == RENJU && getters.playerToMove == 'BLACK')
167 | commit('move', pos)
168 | commit('checkWin', checkOverline)
169 | dispatch('ai/checkForbid', {}, { root: true })
170 | return true
171 | },
172 | backward({ commit, dispatch }) {
173 | if (state.position.length == 0) return
174 | commit('undo')
175 | dispatch('ai/checkForbid', {}, { root: true })
176 | },
177 | forward({ commit, dispatch, state, rootGetters }) {
178 | if (state.lastPosition.length <= state.position.length) return
179 | let checkOverline =
180 | rootGetters['settings/gameRule'] == STANDARD ||
181 | (rootGetters['settings/gameRule'] == RENJU && getters.playerToMove == 'BLACK')
182 | commit('move', state.lastPosition[state.position.length])
183 | commit('checkWin', checkOverline)
184 | dispatch('ai/checkForbid', {}, { root: true })
185 | },
186 | backToBegin({ dispatch, state }) {
187 | while (state.position.length > 0) {
188 | dispatch('backward')
189 | }
190 | },
191 | forwardToEnd({ dispatch, state }) {
192 | while (state.lastPosition.length > state.position.length) dispatch('forward')
193 | },
194 | rotate({ commit, dispatch, state }) {
195 | let position = state.position
196 | commit('new', state.size)
197 | for (let p of position) {
198 | dispatch('makeMove', [state.size - 1 - p[1], p[0]])
199 | }
200 | },
201 | flip({ commit, dispatch, state }, dir) {
202 | let position = state.position
203 | commit('new', state.size)
204 | for (let p of position) {
205 | if (dir[0] == 0 && dir[1] == 0) p = [p[1], p[0]]
206 | else if (dir[0] == 1 && dir[1] == 1) p = [state.size - 1 - p[1], state.size - 1 - p[0]]
207 | else if (dir[0] == 1) p[0] = state.size - 1 - p[0]
208 | else if (dir[1] == 1) p[1] = state.size - 1 - p[1]
209 | dispatch('makeMove', p)
210 | }
211 | },
212 | moveTowards({ commit, dispatch, state }, dir) {
213 | let position = state.position
214 | commit('new', state.size)
215 | for (let p of position) {
216 | dispatch('makeMove', [p[0] + dir[0], p[1] + dir[1]])
217 | }
218 | },
219 | }
220 |
221 | export default {
222 | namespaced: true,
223 | state,
224 | getters,
225 | actions,
226 | mutations,
227 | }
228 |
--------------------------------------------------------------------------------
/src/store/modules/settings.js:
--------------------------------------------------------------------------------
1 | import { version } from '@/../package.json'
2 |
3 | export const FREESTYLE = 0,
4 | STANDARD = 1,
5 | RENJU = 2
6 |
7 | export const CONFIGS = ['config.toml', 'classical220723.toml', 'classical210901.toml']
8 |
9 | const state = {
10 | language: null,
11 | boardStyle: {
12 | boardColor: '#F4D03F',
13 | lineColor: '#000000',
14 | lineWidth: 0.03,
15 | coordColor: '#000000',
16 | coordFontStyle: '',
17 | coordFontFamily: 'sans-serif',
18 | starRadiusScale: 0.1,
19 | pieceBlack: '#000000',
20 | pieceWhite: '#FFFFFF',
21 | pieceStrokeWidth: 0.021,
22 | pieceStrokeBlack: '#000000',
23 | pieceStrokeWhite: '#000000',
24 | pieceScale: 0.95,
25 | indexColorBlack: '#FFFFFF',
26 | indexColorWhite: '#000000',
27 | indexFontStyle: 'bold',
28 | indexFontFamily: 'sans-serif',
29 | indexScale: 0.45,
30 | lastStepColor: '#E74C3C',
31 | lastStepScale: 0.15,
32 | winlineWidth: 0.12,
33 | winlineColor: '#2E86C1',
34 | bestMoveColor: '#E74C3C',
35 | thoughtMoveColor: '#3C5EE7',
36 | lostMoveColor: '#FDFEFE',
37 | bestMoveScale: 0.12,
38 | realtimeMoveScale: 0.09,
39 | selectionStrokeWidth: 0.08,
40 | selectionStrokeColor: '#E74C3C',
41 | forbidStrokeWidth: 0.12,
42 | forbidStrokeColor: '#E74C3C',
43 | pvEvalFontStyle: 600,
44 | pvEvalFontFamily: 'sans-serif',
45 | pvEvalScale: 0.45,
46 | pvEvalAlpha: 0.9,
47 | },
48 | boardSize: 15,
49 | thinkTimeOption: 1,
50 | turnTime: 5000,
51 | matchTime: 9999000,
52 | maxDepth: 64,
53 | maxNodes: 0,
54 | rule: 0, // 规则: 0-无禁手 1-无禁长连不赢 2,4-有禁手 5-无禁一手交换
55 | threads: null, // 线程数
56 | strength: 100, // 棋力限制 (默认100%棋力)
57 | nbest: 1, // MultiPV多点分析
58 | configIndex: 0, // 配置序号: [0, CONFIGS.length)
59 | candRange: 3, // 选点范围: {0, 1, 2, 3, 4, 5}
60 | hashSize: 128, // 置换表大小, 单位 MiB
61 | pondering: false, // 后台思考
62 | clickCheck: 0, // 点击方式: 0-直接落子 1-二次确认 2-滑动落子
63 | indexOrigin: 0, // 棋子序号起点
64 | showCoord: true,
65 | showAnalysis: true,
66 | showDetail: true,
67 | showPvEval: 0, // 是否显示实时估值: 0-不显示 1-显示估值 2-显示胜率
68 | showIndex: true,
69 | showLastStep: true,
70 | showWinline: true,
71 | showForbid: true,
72 | aiThinkBlack: false,
73 | aiThinkWhite: false,
74 | }
75 |
76 | const propertiesToSave = [
77 | 'language',
78 | 'boardSize',
79 | 'thinkTimeOption',
80 | 'turnTime',
81 | 'matchTime',
82 | 'maxDepth',
83 | 'maxNodes',
84 | 'rule',
85 | 'threads',
86 | 'strength',
87 | 'nbest',
88 | 'configIndex',
89 | 'candRange',
90 | 'hashSize',
91 | 'pondering',
92 | 'clickCheck',
93 | 'showCoord',
94 | 'showAnalysis',
95 | 'showDetail',
96 | 'showPvEval',
97 | 'showIndex',
98 | 'showLastStep',
99 | 'showWinline',
100 | 'showForbid',
101 | 'aiThinkBlack',
102 | 'aiThinkWhite',
103 | ]
104 |
105 | const boardPropertiesToSave = [
106 | 'boardColor',
107 | 'lastStepColor',
108 | 'winlineColor',
109 | 'bestMoveColor',
110 | 'thoughtMoveColor',
111 | 'lostMoveColor',
112 | ]
113 |
114 | const getters = {
115 | turnTime: (state) => {
116 | let turn = [state.turnTime, 7000, 40000, -1]
117 | return turn[state.thinkTimeOption]
118 | },
119 | matchTime: (state) => {
120 | let match = [state.matchTime, 180000, 900000, -1]
121 | return match[state.thinkTimeOption]
122 | },
123 | depth: (state) => {
124 | return state.thinkTimeOption == 0 ? state.maxDepth : 100
125 | },
126 | nodes: (state) => {
127 | return state.thinkTimeOption == 0 ? state.maxNodes : 0
128 | },
129 | gameRule: (state) => {
130 | switch (state.rule) {
131 | case 0:
132 | case 5:
133 | return FREESTYLE
134 | case 1:
135 | return STANDARD
136 | case 2:
137 | case 4:
138 | return RENJU
139 | default:
140 | throw Error('unknown rule')
141 | }
142 | },
143 | }
144 |
145 | function saveCookies() {
146 | let stateToSave = {}
147 | for (let p of propertiesToSave) stateToSave[p] = state[p]
148 | for (let p of boardPropertiesToSave) stateToSave[p] = state.boardStyle[p]
149 | localStorage.setItem('GMKC_CFG_' + version, JSON.stringify(stateToSave))
150 | }
151 |
152 | const mutations = {
153 | setValue(state, payload) {
154 | state[payload.key] = payload.value
155 | if (propertiesToSave.includes(payload.key)) saveCookies()
156 | },
157 | setValueNoSave(state, payload) {
158 | state[payload.key] = payload.value
159 | },
160 | setBoardStyle(state, payload) {
161 | state.boardStyle[payload.key] = payload.value
162 | if (boardPropertiesToSave.includes(payload.key)) saveCookies()
163 | },
164 | setBoardStyleNoSave(state, payload) {
165 | state.boardStyle[payload.key] = payload.value
166 | },
167 | }
168 |
169 | const actions = {
170 | readCookies({ commit }) {
171 | let json = localStorage.getItem('GMKC_CFG_' + version)
172 | if (!json) return
173 |
174 | let stateToRead = JSON.parse(json)
175 | for (let p of propertiesToSave) commit('setValueNoSave', { key: p, value: stateToRead[p] })
176 | for (let p of boardPropertiesToSave)
177 | commit('setBoardStyleNoSave', { key: p, value: stateToRead[p] })
178 | },
179 | clearCookies() {
180 | localStorage.removeItem('GMKC_CFG_' + version)
181 | },
182 | }
183 |
184 | export default {
185 | namespaced: true,
186 | state,
187 | getters,
188 | actions,
189 | mutations,
190 | }
191 |
--------------------------------------------------------------------------------
/src/theme.less:
--------------------------------------------------------------------------------
1 | @background-color: #f1f2f3;
2 |
3 | @header-background-color: #2E86C1;
4 |
5 | @tabber-icon-active-color: #09BB07;
--------------------------------------------------------------------------------
/src/views/About-en.md:
--------------------------------------------------------------------------------
1 | ## Gomoku Calculator V0.30
2 |
3 | ### FAQ
4 |
5 | + What can this app do?
6 |
7 | It can be used as a gomoku board, or used to analyze gomoku positions with the help of AI. You can also play against the AI.
8 |
9 | + There are already many gomoku software on mobile platform, does this app have anything special?
10 |
11 | Most gomoku apps on mobile platform do not provide detailed analysis, meanwhile other apps providing analysis support limited platforms. As this app is fundamentally a web page, it can be run on almost every devices with an appropriate browser, which is more convenient.
12 |
13 | + When using in mobile browser, the address bar takes a lot of screen space. Is there any way to using the app in fullscreen mode?
14 |
15 | For Safari browser on IOS and Chrome browser on Android, it is recommended to choose the "**Add to home screen**" option to gain use experience similar to a native app.
16 |
17 | + What gomoku AI is used?
18 |
19 | This app uses Rapfi engine, which participated in Gomocup 2021. With the use of web-assembly technology, rapfi can achieve performance close to native app, and doesn't need to communicate with the server when thinking.
20 |
21 | + Which rules are supported?
22 |
23 | Currently supported rules include "Freestyle Gomoku", "Standard Gomoku", "Free Renju" and "Freestyle Gomoku - SWAP1".
24 |
25 | + How to make the AI move automatically?
26 |
27 | Select "**AI plays Black**" or "**AI plays White**" in setting page, then when it is time for AI to move, click anywhere on the board and AI will start to think.
28 |
29 | + What is the meaning of "+M" and "-M" in evaluation?
30 |
31 | If "+M" or "-M" is shown in evaluation, it means AI have found a forced win or loss, and the number after that is the max step to end the game.
32 |
33 | + What is the difference of analysis mode besides infinite time?
34 |
35 | In analysis mode, AI does not stop thinking when found a force win or loss, because continuing the thinking is more likely to find a better solution with less steps.
36 |
37 | + How to let AI analyze the N best lines?
38 |
39 | Set the "**Multi PV**" option to N, the AI will give the N best principal variations after thinking. Note: When you do not need this feature, leave it at 1 for best performance.
40 |
41 | + How to reduce the difficulty of AI / increase the diversity of AI moves?
42 |
43 | The "**Handicap**" option can increase the randomness of the AI's moves and significantly reduce the AI's strength. Greater value implies more reduction of strength (0 means no limitation of strength, 100 means maximum limitation of strength). In addition, reducing calculation time/nodes will also reduce the AI's strength, but it will not increase the randomness of AI's move.
44 |
45 | + What is a **balanced move**? What is the use of calculating a balanced move?
46 |
47 | The one-step/two-step **balanced moves** can make the position evaluation tend to zero. After completing the one/two moves, black and white players will get a (approximately) balanced opening. The estimated value obtained by search represents the evaluation of the balanced move, and the closer the value is to 0, the more balanced is the position. Similar to normal calculations, the credibility of the value increases as the search depth increases. Balanced move calculation is very useful under some rules (such as SWAP2).
48 |
49 | + How should I set the **number of threads**? What is the use of **pondering**?
50 |
51 | Multi-threading allows AI to use multiple CPU cores for calculations, which can significantly improve search speed and thinking skills. If you want to use all the computing power to get the best playing strength, it is recommended to set "**Thread Num**" to the maximum. If you turn on **Pondering**, AI will use the opponent's time to think.
52 |
53 | Note: If you do not see this option, your browser does not support multi-threaded computing. Currently, only some browsers support multi-threading. You can check browsers support in [here](https://caniuse.com/sharedarraybuffer).
54 |
55 | + What is "engine model"? Which engine model should I choose?
56 |
57 | Engine model provides information such as evaluation used by AI when thinking, and will have a certain impact on the playing strength and playing style of the AI. In general, the "latest" model has the best strength per unit time, thus it is recommended to use the "latest" model.
58 |
59 | + How should I set the "**Hash Table Size**"?
60 |
61 | If you need to analyze for a long time, or use multiple threads, you will need a larger hash table. For fast calculations, use the default hash table size.
62 |
63 | + My screen is small, how to avoid clicking the wrong position?
64 |
65 | Select the click manner to "**Secondary Confirmation**" or "**Slide to Move**". "Secondary Confirmation" needs you to click twice to confirm a move, while "Slide to Move" makes move according to the selection box when pressed.
66 |
67 | + How to get/set position code?
68 |
69 | The "current pos" field in "game" page“ is the current position code, which you can copy or modify freely.
70 |
71 | + How to share a position to others?
72 |
73 | You can click "Shot" button in the button bar to get a picture of current position, then you can long press to save the pic. Besides, you can just copy the link in browser, and open it on other places to get the position.
74 |
75 | + How can I save a position?
76 |
77 | Currently, it needs you to save the position code or link manually.
78 |
79 | + How to preview bestline calcuated by AI?
80 |
81 | Hover on one bestline move to preview bestlines calcuated by AI currently. On desktop client you can set current position to bestline by double-clicking bestline move.
82 |
83 | + Can keyboard shortcuts be used?
84 |
85 | You can use the left/right arrow keys on the keyboard to move backward/forward through the board states, the HOME/END keys to jump to the start/end of the board states, the spacebar to start or stop calculations, and the "b/B" key to calculate one/two equilibrium points.
86 |
87 | + I found a bug? I want to make a suggestion?
88 |
89 | Welcome to submit an issue [here](https://github.com/gomocalc/gomocalc.github.io/issues) to feedback the bug or suggestion.
90 |
91 |
92 |
93 | ### Update Record
94 |
95 | + 0.30
96 | + engine update,add mix9lite model
97 | + more language support
98 | + add some keyboard shortcuts
99 | + 0.26
100 | + fix performance issue for multi-threading
101 | + engine weight update
102 | + 0.25
103 | + add option for adjusting candidate range
104 | + add winrate display
105 | + 0.24
106 | + fix bug on move generation
107 | + update engine weights
108 | + 0.23
109 | + add option for strength handicap and pondering
110 | + 0.22
111 | + engine update, support for multi-threading
112 | + support for balanced move calculation
113 | + 0.21
114 | + fix wrong forbidden point judgement
115 | + 0.20
116 | + update engine to Rapfi2021, new support for Standard and Renju rule
117 | + 0.17
118 | + add option for transposition table size
119 | + 0.16
120 | + optimize landscape layout
121 | + bestline preview
122 | + 0.15
123 | + board color is changeable now
124 | + support for exporting high-res JPEG and GIF
125 | + realtime evaluation display of multi-PV analysis
126 | + 0.14
127 | + add display for multi pv mode
128 | + settings can be automatically saved now
129 | + 0.13
130 | + fix position error after going backward when AI is thinking
131 | + fix display of pv line field
132 |
133 |
134 |
135 | ### About the App
136 |
137 | App websites: [Main Site](https://gomocalc.com)
138 |
139 | This is an open-source application. Source code is available at: [Github](https://github.com/dhbloo/gomoku-calculator)
140 |
141 | Join our user community groups: [QQ Group](https://qm.qq.com/q/xj9OHByFTG), [Discord](https://discord.gg/7kEpFCGdb5)
142 |
--------------------------------------------------------------------------------
/src/views/About-zh-CN.md:
--------------------------------------------------------------------------------
1 | ## 五子棋计算器 V0.30
2 |
3 | ### FAQ
4 |
5 | + 这个应用可以干什么?
6 |
7 | 可以用来摆谱、借助AI分析五子棋局面,或是与AI对弈。
8 |
9 | + 手机端已经有许多五子棋软件了,该应用有什么不同之处吗?
10 |
11 | 大部分的移动端五子棋软件只能进行娱乐性的对弈,不能给出比较完整的分析功能,而少数带分析功能的软件支持的平台有限。由于该应用本质上是一个网页,因此可以在绝大部分安装了浏览器的设备上运行,使用更加方便。
12 |
13 | + 在手机浏览器里使用时总是显示地址栏,有什么方法全屏显示吗?
14 |
15 | 对于IOS端Safari浏览器和安卓端Chrome浏览器,推荐在浏览器中选择**“添加到主屏幕”**以获得类似原生应用的使用体验。
16 |
17 | + 使用了什么五子棋AI?
18 |
19 | 该应用采用了参加2021年Gomocup五子棋AI比赛的Rapfi引擎。借助Web Assembly技术,Rapfi能在网页端取得与原生应用相差不大的性能,并且思考时不需要与服务器通信。
20 |
21 | + 目前支持哪些规则?
22 |
23 | 目前支持的规则包括“无禁手”、“无禁手长连不胜”、“自由连珠”、“无禁手一手交换”。
24 |
25 | + 如何设置成对弈模式?
26 |
27 | 在设置中将“**AI执黑**”或“**AI执白**”选中,之后在轮到AI下棋时点击棋盘的任意位置便可以进入对弈模式。
28 |
29 | + 估值中的“+M”与“-M”是什么意思?
30 |
31 | 如果估值中出现了“+M”或“-M”,说明AI发现了必胜/必败,后面附加的数字为达成连五的最大步数。
32 |
33 | + 分析模式除了时间无限外还有什么不同点?
34 |
35 | 分析模式下,即使发现必胜/必败也不会停止思考,这样更可能发现步数最优的着法。
36 |
37 | + 如何让AI计算同时计算最优的n个点的分数和路线?
38 |
39 | 将设置中的“**多点分析**”设为需要的n值,AI思考后,会输出相应的最优n个点的分数与路线记录。注:多点分析模式下,AI由于需要计算更多的点,思考速度变慢,综合棋力会有所下降,建议只在必要时开启该功能。
40 |
41 | + 如何降低AI的难度/增加AI着法的多样性?
42 |
43 | “**限制棋力**”选项可以使得AI在着法上增加随机性,且显著降低AI的棋力。值越大棋力限制约显著(0为不限制棋力,100为最大程度限制棋力)。此外,减少计算时间/计算节点数也会使AI的棋力降低,不过并不会增加着法随机性。
44 |
45 | + 什么是**平衡点**?计算平衡点有什么用?
46 |
47 | 一步/两步**平衡点**指能够使局面估值趋于0的某一步/两步棋。在完成该一步/两步棋之后,黑白双方会得到一个(近似)平衡开局。此时搜索得到的估值表示平衡点的分数,分数越接近0表示越平衡。与正常计算类似,分数的可信度随着搜索深度的增加而增加。平衡点计算在一些规则(如SWAP2)下十分有用。
48 |
49 | + 我应该怎样设定**线程数**?**后台思考**有什么用?
50 |
51 | 多线程能够让AI使用多个CPU核心进行计算,能够显著提升搜索速度与思考棋力。若想要利用所有算力获得最佳的棋力,建议将**线程数**设定为最大值。若开启**后台思考**,AI会利用对手的时间进行思考。
52 |
53 | 注:如果你没有看见此选项,说明你的浏览器不支持多线程计算。目前仅有部分浏览器支持多线程,可以在[这里](https://caniuse.com/sharedarraybuffer)查询支持多线程的浏览器列表。
54 |
55 | + 引擎权重是什么?我应该选择什么引擎权重?
56 |
57 | 引擎权重提供了AI在思考时所采用的估值等信息,会对引擎的棋力与下棋风格具有一定影响。一般情况下,"最新"权重具有最佳的单位时间棋力,建议使用“最新”权重即可。
58 |
59 | + 我应该如何设定**置换表大小**?
60 |
61 | 若需要长时间分析,或是使用多线程,就需要更大的置换表。对于快速计算,采用默认的置换表大小即可。
62 |
63 | + 屏幕太小落子容易误触怎么办?
64 |
65 | 在设置中将落子方式设置为“**二次确认**”或“**滑动落子**”。“二次确认”落子方式需要在第一次点击位置附近再点击第二次;“滑动落子”根据按下时出现的选择框确定落子位置。
66 |
67 | + 如何导出/设置棋谱代码?
68 |
69 | “游戏”页面中的"局面代码"即为当前的棋谱代码,可以任意复制或者修改。
70 |
71 | + 怎么给其他人分享局面?
72 |
73 | 可以通过按钮栏中的“**截图**”按钮截取当前局面图片,再长按保存到移动设备或右键另存到电脑中。另外,如果是在浏览器中打开的页面,可以直接复制浏览器中带局面代码的链接,这样便可以通过链接直接打开相应局面。
74 |
75 | + 可以保存局面吗?
76 |
77 | 目前若要保存局面,需要手动保存复制的局面代码或是保存带有局面代码的网址。
78 |
79 | + 预览AI计算的最优路线?
80 |
81 | 悬停在AI输出的某个最优路线的着法上即可预览AI当前计算到的最优路线,在桌面端上双击某个最优路线着法可以将局面直接设置到该路线上。
82 |
83 | + 可以用键盘按键操作吗?
84 |
85 | 可以使用键盘上的左/右方向键进行局面的回退/前进,HOME/END键回到局面的起始/末尾,空格键进行计算或停止计算,b/B键计算一步/两步平衡点。
86 |
87 | + 我发现了bug?我想提出建议?
88 |
89 | 欢迎[在此](https://github.com/gomocalc/gomocalc.github.io/issues)提交issue反馈相应的bug或建议。
90 |
91 |
92 |
93 | ### 更新记录
94 |
95 | + 0.30
96 | + 引擎更新,加入mix9lite权重
97 | + 更多语言支持
98 | + 新增键盘按键操作
99 | + 0.26
100 | + 修复多线程下的速度问题
101 | + 权重更新
102 | + 0.25
103 | + 支持选点范围调节
104 | + 加入胜率显示
105 | + 0.24
106 | + 修复着法生成的bug
107 | + 更新引擎权重
108 | + 0.23
109 | + 加入棋力限制、后台思考功能
110 | + 0.22
111 | + 更新引擎,加入多线程支持
112 | + 加入计算平衡点的功能
113 | + 0.21
114 | + 修复禁手判断的bug
115 | + 0.20
116 | + 更新至Rapfi2021引擎,新增支持长连不胜与连珠规则
117 | + 0.17
118 | + 加入置换表大小选项
119 | + 0.16
120 | + 优化在宽屏下的布局
121 | + 加入路线预览
122 | + 0.15
123 | + 棋盘颜色可以修改了
124 | + 支持导出高清图片与GIF
125 | + 多线分析实时分数显示
126 | + 0.14
127 | + 加入了多点分析输出的显示
128 | + 设置现在能自动保存了
129 | + 0.13
130 | + 修复了AI思考时回退产生的局面错误
131 | + 修复了路线栏显示不全的问题
132 |
133 |
134 |
135 | ### 关于应用
136 |
137 | 应用地址:[主站](https://gomocalc.com)
138 |
139 | 本界面为开源应用,源代码地址:[Github](https://github.com/dhbloo/gomoku-calculator)
140 |
141 | 欢迎加入使用交流群组:[QQ群](https://qm.qq.com/q/xj9OHByFTG), [Discord](https://discord.gg/7kEpFCGdb5)
142 |
--------------------------------------------------------------------------------
/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
6 |
7 |
8 |
19 |
--------------------------------------------------------------------------------
/src/views/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
104 |
105 |
106 |
497 |
498 |
508 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | configureWebpack: (config) => {
3 | require('vux-loader').merge(config, {
4 | options: {},
5 | plugins: [
6 | 'vux-ui',
7 | {
8 | name: 'duplicate-style',
9 | options: {
10 | assetNameRegExp: /^(?!css\/font-awesome\.min\.css$).*\.css$/g,
11 | },
12 | },
13 | {
14 | name: 'less-theme',
15 | path: 'src/theme.less',
16 | },
17 | ],
18 | })
19 | },
20 |
21 | chainWebpack: (config) => {
22 | // set worker-loader
23 | config.module
24 | .rule('worker')
25 | .test(/\.worker\.js$/)
26 | .use('worker-loader')
27 | .loader('worker-loader')
28 | .end()
29 |
30 | // 解决:worker 热更新问题
31 | config.module.rule('js').exclude.add(/\.worker\.js$/)
32 | },
33 |
34 | devServer: {
35 | https: false,
36 | headers: {
37 | 'Cross-Origin-Embedder-Policy': 'require-corp',
38 | 'Cross-Origin-Opener-Policy': 'same-origin',
39 | 'Cross-Origin-Resource-Policy': 'same-site',
40 | },
41 | },
42 |
43 | pluginOptions: {
44 | i18n: {
45 | localeDir: 'locales',
46 | enableInSFC: false,
47 | },
48 | },
49 |
50 | publicPath: process.env.NODE_ENV === 'production' ? '/' : '/',
51 |
52 | pwa: {
53 | name: 'Gomoku Calculator',
54 | themeColor: '#2E86C1',
55 | msTileColor: '#2E86C1',
56 | appleMobileWebAppCapable: 'yes',
57 | appleMobileWebAppStatusBarStyle: 'default',
58 | manifestOptions: {
59 | short_name: 'Gomocalc',
60 | icons: [
61 | {
62 | src: './icon.png',
63 | sizes: '192x192',
64 | type: 'image/png',
65 | },
66 | {
67 | src: './favicon.png',
68 | sizes: '32x32',
69 | type: 'image/png',
70 | },
71 | ],
72 | },
73 | iconPaths: {
74 | favicon32: 'favicon.png',
75 | favicon16: 'favicon.png',
76 | appleTouchIcon: 'icon.png',
77 | msTileImage: 'icon.png',
78 | maskIcon: null,
79 | },
80 |
81 | // configure the workbox plugin
82 | workboxPluginMode: 'GenerateSW',
83 | workboxOptions: {
84 | importWorkboxFrom: 'local',
85 | skipWaiting: true,
86 | clientsClaim: false,
87 | offlineGoogleAnalytics: true,
88 | cleanupOutdatedCaches: true,
89 | },
90 | },
91 | }
92 |
--------------------------------------------------------------------------------