├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .firebaserc ├── .gitignore ├── .idea ├── .gitignore ├── encodings.xml ├── misc.xml ├── modules.xml ├── vcs.xml ├── vuego.iml └── watcherTasks.xml ├── .travis.yml ├── 404.html ├── README.md ├── build ├── build.js ├── dev-client.js ├── dev-server.js ├── gh-pages.js ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js ├── webpack.prod.conf.js └── webpack.test.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── firebase.json ├── firebase └── rules.json ├── index.html ├── package.json ├── src ├── App.vue ├── _mdl_variables.scss ├── arrays.js ├── assets │ ├── github_white_24.svg │ ├── logo.png │ ├── logo.svg │ └── post_twitter_white_24dp.png ├── components │ └── Hello.vue ├── game │ ├── Board.vue │ ├── Captures.vue │ ├── Grid.vue │ ├── InviteOpponentDialog.vue │ ├── JoiningGameOverlay.vue │ ├── KoMarker.vue │ ├── NewGameDialog.vue │ ├── Stone.vue │ ├── color.js │ ├── engine.js │ ├── graphics.js │ ├── local_game.js │ ├── remote_game.js │ └── store.js ├── main.js └── style.scss ├── static └── .gitkeep ├── test ├── e2e │ ├── custom-assertions │ │ └── elementCount.js │ ├── nightwatch.conf.js │ ├── runner.js │ └── specs │ │ └── test.js ├── firebase │ └── rules │ │ └── tests.json └── unit │ ├── .eslintrc │ ├── index.js │ ├── karma.conf.js │ └── specs │ ├── Hello.spec.js │ └── game │ ├── actions.spec.js │ ├── engine.spec.js │ ├── freedomDetection.data.js │ ├── getters │ ├── score.data.js │ ├── score.spec.js │ └── waitingForRemoteOpponent.spec.js │ ├── helpers.js │ ├── play.data.js │ ├── store.spec.js │ └── validatePlay.data.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 4 | extends: 'standard', 5 | // required to lint *.vue files 6 | plugins: [ 7 | 'html' 8 | ], 9 | globals: { 10 | "componentHandler": true 11 | }, 12 | // add your custom rules here 13 | 'rules': { 14 | 'semi': 0, 15 | // allow paren-less arrow functions 16 | 'arrow-parens': 0, 17 | // allow debugger during development 18 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "project-1396985000601130379" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | selenium-debug.log 6 | test/unit/coverage 7 | test/e2e/reports 8 | *.swp 9 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | workspace.xml 2 | dictionaries/ 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vuego.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: node_js 4 | node_js: 5 | - '6.10.1' 6 | 7 | addons: 8 | firefox: 'latest' 9 | apt: 10 | sources: 11 | - google-chrome 12 | packages: 13 | - google-chrome-stable 14 | 15 | before_script: 16 | - export DISPLAY=:99.0 17 | - sh -e /etc/init.d/xvfb start 18 | - sleep 3 19 | after_success: 20 | - test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && npm run 21 | deploy 22 | env: 23 | global: 24 | - secure: xAmB74IXZ+1uiyFvaApsptIwmHQDrAmVVZ8FhS2av7VGtNEzlbN+1kXbPJ02HbfgL9hmhpMOr7Upw10Wu0Is6qZj0QL3TmKeU9nUxOe9vmh5ti+6EqCQo8T6HnBj1s2hl1d07gPMyFRkOpREs1WFOfOlh+Fhu5hSlD0NXhezbMUwc3fChB9W16SIn4s3bny78tmRhfeZrtRZFdFe1j0YDAk1WTPtmhr0aEOaHjDsoer1sZdPw/T56+xi4jaDmZ0ATXs3RtAQl/zSOn4uWJrdazQ5FWsAsubXxSm5tFMxIs7Jw844/4puYvK2yg1kfjcWZSAfWTW4Dvd6HvckFa/Ve1YVCXqAOvztXUMoqC77abtG7v3eM2i47lPvoe/qsvReP07rdtavqBmLhr2AboHdv8JexjUUQryrtWZDv8q2R0ZfVKsJG0OxmddWE+qirU2B3xn5PKMzcOUtD82ToB61LXKC5IOZBNMqOcXlUv63uWd9QkhDh3bkdxK8NgOJ0KxARY0vPYM9LPbinvzeRLNN0rx4D3odTQgiaGePz3VV65Bxs6D0zhXgi2q2ekiDmNfbtUK3uRjLVQ+rdwV81unnxx9YcUDTHwDgBEADltkhoIOfjy2gbsEeXNPVCOj5Q9Yz6i/R5pSJbKdiprsevn5quHNqe40DeouScmGtfof3cGM= 25 | - secure: MSN2SPmJ5lzLuhy+n8HI2fE1S3YMnwMq2115smJZuOZFegKzI4eE1XgiDQ8ytFfJgjmzc0ECbNnrmvPoOuQ8R35D5XmQc8wg0PADjueyuofeflcd0oSULvb1R9UKHtdFqSyq3SORz5EipRuD/F6JSVW0a28ZDfrSl7OP6InKwY8ZvAraZpcCtDIYTqggkcsbvx7hWHrvfAwn6kUsrf6Zm9OZjELwHnCilUlu7LgV904cpoAhPqfixhtr4wofY1MOHIol+9jVYcuLgug5f/qIQXrd0zJMfHtaIdOqjPcR04Iu+IoDqhOqEG38p6Wo+6ssYBU4jobz5lX5dfHCJGchpfIxrDq1FqUXdP+kG4c8KDR0Un2jZj7d73AXA3GSQttJEkgx5rTI7P68NpLptw5eL47u1emeIj8rGHrEBWkcytU+nkVP/Wagewhl1d+Wb+N7hEPCy/4dYcaK+U5jEWA9uIHM8d7O2tTCHJ+PGEZEH+p5ZzLpwWD7sG7Mkn9mfDaputKTBPxYa0DaeOAdefDMkjjlhB+rUgdoiNq8/3r8PbkG2I5QyJbB7NlcvplOmTH1RQGupLzyhr/DrmTB/9gTPD8u1OXzkOKSmGumfZ5/3lnSihNZMl+sGvVmDDMEV7X52XazcizqvDTFI3XgDCesFumPuOU50R3c25pbg+6dEK8= 26 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found 6 | 71 | 72 | 73 |

Page Not Found

74 |

This specified file was not found on this website. Please check the URL for mistakes and try again.

75 |

Why am I seeing this?

76 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

77 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VueGo [![Build Status](https://travis-ci.org/petejohanson/vuego.svg?branch=master)](https://travis-ci.org/petejohanson/vuego) 2 | 3 | > A Vue.js Go Game 4 | 5 | You can play test the game here: http://petejohanson.github.io/vuego/ 6 | 7 | ## Build Setup 8 | 9 | ``` bash 10 | # install dependencies 11 | npm install 12 | 13 | # serve with hot reload at localhost:8080 14 | npm run dev 15 | 16 | # build for production with minification 17 | npm run build 18 | 19 | # run unit tests 20 | npm run unit 21 | 22 | # run e2e tests 23 | npm run e2e 24 | 25 | # run all tests 26 | npm test 27 | ``` 28 | 29 | For detailed explanation on how things work, checkout the [guide](https://github.com/vuejs-templates/webpack#vue-webpack-boilerplate) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 30 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | // https://github.com/shelljs/shelljs 2 | require('shelljs/global') 3 | env.NODE_ENV = 'production' 4 | 5 | var ora = require('ora') 6 | var webpack = require('webpack') 7 | var conf = require('./webpack.prod.conf') 8 | 9 | var spinner = ora('building for production...') 10 | spinner.start() 11 | 12 | rm('-rf', 'dist') 13 | mkdir('dist') 14 | cp('-R', 'static', conf.output.path) 15 | cp('404.html', 'dist') 16 | 17 | webpack(conf, function (err, stats) { 18 | spinner.stop() 19 | if (err) throw err 20 | process.stdout.write(stats.toString({ 21 | colors: true, 22 | modules: false, 23 | children: false, 24 | chunks: false, 25 | chunkModules: false 26 | }) + '\n') 27 | }) 28 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | require('eventsource-polyfill') 2 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 3 | 4 | hotClient.subscribe(function (event) { 5 | if (event.action === 'reload') { 6 | window.location.reload() 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var webpack = require('webpack') 3 | var config = require('./webpack.dev.conf') 4 | var proxyMiddleware = require('http-proxy-middleware') 5 | 6 | var app = express() 7 | var compiler = webpack(config) 8 | 9 | // Define HTTP proxies to your custom API backend 10 | // https://github.com/chimurai/http-proxy-middleware 11 | var proxyTable = { 12 | // '/api': { 13 | // target: 'http://jsonplaceholder.typicode.com', 14 | // changeOrigin: true, 15 | // pathRewrite: { 16 | // '^/api': '' 17 | // } 18 | // } 19 | } 20 | 21 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 22 | publicPath: config.output.publicPath, 23 | stats: { 24 | colors: true, 25 | chunks: false 26 | } 27 | }) 28 | 29 | var hotMiddleware = require('webpack-hot-middleware')(compiler) 30 | // force page reload when html-webpack-plugin template changes 31 | compiler.plugin('compilation', function (compilation) { 32 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 33 | hotMiddleware.publish({ action: 'reload' }) 34 | cb() 35 | }) 36 | }) 37 | 38 | // proxy api requests 39 | Object.keys(proxyTable).forEach(function (context) { 40 | var options = proxyTable[context] 41 | if (typeof options === 'string') { 42 | options = { target: options } 43 | } 44 | app.use(proxyMiddleware(context, options)) 45 | }) 46 | 47 | // handle fallback for HTML5 history API 48 | app.use(require('connect-history-api-fallback')()) 49 | 50 | // serve webpack bundle output 51 | app.use(devMiddleware) 52 | 53 | // enable hot-reload and state-preserving 54 | // compilation error display 55 | app.use(hotMiddleware) 56 | 57 | app.use('/images/mdl', express.static('./node_modules/material-design-lite/dist/images')) 58 | 59 | // serve pure static assets 60 | app.use('/static', express.static('./static')) 61 | 62 | module.exports = app.listen(8080, function (err) { 63 | if (err) { 64 | console.log(err) 65 | return 66 | } 67 | console.log('Listening at http://localhost:8080\n') 68 | }) 69 | -------------------------------------------------------------------------------- /build/gh-pages.js: -------------------------------------------------------------------------------- 1 | var ghpages = require('gh-pages'); 2 | var path = require('path'); 3 | var ora = require('ora'); 4 | var pkg = require('../package.json'); 5 | var spinner = ora('publishing to GitHub Pages...'); 6 | spinner.start(); 7 | 8 | var options = { 9 | user: pkg.author 10 | }; 11 | 12 | if (process.env.GH_TOKEN) { 13 | options.repo = 'https://' + process.env.GH_TOKEN + '@github.com/petejohanson/vuego.git'; 14 | } 15 | 16 | ghpages.publish(path.join(__dirname, '../dist'), options, 17 | function(err) { 18 | spinner.stop(); 19 | if (err) { 20 | process.stderr.write(err.toString()); 21 | throw err; 22 | } 23 | process.stdout.write('Finished publishing to GitHub Pages'); 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | 15 | var cssLoader = { 16 | loader: 'css-loader', 17 | options: { 18 | minimize: process.env.NODE_ENV === 'production', 19 | sourceMap: options.sourceMap 20 | } 21 | } 22 | 23 | // generate loader string to be used with extract text plugin 24 | function generateLoaders (loader, loaderOptions) { 25 | var loaders = [cssLoader] 26 | if (loader) { 27 | loaders.push({ 28 | loader: loader + '-loader', 29 | options: Object.assign({}, loaderOptions, { 30 | sourceMap: options.sourceMap 31 | }) 32 | }) 33 | } 34 | 35 | // Extract CSS when that option is specified 36 | // (which is the case during production build) 37 | if (options.extract) { 38 | return ExtractTextPlugin.extract({ 39 | use: loaders, 40 | fallback: 'vue-style-loader' 41 | }) 42 | } else { 43 | return ['vue-style-loader'].concat(loaders) 44 | } 45 | } 46 | 47 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 48 | return { 49 | css: generateLoaders(), 50 | postcss: generateLoaders(), 51 | less: generateLoaders('less'), 52 | sass: generateLoaders('sass', { indentedSyntax: true }), 53 | scss: generateLoaders('sass'), 54 | stylus: generateLoaders('stylus'), 55 | styl: generateLoaders('stylus') 56 | } 57 | } 58 | 59 | // Generate loaders for standalone style files (outside of .vue) 60 | exports.styleLoaders = function (options) { 61 | var output = [] 62 | var loaders = exports.cssLoaders(options) 63 | for (var extension in loaders) { 64 | var loader = loaders[extension] 65 | output.push({ 66 | test: new RegExp('\\.' + extension + '$'), 67 | use: loader 68 | }) 69 | } 70 | return output 71 | } 72 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction 8 | ? config.build.productionSourceMap 9 | : config.dev.cssSourceMap, 10 | extract: isProduction 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var path = require('path') 4 | var CopyWebpackPlugin = require('copy-webpack-plugin') 5 | var vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | module.exports = { 12 | entry: { 13 | app: './src/main.js' 14 | }, 15 | output: { 16 | path: config.build.assetsRoot, 17 | filename: '[name].js', 18 | publicPath: process.env.NODE_ENV === 'production' 19 | ? config.build.assetsPublicPath 20 | : config.dev.assetsPublicPath 21 | }, 22 | plugins: [ 23 | new CopyWebpackPlugin([ 24 | { from: 'node_modules/material-design-lite/dist/images', to: path.join(config.build.assetsRoot, config.build.assetsSubDirectory, './images/mdl') } 25 | ]) 26 | ], 27 | resolve: { 28 | extensions: ['.js', '.vue', '.scss', '.css'], 29 | alias: { 30 | 'vue$': 'vue/dist/vue.esm.js', 31 | '@': resolve('src') 32 | }, 33 | modules: [ 34 | resolve('src'), 35 | resolve('node_modules') 36 | ] 37 | }, 38 | module: { 39 | rules: [ 40 | { 41 | test: /\.(js|vue)$/, 42 | loader: 'eslint-loader', 43 | enforce: 'pre', 44 | include: [resolve('src'), resolve('test')], 45 | exclude: /node_modules/, 46 | options: { 47 | formatter: require('eslint-friendly-formatter') 48 | } 49 | }, 50 | { 51 | test: /\.vue$/, 52 | loader: 'vue-loader', 53 | options: vueLoaderConfig 54 | }, 55 | { 56 | test: /\.js$/, 57 | loader: 'babel-loader', 58 | include: [resolve('src'), resolve('test')], 59 | exclude: /node_modules/ 60 | }, 61 | { 62 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 63 | loader: 'url-loader', 64 | options: { 65 | limit: 10000, 66 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 67 | } 68 | }, 69 | { 70 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 71 | loader: 'url-loader', 72 | options: { 73 | limit: 10000, 74 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 75 | } 76 | } 77 | ] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseConfig = require('./webpack.base.conf') 7 | var HtmlWebpackPlugin = require('html-webpack-plugin') 8 | var PwaManifestPlugin = require('pwa-manifest-webpack-plugin') 9 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 10 | 11 | // add hot-reload related code to entry chunks 12 | Object.keys(baseConfig.entry).forEach(function (name) { 13 | baseConfig.entry[name] = ['./build/dev-client'].concat(baseConfig.entry[name]) 14 | }) 15 | 16 | module.exports = merge(baseConfig, { 17 | module: { 18 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 19 | }, 20 | // cheap-module-eval-source-map is faster for development 21 | devtool: '#cheap-module-eval-source-map', 22 | output: { 23 | // necessary for the html plugin to work properly 24 | // when serving the html from in-memory 25 | publicPath: '/' 26 | }, 27 | plugins: [ 28 | new webpack.DefinePlugin({ 29 | 'process.env': config.dev.env 30 | }), 31 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 32 | new webpack.HotModuleReplacementPlugin(), 33 | new webpack.NoEmitOnErrorsPlugin(), 34 | new PwaManifestPlugin({ 35 | name: 'VueGo Go Game', 36 | short_name: 'VueGo', 37 | start_url: 'index.html', 38 | description: 'VueGo - A Go Game written with VueJS', 39 | icon: path.resolve('src/assets/logo.png'), 40 | background_color: 'white', 41 | theme_color: '#3f51b5' 42 | }), 43 | // https://github.com/ampedandwired/html-webpack-plugin 44 | new HtmlWebpackPlugin({ 45 | filename: 'index.html', 46 | template: 'index.html', 47 | inject: true 48 | }), 49 | new FriendlyErrorsPlugin() 50 | ] 51 | }) 52 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseConfig = require('./webpack.base.conf') 7 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 8 | var PwaManifestPlugin = require('pwa-manifest-webpack-plugin') 9 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 10 | var HtmlWebpackPlugin = require('html-webpack-plugin') 11 | var CopyWebpackPlugin = require('copy-webpack-plugin') 12 | var SWPrecachePlugin = require('sw-precache-webpack-plugin') 13 | 14 | var env = process.env.NODE_ENV === 'testing' 15 | ? require('../config/test.env') 16 | : config.build.env 17 | 18 | var webpackConfig = merge(baseConfig, { 19 | module: { 20 | rules: utils.styleLoaders({ 21 | sourceMap: config.build.productionSourceMap, 22 | extract: true 23 | }) 24 | }, 25 | devtool: config.build.productionSourceMap ? '#source-map' : false, 26 | output: { 27 | path: config.build.assetsRoot, 28 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 29 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 30 | }, 31 | plugins: [ 32 | // http://vuejs.github.io/vue-loader/workflow/production.html 33 | new webpack.DefinePlugin({ 34 | 'process.env': env 35 | }), 36 | new webpack.optimize.UglifyJsPlugin({ 37 | compress: { 38 | warnings: false 39 | }, 40 | sourceMap: true 41 | }), 42 | new PwaManifestPlugin({ 43 | name: 'VueGo Go Game', 44 | short_name: 'VueGo', 45 | start_url: 'index.html', 46 | description: 'VueGo - A Go Game written with VueJS', 47 | icon: path.resolve('src/assets/logo.png'), 48 | background_color: 'white', 49 | theme_color: '#3f51b5' 50 | }), 51 | // extract css into its own file 52 | new ExtractTextPlugin({ 53 | filename: utils.assetsPath('css/[name].[contenthash].css') 54 | }), 55 | // Compress extracted CSS. We are using this plugin so that possible 56 | // duplicated CSS from different components can be deduped. 57 | new OptimizeCSSPlugin({ 58 | cssProcessorOptions: { 59 | safe: true 60 | } 61 | }), 62 | // generate dist index.html with correct asset hash for caching. 63 | // you can customize output by editing /index.html 64 | // see https://github.com/ampedandwired/html-webpack-plugin 65 | new HtmlWebpackPlugin({ 66 | filename: process.env.NODE_ENV === 'testing' 67 | ? 'index.html' 68 | : config.build.index, 69 | template: 'index.html', 70 | inject: true, 71 | minify: { 72 | removeComments: true, 73 | collapseWhitespace: true, 74 | removeAttributeQuotes: true 75 | // more options: 76 | // https://github.com/kangax/html-minifier#options-quick-reference 77 | }, 78 | chunksSortMode: 'dependency' 79 | }), 80 | new webpack.optimize.CommonsChunkPlugin({ 81 | name: 'vendor', 82 | minChunks: function (module, count) { 83 | // any required modules inside node_modules are extracted to vendor 84 | return ( 85 | module.resource && 86 | /\.js$/.test(module.resource) && 87 | module.resource.indexOf( 88 | path.join(__dirname, '../node_modules') 89 | ) === 0 90 | ) 91 | } 92 | }), 93 | // extract webpack runtime and module manifest to its own file in order to 94 | // prevent vendor hash from being updated whenever app bundle is updated 95 | new webpack.optimize.CommonsChunkPlugin({ 96 | name: 'manifest', 97 | chunks: ['vendor'] 98 | }), 99 | // copy custom static assets 100 | new CopyWebpackPlugin([ 101 | { 102 | from: path.resolve(__dirname, '../static'), 103 | to: config.build.assetsSubDirectory, 104 | ignore: ['.*'] 105 | } 106 | ]), 107 | new SWPrecachePlugin({ 108 | cacheId: 'vuego-app', 109 | staticFileGlobsIgnorePatterns: [/\.map$/], 110 | minify: true, 111 | runtimeCaching: [ 112 | { 113 | urlPattern: /^https?:\/\/fonts\.google.+/, 114 | handler: 'fastest' 115 | } 116 | ] 117 | }) 118 | ] 119 | }) 120 | 121 | if (config.build.productionGzip) { 122 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 123 | 124 | webpackConfig.plugins.push( 125 | new CompressionWebpackPlugin({ 126 | asset: '[path].gz[query]', 127 | algorithm: 'gzip', 128 | test: new RegExp( 129 | '\\.(' + 130 | config.build.productionGzipExtensions.join('|') + 131 | ')$' 132 | ), 133 | threshold: 10240, 134 | minRatio: 0.8 135 | }) 136 | ) 137 | } 138 | 139 | if (config.build.bundleAnalyzerReport) { 140 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 141 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 142 | } 143 | 144 | module.exports = webpackConfig 145 | -------------------------------------------------------------------------------- /build/webpack.test.conf.js: -------------------------------------------------------------------------------- 1 | // This is the webpack config used for unit tests. 2 | 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseConfig = require('./webpack.base.conf') 7 | 8 | var webpackConfig = merge(baseConfig, { 9 | // use inline sourcemap for karma-sourcemap-loader 10 | module: { 11 | rules: utils.styleLoaders() 12 | }, 13 | devtool: '#inline-source-map', 14 | resolveLoader: { 15 | alias: { 16 | // necessary to to make lang="scss" work in test when using vue-loader's ?inject option 17 | // see discussion at https://github.com/vuejs/vue-loader/issues/724 18 | 'scss-loader': 'sass-loader' 19 | } 20 | }, 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': require('../config/test.env') 24 | }) 25 | ] 26 | }) 27 | 28 | // no need for app entry during tests 29 | delete webpackConfig.entry 30 | 31 | module.exports = webpackConfig 32 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8080, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: {}, 31 | // CSS Sourcemaps off by default because relative paths are "buggy" 32 | // with this option, according to the CSS-Loader README 33 | // (https://github.com/webpack/css-loader#sourcemaps) 34 | // In our experience, they generally work as expected, 35 | // just be aware of this issue when enabling this option. 36 | cssSourceMap: false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "rules": "firebase/rules.json" 4 | }, 5 | "hosting": { 6 | "public": "dist", 7 | "headers": [ 8 | { 9 | "source": "**/static/js/*.js", 10 | "headers": [ { 11 | "key": "Cache-Control", 12 | "value": "max-age=31536000" 13 | } ] 14 | }, 15 | { 16 | "source": "**/static/css/*.css", 17 | "headers": [ { 18 | "key": "Cache-Control", 19 | "value": "max-age=31536000" 20 | } ] 21 | } 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /firebase/rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "games": { 4 | "$game": { 5 | ".read": "auth.uid !== null && (data.child('black').val() === auth.uid || data.child('white').val() === auth.uid)", 6 | ".write": "auth.uid !== null && newData.val() !== null && data.val() === null", 7 | "white": { 8 | ".write": "data.val() === null && newData.val() === auth.uid" 9 | } 10 | } 11 | }, 12 | "moves": { 13 | "$game": { 14 | ".read": "root.child('games').child($game).child('black').val() === auth.uid || root.child('games').child($game).child('white').val() === auth.uid", 15 | ".validate": "root.child('games').child($game).child('black').val() === auth.uid || root.child('games').child($game).child('white').val() === auth.uid", 16 | "$move_id": { 17 | ".write": "!data.exists() && newData.exists()" 18 | } 19 | } 20 | } 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue.js Go Game 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 23 | 24 | 37 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuego", 3 | "version": "0.1.0", 4 | "description": "A Vue.js Go Game", 5 | "author": { 6 | "name": "Peter Johanson", 7 | "email": "peter@peterjohanson.com", 8 | "url": "https://github.com/petejohanson" 9 | }, 10 | "private": true, 11 | "scripts": { 12 | "dev": "node build/dev-server.js", 13 | "build": "node build/build.js", 14 | "serve-dist": "http-server dist/", 15 | "gh-pages": "npm run build && node build/gh-pages.js", 16 | "deploy": "npm run build && firebase deploy --token $FIREBASE_TOKEN", 17 | "security-rules": "targaryen --verbose firebase/rules.json test/firebase/rules/tests.json", 18 | "unit": "karma start test/unit/karma.conf.js --single-run", 19 | "e2e": "node test/e2e/runner.js", 20 | "test": "npm run unit && npm run e2e && npm run security-rules" 21 | }, 22 | "dependencies": { 23 | "babel-runtime": "^6.23.0", 24 | "dialog-polyfill": "^0.4.3", 25 | "es6-promise-try": "^1.0.2", 26 | "firebase": "^3.0.0", 27 | "lodash": "^4.10.0", 28 | "material-design-lite": "^1.1.3", 29 | "mdl-selectfield": "^1.0.1", 30 | "urijs": "^1.18.0", 31 | "vue": "^2.2.6", 32 | "vue-mdl": "^1.1.1", 33 | "vuex": "^2.3.1" 34 | }, 35 | "devDependencies": { 36 | "babel-core": "^6.0.0", 37 | "babel-loader": "^7.0.0", 38 | "babel-plugin-transform-runtime": "^6.0.0", 39 | "babel-preset-es2015": "^6.0.0", 40 | "babel-preset-stage-2": "^6.0.0", 41 | "chromedriver": "^2.29.0", 42 | "connect-history-api-fallback": "^1.1.0", 43 | "copy-webpack-plugin": "^4.0.1", 44 | "cross-spawn": "^5.1.0", 45 | "css-loader": "^0.28.0", 46 | "eslint": "^3.19.0", 47 | "eslint-config-standard": "^10.2.1", 48 | "eslint-friendly-formatter": "^2.0.7", 49 | "eslint-loader": "^1.3.0", 50 | "eslint-plugin-html": "^2.0.1", 51 | "eslint-plugin-import": "^2.2.0", 52 | "eslint-plugin-node": "^4.2.2", 53 | "eslint-plugin-promise": "^3.5.0", 54 | "eslint-plugin-standard": "^3.0.1", 55 | "eventsource-polyfill": "^0.9.6", 56 | "express": "^4.13.3", 57 | "extract-text-webpack-plugin": "^2.1.0", 58 | "file-loader": "^0.11.1", 59 | "firebase-tools": "^3.0.2", 60 | "friendly-errors-webpack-plugin": "^1.6.1", 61 | "function-bind": "^1.0.2", 62 | "geckodriver": "^1.6.0", 63 | "gh-pages": "^0.12.0", 64 | "html-webpack-plugin": "^2.8.1", 65 | "http-proxy-middleware": "^0.17.4", 66 | "http-server": "^0.9.0", 67 | "inject-loader": "^3.0.0", 68 | "isparta-loader": "^2.0.0", 69 | "jasmine-core": "^2.4.1", 70 | "json-loader": "^0.5.4", 71 | "karma": "^1.6.0", 72 | "karma-coverage": "^1.1.1", 73 | "karma-es6-shim": "^1.0.0", 74 | "karma-jasmine": "^1.1.0", 75 | "karma-phantomjs-launcher": "^1.0.0", 76 | "karma-phantomjs-shim": "^1.4.0", 77 | "karma-sourcemap-loader": "^0.3.7", 78 | "karma-spec-reporter": "^0.0.31", 79 | "karma-webpack": "^2.0.3", 80 | "nightwatch": "^0.9.14", 81 | "node-sass": "^4.5.2", 82 | "optimize-css-assets-webpack-plugin": "^1.3.1", 83 | "ora": "^1.2.0", 84 | "phantomjs-prebuilt": "^2.1.3", 85 | "pwa-manifest-webpack-plugin": "^1.0.3", 86 | "sass-loader": "^6.0.3", 87 | "selenium-server": "^3.4.0", 88 | "selenium-webdriver": "^3.4.0", 89 | "shelljs": "^0.7.7", 90 | "sw-precache-webpack-plugin": "^0.10.1", 91 | "targaryen": "^3.0.1", 92 | "url-loader": "^0.5.7", 93 | "vue-hot-reload-api": "^2.1.0", 94 | "vue-html-loader": "^1.0.0", 95 | "vue-loader": "^11.3.4", 96 | "vue-style-loader": "^3.0.0", 97 | "vue-template-compiler": "^2.2.6", 98 | "webpack": "^2.4.1", 99 | "webpack-bundle-analyzer": "^2.4.0", 100 | "webpack-dev-middleware": "^1.4.0", 101 | "webpack-hot-middleware": "^2.6.0", 102 | "webpack-merge": "^4.1.0" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 175 | 176 | 343 | -------------------------------------------------------------------------------- /src/_mdl_variables.scss: -------------------------------------------------------------------------------- 1 | @import '../node_modules/material-design-lite/src/color-definitions'; 2 | 3 | $color-primary: $palette-indigo-500; 4 | $color-primary-dark: $palette-indigo-800; 5 | $color-accent: $palette-red-500; 6 | 7 | $image_path: '/static/images/mdl'; 8 | -------------------------------------------------------------------------------- /src/arrays.js: -------------------------------------------------------------------------------- 1 | 2 | export function matrix (colCount, rowCount, initial) { 3 | let columns = []; 4 | 5 | for (let i = 0; i < colCount; ++i) { 6 | let rows = []; 7 | for (let j = 0; j < rowCount; ++j) { 8 | rows[j] = initial; 9 | } 10 | 11 | columns[i] = rows; 12 | } 13 | 14 | return columns; 15 | } 16 | -------------------------------------------------------------------------------- /src/assets/github_white_24.svg: -------------------------------------------------------------------------------- 1 | Shape -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petejohanson/vuego/ed077298a6d80957b4640dca0d33292079668d96/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 150 | -------------------------------------------------------------------------------- /src/assets/post_twitter_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petejohanson/vuego/ed077298a6d80957b4640dca0d33292079668d96/src/assets/post_twitter_white_24dp.png -------------------------------------------------------------------------------- /src/components/Hello.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /src/game/Board.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 132 | 133 | 150 | -------------------------------------------------------------------------------- /src/game/Captures.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 24 | 25 | 41 | -------------------------------------------------------------------------------- /src/game/Grid.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 63 | -------------------------------------------------------------------------------- /src/game/InviteOpponentDialog.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 57 | 58 | 68 | -------------------------------------------------------------------------------- /src/game/JoiningGameOverlay.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | 60 | -------------------------------------------------------------------------------- /src/game/KoMarker.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 31 | 32 | 38 | -------------------------------------------------------------------------------- /src/game/NewGameDialog.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 64 | 65 | 85 | -------------------------------------------------------------------------------- /src/game/Stone.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | 25 | 38 | -------------------------------------------------------------------------------- /src/game/color.js: -------------------------------------------------------------------------------- 1 | const BLACK = 'black'; 2 | const WHITE = 'white'; 3 | 4 | let oppositeColor = c => c === BLACK ? WHITE : BLACK; 5 | 6 | export { BLACK, WHITE, oppositeColor } 7 | -------------------------------------------------------------------------------- /src/game/engine.js: -------------------------------------------------------------------------------- 1 | 2 | import isUndefined from 'lodash/isUndefined'; 3 | import isEqual from 'lodash/isEqual'; 4 | import filter from 'lodash/fp/filter'; 5 | import flatMap from 'lodash/fp/flatMap'; 6 | import map from 'lodash/fp/map'; 7 | import partition from 'lodash/fp/partition'; 8 | import differenceWith from 'lodash/fp/differenceWith'; 9 | import every from 'lodash/fp/every'; 10 | import uniqWith from 'lodash/fp/uniqWith'; 11 | import some from 'lodash/fp/some'; 12 | import flow from 'lodash/fp/flow'; 13 | import { matrix } from '../arrays'; 14 | import { oppositeColor } from './color'; 15 | 16 | function neighboringPoints (x, y, size) { 17 | let ret = []; 18 | 19 | if (x > 0) { 20 | ret.push({x: x - 1, y}); 21 | } 22 | 23 | if (x + 1 < size) { 24 | ret.push({x: x + 1, y}); 25 | } 26 | 27 | if (y > 0) { 28 | ret.push({y: y - 1, x}); 29 | } 30 | 31 | if (y + 1 < size) { 32 | ret.push({y: y + 1, x}); 33 | } 34 | 35 | return ret; 36 | } 37 | 38 | function moveIsSuicide (state, x, y, color) { 39 | let neighbors = neighboringPoints(x, y, state.size); 40 | let [occupied, free] = partition(p => state.board[p.y][p.x])(neighbors); 41 | 42 | if (free.length > 0) { 43 | return false; 44 | } 45 | 46 | let [friends, enemies] = partition(p => state.board[p.y][p.x] === color)(occupied); 47 | 48 | free = flow( 49 | flatMap(p => { 50 | return freedoms(state, p.x, p.y); 51 | }), 52 | uniqWith(isEqual) 53 | )(friends); 54 | 55 | if (free.length > 1) { 56 | return false; 57 | } 58 | 59 | return !some(p => freedoms(state, p.x, p.y).length === 1)(enemies); 60 | } 61 | 62 | function freedoms (state, x, y) { 63 | return findGroupAndFreedoms(state, x, y).freedoms; 64 | } 65 | 66 | function findGroupAndFreedoms (state, x, y) { 67 | let points = matrix(state.size, state.size); 68 | 69 | let groupColor = state.board[y][x]; 70 | 71 | if (!groupColor) { 72 | throw Error(`No piece at ${x},${y}`); 73 | } 74 | 75 | calculateFreedoms(state, groupColor, x, y, points); 76 | 77 | let freedoms = []; 78 | let group = []; 79 | for (let i = 0; i < points.length; ++i) { 80 | let row = points[i]; 81 | for (let j = 0; j < row.length; ++j) { 82 | if (row[j] === true) { 83 | freedoms.push({ x: j, y: i }); 84 | } else if (row[j] === groupColor) { 85 | group.push({ x: j, y: i }); 86 | } 87 | } 88 | } 89 | 90 | return { freedoms, group }; 91 | } 92 | 93 | function calculateFreedoms (state, color, x, y, calculated) { 94 | if (!isUndefined(calculated[y][x])) { 95 | return; 96 | } 97 | 98 | if (state.board[y][x] === color) { 99 | calculated[y][x] = color; 100 | } else { 101 | calculated[y][x] = isUndefined(state.board[y][x]); 102 | } 103 | 104 | let candidates = neighboringPoints(x, y, state.size); 105 | 106 | for (let i = 0; i < candidates.length; ++i) { 107 | let c = candidates[i]; 108 | if (isUndefined(state.board[c.y][c.x])) { 109 | calculated[c.y][c.x] = true; 110 | } else if (state.board[c.y][c.x] === color) { 111 | calculateFreedoms(state, color, c.x, c.y, calculated); 112 | } 113 | } 114 | } 115 | 116 | function validatePlay (state, x, y) { 117 | if (state.board[y][x]) { 118 | return false; 119 | } 120 | 121 | if (isEqual(state.ko, { x, y })) { 122 | return false; 123 | } 124 | 125 | if (moveIsSuicide(state, x, y, state.current_turn)) { 126 | return false; 127 | } 128 | 129 | return true; 130 | } 131 | 132 | function play (state, x, y) { 133 | let changes = flow( 134 | filter(p => state.board[p.y][p.x] === oppositeColor(state.current_turn)), 135 | map(p => findGroupAndFreedoms(state, p.x, p.y)), 136 | filter(found => found.freedoms.length === 1), 137 | flatMap(found => found.group), 138 | uniqWith(isEqual) 139 | )(neighboringPoints(x, y, state.size)); 140 | 141 | let ko = null; 142 | 143 | if (changes.length === 1) { 144 | let isKo = flow( 145 | differenceWith(isEqual)(neighboringPoints(x, y, state.size)), 146 | every(p => state.board[p.y][p.x] === oppositeColor(state.current_turn)) 147 | )(changes); 148 | 149 | if (isKo) { 150 | ko = changes[0]; 151 | } 152 | } 153 | 154 | changes.unshift({ x, y, color: state.current_turn }); 155 | 156 | return { changes, ko }; 157 | } 158 | 159 | export { neighboringPoints, freedoms, validatePlay, play } 160 | -------------------------------------------------------------------------------- /src/game/graphics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by peter on 4/21/16. 3 | */ 4 | 5 | const SCALE = 20; 6 | 7 | export { SCALE }; 8 | -------------------------------------------------------------------------------- /src/game/local_game.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by peter on 5/13/16. 3 | */ 4 | 5 | export default class LocalGame { 6 | constructor (store) { 7 | this.store = store; 8 | } 9 | 10 | pass () { 11 | this.store.dispatch('pass'); 12 | } 13 | 14 | play (location) { 15 | this.store.dispatch('playerTurn', location); 16 | } 17 | 18 | localCurrentTurn () { 19 | return this.store.getters.currentTurn; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/game/remote_game.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by peter on 5/16/16. 3 | */ 4 | 5 | import promiseTry from 'es6-promise-try'; 6 | import firebase from 'firebase'; 7 | 8 | import { BLACK, WHITE } from './color'; 9 | 10 | firebase.initializeApp({ 11 | apiKey: 'AIzaSyD1sB2mtSubsImIEh4SklU7EivMnJHKLDA', 12 | authDomain: 'vuego.firebaseapp.com', 13 | databaseURL: 'https://vuego.firebaseio.com', 14 | storageBucket: 'project-1396985000601130379.appspot.com' 15 | }); 16 | 17 | let fb = firebase.database(); 18 | 19 | class RemoteGame { 20 | constructor (store) { 21 | this.store = store; 22 | let gameId = store.state.remoteGameId; 23 | 24 | if (!store.state[WHITE]) { 25 | let opponent = fb.ref(`games/${gameId}/${WHITE}`); 26 | let cb = snapshot => { 27 | if (!snapshot.val()) { 28 | return; 29 | } 30 | 31 | store.dispatch('remoteOpponentAccepted', { opponentId: snapshot.val() }); 32 | opponent.off('value', cb); 33 | }; 34 | opponent.on('value', cb); 35 | } 36 | 37 | // TODO: If no opponent yet, listen for acceptance of invitation! 38 | this.movesRef = fb.ref('moves').child(gameId); 39 | this.movesRef.on('child_added', m => { 40 | store.dispatch('addRemoteMove', { id: m.key, move: m.val() }); 41 | }); 42 | } 43 | 44 | pass () { 45 | this.movesRef.push({ 46 | type: 'pass' 47 | }); 48 | } 49 | 50 | play ({ x, y }) { 51 | this.movesRef.push({ 52 | type: 'play', 53 | params: { 54 | x, 55 | y 56 | } 57 | }); 58 | } 59 | 60 | localCurrentTurn () { 61 | let t = this.store.getters.currentTurn; 62 | 63 | if (!t) { 64 | return t; 65 | } 66 | 67 | let uid = firebase.auth().currentUser.uid; 68 | 69 | return this.store.state[t] === uid ? t : null; 70 | } 71 | } 72 | 73 | RemoteGame.create = function ({ size }) { 74 | if (!firebase.auth().currentUser) { 75 | return promiseTry(() => 76 | firebase.auth().signInAnonymously() 77 | ).then(() => 78 | RemoteGame.create({ size }) 79 | ); 80 | } 81 | 82 | let uid = firebase.auth().currentUser.uid; 83 | let games = fb.ref('games'); 84 | let newGame = games.push({ 85 | size, 86 | [BLACK]: uid, 87 | [WHITE]: null 88 | }); 89 | 90 | let key = newGame.key; 91 | 92 | return Promise.resolve({ size, gameId: key, inviteId: key, [BLACK]: uid }); 93 | }; 94 | 95 | RemoteGame.join = function ({ gameId }) { 96 | if (!firebase.auth().currentUser) { 97 | return promiseTry(() => 98 | firebase.auth().signInAnonymously() 99 | ).then(() => 100 | RemoteGame.join({ gameId }) 101 | ); 102 | } 103 | 104 | let game = fb.ref(`games/${gameId}`); 105 | return promiseTry(() => 106 | game.update({ [WHITE]: firebase.auth().currentUser.uid }) 107 | ).then(() => 108 | game.once('value') 109 | ).then(v => { 110 | let { size, [WHITE]: white, [BLACK]: black } = v.val(); 111 | 112 | return { size, [WHITE]: white, [BLACK]: black, gameId: v.key }; 113 | }); 114 | }; 115 | 116 | export default RemoteGame; 117 | -------------------------------------------------------------------------------- /src/game/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import promiseTry from 'es6-promise-try'; 5 | 6 | import countBy from 'lodash/fp/countBy'; 7 | import flattenDeep from 'lodash/fp/flattenDeep'; 8 | import filter from 'lodash/fp/filter'; 9 | import defaults from 'lodash/fp/defaults'; 10 | import flow from 'lodash/fp/flow'; 11 | 12 | import { matrix } from '../arrays'; 13 | import { BLACK, WHITE, oppositeColor } from './color'; 14 | import { play, validatePlay, neighboringPoints } from './engine'; 15 | 16 | import RemoteGame from './remote_game'; 17 | 18 | Vue.use(Vuex); 19 | 20 | function inspectTerritory (state, x, y, t, territories) { 21 | if (territories[y][x]) { 22 | return; 23 | } 24 | 25 | let { board, size } = state; 26 | let color = board[y][x]; 27 | if (color) { 28 | territories[y][x] = color; 29 | return; 30 | } 31 | 32 | territories[y][x] = t; 33 | 34 | let neighbors = neighboringPoints(x, y, size); 35 | 36 | for (let i = 0; i < neighbors.length; ++i) { 37 | let { x: nx, y: ny } = neighbors[i]; 38 | 39 | if (board[ny][nx]) { 40 | t.neighbor(board[ny][nx]); 41 | } 42 | inspectTerritory(state, nx, ny, t, territories); 43 | } 44 | } 45 | 46 | class Territory { 47 | constructor (color) { 48 | this.neighbors = []; 49 | if (color) { 50 | this.neighbor(color); 51 | } 52 | } 53 | 54 | neighbor (color) { 55 | if (this.neighbors[0] !== color) { 56 | this.neighbors.push(color); 57 | } 58 | } 59 | 60 | owner () { 61 | if (this.neighbors.length === 1) { 62 | return this.neighbors[0]; 63 | } else { 64 | return null; 65 | } 66 | } 67 | } 68 | 69 | function territoryCounts (state) { 70 | let { size } = state; 71 | let territories = matrix(size, size); 72 | for (let row = 0; row < size; ++row) { 73 | for (let col = 0; col < size; ++col) { 74 | if (territories[row][col]) { 75 | continue; 76 | } 77 | 78 | inspectTerritory(state, col, row, new Territory(), territories); 79 | } 80 | } 81 | 82 | return flow( 83 | flattenDeep, 84 | filter(t => t instanceof Territory), 85 | countBy(t => t.owner()), 86 | defaults({ [BLACK]: 0, [WHITE]: 0 }) 87 | )(territories); 88 | } 89 | 90 | export const state = { 91 | online: true, 92 | game_done: false, 93 | board: matrix(9, 9), 94 | gameType: 'placeholder', 95 | current_turn: null, 96 | remoteInviteId: null, 97 | remoteGameId: null, 98 | pass_last_turn: false, 99 | size: 9, 100 | captures: { 101 | [BLACK]: 0, 102 | [WHITE]: 0 103 | }, 104 | ko: null 105 | }; 106 | 107 | export const actions = { 108 | online: ({ commit }) => commit('online'), 109 | offline: ({ commit }) => commit('offline'), 110 | 111 | newGame ({ commit }, { size, remoteGame }) { 112 | if (remoteGame) { 113 | return promiseTry(() => 114 | RemoteGame.create({size}) 115 | ).then(g => 116 | commit('new_remote_game', g) 117 | ); 118 | } else { 119 | return Promise.resolve(commit('new_local_game', size)); 120 | } 121 | }, 122 | 123 | pass: ({ commit }) => commit('pass_turn'), 124 | playerTurn: ({ commit }, x, y) => commit('player_turn', x, y), 125 | 126 | joinGame ({ commit }, { gameId }) { 127 | return promiseTry(() => 128 | RemoteGame.join({ gameId }) 129 | ).then(g => 130 | commit('join_remote_game', g) 131 | ); 132 | }, 133 | 134 | remoteOpponentAccepted ({ commit }, { opponentId }) { 135 | commit('remote_opponent_accepted', { opponentId }); 136 | }, 137 | 138 | cancelRemoteGame ({ commit }) { 139 | commit('cancel_remote_game'); 140 | }, 141 | 142 | addRemoteMove ({ dispatch, commit }, { id, move }) { 143 | let { type, params } = move; 144 | switch (type) { 145 | case 'play': 146 | let { x, y } = params; 147 | dispatch('playerTurn', { x, y }); 148 | break; 149 | case 'pass': 150 | dispatch('pass'); 151 | break; 152 | } 153 | 154 | commit('remote_move', { id }); 155 | } 156 | }; 157 | 158 | export const mutations = { 159 | online (state) { 160 | state.online = true; 161 | }, 162 | offline (state) { 163 | state.online = false; 164 | }, 165 | new_local_game (state, size) { 166 | state.gameType = 'local'; 167 | state.game_done = false; 168 | state.size = size; 169 | state.board = matrix(size, size); 170 | state.current_turn = BLACK; 171 | state.captures[BLACK] = 0; 172 | state.captures[WHITE] = 0; 173 | state.ko = null; 174 | state.remoteGameId = null; 175 | state.pass_last_turn = false; 176 | }, 177 | 178 | new_remote_game (state, { size, gameId, inviteId, [BLACK]: blackId, [WHITE]: whiteId }) { 179 | state.gameType = 'remote'; 180 | state.game_done = false; 181 | state.size = size; 182 | state.captures = { [BLACK]: 0, [WHITE]: 0 }; 183 | state.board = matrix(size, size); 184 | state.remoteGameId = gameId; 185 | state.remoteInviteId = inviteId; 186 | state.current_turn = BLACK; 187 | Vue.set(state, BLACK, blackId); 188 | Vue.set(state, WHITE, whiteId); 189 | }, 190 | 191 | cancel_remote_game (s) { 192 | s.gameType = 'placeholder'; 193 | s.remoteGameId = null; 194 | s.remoteInviteId = null; 195 | s.current_turn = null; 196 | s.size = state.size; 197 | s.board = matrix(s.size, s.size); 198 | Vue.set(s, BLACK, null); 199 | Vue.set(s, WHITE, null); 200 | }, 201 | 202 | join_remote_game (state, { size, gameId, [BLACK]: blackId, [WHITE]: whiteId }) { 203 | state.gameType = 'remote'; 204 | state.game_done = false; 205 | state.size = size; 206 | state.captures = { [BLACK]: 0, [WHITE]: 0 }; 207 | state.board = matrix(size, size); 208 | state.remoteGameId = gameId; 209 | state.current_turn = BLACK; 210 | Vue.set(state, BLACK, blackId); 211 | Vue.set(state, WHITE, whiteId); 212 | }, 213 | 214 | remote_opponent_accepted (state, { opponentId }) { 215 | Vue.set(state, WHITE, opponentId); 216 | }, 217 | 218 | remote_move (state, moveId) { 219 | state.lastRemoteMove = moveId; 220 | }, 221 | 222 | player_turn (state, { x, y }) { 223 | if (!state.current_turn || !validatePlay(state, x, y)) { 224 | return; 225 | } 226 | 227 | let { changes: turn, ko } = play(state, x, y); 228 | 229 | for (let i = 0; i < turn.length; ++i) { 230 | let t = turn[i]; 231 | Vue.set(state.board[t.y], t.x, t.color); 232 | 233 | if (!t.color) { 234 | state.captures[state.current_turn]++; 235 | } 236 | } 237 | 238 | state.ko = ko; 239 | state.pass_last_turn = false; 240 | state.current_turn = oppositeColor(state.current_turn); 241 | }, 242 | 243 | pass_turn (state) { 244 | state.ko = null; 245 | 246 | if (state.pass_last_turn) { 247 | state.current_turn = null; 248 | state.game_done = true; 249 | } else { 250 | state.current_turn = oppositeColor(state.current_turn); 251 | state.pass_last_turn = true; 252 | } 253 | } 254 | }; 255 | 256 | export const getters = { 257 | online: state => state.online, 258 | ko: state => state.ko, 259 | size: state => state.size, 260 | gameType: state => state.gameType, 261 | gameDone: state => state.game_done, 262 | board: state => state.board, 263 | waitingForRemoteOpponent: state => state.gameType === 'remote' && !state[WHITE], 264 | inviteId: state => state.remoteInviteId, 265 | captures: state => state.captures, 266 | currentTurn: state => state.current_turn, 267 | 268 | score (state) { 269 | let { captures: { [BLACK]: blackCaptures, [WHITE]: whiteCaptures } } = state; 270 | let { [BLACK]: blackTerritory, [WHITE]: whiteTerritory } = territoryCounts(state); 271 | 272 | return { 273 | [BLACK]: blackTerritory + blackCaptures, 274 | [WHITE]: whiteTerritory + whiteCaptures 275 | }; 276 | } 277 | } 278 | 279 | export default new Vuex.Store({ 280 | state, 281 | mutations, 282 | actions, 283 | getters 284 | }) 285 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import './style.scss'; 2 | import 'material-design-lite/material.js'; 3 | import 'dialog-polyfill/dialog-polyfill.css'; 4 | import 'mdl-selectfield/src/selectfield/selectfield'; 5 | import Vue from 'vue'; 6 | import App from './App'; 7 | 8 | /* eslint-disable no-new */ 9 | new Vue({ 10 | el: '#app', 11 | components: { App }, 12 | render (h) { 13 | return h('app'); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /src/style.scss: -------------------------------------------------------------------------------- 1 | @import './mdl_variables'; 2 | @import '../node_modules/material-design-lite/src/material-design-lite'; 3 | @import '../node_modules/mdl-selectfield/src/selectfield/selectfield'; 4 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petejohanson/vuego/ed077298a6d80957b4640dca0d33292079668d96/static/.gitkeep -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // the name of the method is the filename. 3 | // can be used in tests like this: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // for how to write custom assertions see 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | exports.assertion = function (selector, count) { 10 | this.message = 'Testing if element <' + selector + '> has count: ' + count 11 | this.expected = count 12 | this.pass = function (val) { 13 | return val === this.expected 14 | } 15 | this.value = function (res) { 16 | return res.value 17 | } 18 | this.command = function (cb) { 19 | var self = this 20 | return this.api.execute(function (selector) { 21 | return document.querySelectorAll(selector).length 22 | }, [selector], function (res) { 23 | cb.call(self, res) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | // http://nightwatchjs.org/guide#settings-file 2 | module.exports = { 3 | "src_folders": ["test/e2e/specs"], 4 | "output_folder": "test/e2e/reports", 5 | "custom_assertions_path": ["test/e2e/custom-assertions"], 6 | 7 | "selenium": { 8 | "start_process": true, 9 | "server_path": "node_modules/selenium-server/lib/runner/selenium-server-standalone-3.4.0.jar", 10 | "host": "127.0.0.1", 11 | "port": 4444, 12 | "cli_args": { 13 | "webdriver.chrome.driver": require('chromedriver').path 14 | } 15 | }, 16 | 17 | "test_settings": { 18 | "default": { 19 | "selenium_port": 4444, 20 | "selenium_host": "localhost", 21 | "silent": true 22 | }, 23 | 24 | "chrome": { 25 | "desiredCapabilities": { 26 | "browserName": "chrome", 27 | "javascriptEnabled": true, 28 | "acceptSslCerts": true, 29 | "chromeOptions": { 30 | "args": ["--no-sandbox"] 31 | } 32 | } 33 | }, 34 | 35 | "firefox": { 36 | "desiredCapabilities": { 37 | "browserName": "firefox", 38 | "javascriptEnabled": true, 39 | "acceptSslCerts": true 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server 2 | var server = require('../../build/dev-server.js') 3 | 4 | // 2. run the nightwatch test suite against it 5 | // to run in additional browsers: 6 | // 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings" 7 | // 2. add it to the --env flag below 8 | // For more information on Nightwatch's config file, see 9 | // http://nightwatchjs.org/guide#settings-file 10 | var spawn = require('cross-spawn') 11 | var runner = spawn( 12 | './node_modules/.bin/nightwatch', 13 | [ 14 | '--config', 'test/e2e/nightwatch.conf.js', 15 | '--env', 'chrome,firefox' 16 | ], 17 | { 18 | stdio: 'inherit' 19 | } 20 | ) 21 | 22 | runner.on('exit', function (code) { 23 | server.close() 24 | process.exit(code) 25 | }) 26 | 27 | runner.on('error', function (err) { 28 | server.close() 29 | throw err 30 | }) 31 | -------------------------------------------------------------------------------- /test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | browser 7 | .url('http://localhost:8080') 8 | .waitForElementVisible('#app', 5000) 9 | .assert.elementPresent('svg') 10 | .end() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/firebase/rules/tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": { 3 | "games": { 4 | "in_progress": { 5 | "black": "anon:1", 6 | "white": "anon:2", 7 | "size": 9 8 | }, 9 | "no_opponent": { 10 | "black": "anon:1", 11 | "white": null, 12 | "size": 9 13 | } 14 | }, 15 | "moves": { 16 | "in_progress": { 17 | "0": { "type": "pass" } 18 | } 19 | } 20 | }, 21 | "users": { 22 | "User 1": { "uid": "anon:1" }, 23 | "User 2": { "uid": "anon:2" }, 24 | "User 3": { "uid": "anon:3" } 25 | }, 26 | "tests": { 27 | "games/new_game": { 28 | "cannotWrite": [ 29 | { "auth": null, "data": {} } 30 | ], 31 | "canWrite": [ 32 | { "auth": "User 1", "data": { "black": "anon:1" } } 33 | ] 34 | }, 35 | "games/no_opponent/white": { 36 | "cannotWrite": [ 37 | { "auth": null, "data": "anon:2" }, 38 | { "auth": "User 2", "data": "anon:1" } 39 | ], 40 | "canWrite": [ 41 | { "auth": "User 2", "data": "anon:2" } 42 | ] 43 | }, 44 | "games/in_progress": { 45 | "canRead": [ "User 1", "User 2" ], 46 | "cannotRead": [ null, "User 3" ], 47 | "cannotWrite": [ 48 | { "auth": null, "data": {} }, 49 | { "auth": "User 1", "data": { "black": "anon:1" } } 50 | ] 51 | }, 52 | "moves/123": { 53 | "cannotWrite": [ 54 | { "auth": "User 1", "data": {} }, 55 | { "auth": "User 2", "data": {} }, 56 | { "auth": "User 3", "data": {} } 57 | ] 58 | }, 59 | "moves/in_progress": { 60 | "canRead": [ "User 1", "User 2" ], 61 | "cannotRead": [ null, "User 3" ], 62 | "cannotWrite": [ 63 | { "auth": "User 1", "data": {} }, 64 | { "auth": "User 2", "data": {} }, 65 | { "auth": "User 3", "data": {} } 66 | ] 67 | }, 68 | "moves/in_progress/123": { 69 | "canRead": [ "User 1", "User 2" ], 70 | "cannotRead": [ null, "User 3" ], 71 | "cannotWrite": [ 72 | { "auth": "User 3", "data": { "type": "pass" } } 73 | ], 74 | "canWrite": [ 75 | { "auth": "User 1", "data": { "type": "pass" } }, 76 | { "auth": "User 2", "data": { "type": "pass" } } 77 | ] 78 | }, 79 | "moves/in_progress/0": { 80 | "cannotWrite": [ 81 | { 82 | "auth": "User 1", 83 | "data": { 84 | "type": "move", 85 | "params": { 86 | "x": 1, 87 | "y": 3 88 | } 89 | } 90 | }, 91 | { 92 | "auth": "User 2", 93 | "data": { 94 | "type": "move", 95 | "params": { 96 | "x": 1, 97 | "y": 3 98 | } 99 | } 100 | }, 101 | { 102 | "auth": "User 3", 103 | "data": { 104 | "type": "move", 105 | "params": { 106 | "x": 1, 107 | "y": 3 108 | } 109 | } 110 | } 111 | ] 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jasmine": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | // Polyfill fn.bind() for PhantomJS 2 | /* eslint-disable no-extend-native */ 3 | Function.prototype.bind = require('function-bind') 4 | 5 | // require all test files (files that ends with .spec.js) 6 | var testsContext = require.context('./specs', true, /\.spec$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | var srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/) 13 | srcContext.keys().forEach(srcContext) 14 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // This is a karma config file. For more details see 2 | // http://karma-runner.github.io/0.13/config/configuration-file.html 3 | // we are also using it with karma-webpack 4 | // https://github.com/webpack/karma-webpack 5 | 6 | var webpackConfig = require('../../build/webpack.test.conf') 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | // to run in additional browsers: 11 | // 1. install corresponding karma launcher 12 | // http://karma-runner.github.io/0.13/config/browsers.html 13 | // 2. add it to the `browsers` array below. 14 | browsers: ['PhantomJS'], 15 | frameworks: ['jasmine', 'phantomjs-shim', 'es6-shim'], 16 | reporters: ['spec', 'coverage'], 17 | files: ['./index.js'], 18 | preprocessors: { 19 | './index.js': ['webpack', 'sourcemap'] 20 | }, 21 | webpack: webpackConfig, 22 | webpackMiddleware: { 23 | noInfo: true 24 | }, 25 | coverageReporter: { 26 | dir: './coverage', 27 | reporters: [ 28 | { type: 'lcov', subdir: '.' }, 29 | { type: 'text-summary' } 30 | ] 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /test/unit/specs/Hello.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Hello from '@/components/Hello' 3 | 4 | describe('Hello.vue', () => { 5 | it('should render correct contents', () => { 6 | const vm = new Vue({ 7 | template: '
', 8 | components: { Hello } 9 | }).$mount() 10 | expect(vm.$el.querySelector('.hello h1').textContent).toBe('Hello World!') 11 | }) 12 | }) 13 | 14 | // also see example testing a component with mocks at 15 | // https://github.com/vuejs/vue-loader-example/blob/master/test/unit/a.spec.js#L24-L49 16 | -------------------------------------------------------------------------------- /test/unit/specs/game/actions.spec.js: -------------------------------------------------------------------------------- 1 | 2 | import { WHITE } from '@/game/color'; 3 | import { actions } from '@/game/store'; 4 | 5 | const { addRemoteMove } = actions; 6 | 7 | describe('Game store actions', () => { 8 | describe('adding a remote play', () => { 9 | var dispatch, commit; 10 | 11 | beforeEach(() => { 12 | dispatch = jasmine.createSpy('store dispatch'); 13 | commit = jasmine.createSpy('store commit'); 14 | 15 | addRemoteMove({ dispatch, commit }, { id: '123', move: { type: 'play', params: { x: 0, y: 0, color: WHITE } } }); 16 | }); 17 | 18 | it('sends the player turn', 19 | () => expect(dispatch).toHaveBeenCalledWith('playerTurn', { x: 0, y: 0 })); 20 | 21 | it('Sets the last remote move id', 22 | () => expect(commit).toHaveBeenCalledWith('remote_move', { id: '123' })); 23 | }); 24 | 25 | describe('adding a remote pass', () => { 26 | var dispatch, commit; 27 | 28 | beforeEach(() => { 29 | dispatch = jasmine.createSpy('store dispatcher'); 30 | commit = jasmine.createSpy('store commit'); 31 | 32 | addRemoteMove({ commit, dispatch }, { id: '123', move: { type: 'pass', params: { color: WHITE } } }); 33 | }); 34 | 35 | it('sends the player turn', 36 | () => expect(dispatch).toHaveBeenCalledWith('pass')); 37 | 38 | it('Sets the last remote move id', 39 | () => expect(commit).toHaveBeenCalledWith('remote_move', { id: '123' })); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/unit/specs/game/engine.spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { neighboringPoints, freedoms, validatePlay, play } from '@/game/engine'; 3 | import { parseBoard } from './helpers'; 4 | import FREEDOM_TESTS from './freedomDetection.data'; 5 | import VALID_PLAY_TESTS from './validatePlay.data'; 6 | import PLAY_TESTS from './play.data'; 7 | 8 | describe('game engine', () => { 9 | describe('neighboringPoints', () => { 10 | it('works for points in 0,0 corner', () => { 11 | let n = neighboringPoints(0, 0, 10); 12 | expect(n).toContain({x: 0, y: 1}); 13 | expect(n).toContain({x: 1, y: 0}); 14 | expect(n.length).toBe(2); 15 | }); 16 | 17 | it('works for points in 0,size corner', () => { 18 | let n = neighboringPoints(0, 9, 10); 19 | expect(n).toContain({x: 1, y: 9}); 20 | expect(n).toContain({x: 0, y: 8}); 21 | expect(n.length).toBe(2); 22 | }); 23 | 24 | it('works for points in size,0 corner', () => { 25 | let n = neighboringPoints(9, 0, 10); 26 | expect(n).toContain({y: 1, x: 9}); 27 | expect(n).toContain({y: 0, x: 8}); 28 | expect(n.length).toBe(2); 29 | }); 30 | 31 | it('works for points in size,size corner', () => { 32 | let n = neighboringPoints(9, 9, 10); 33 | expect(n).toContain({y: 8, x: 9}); 34 | expect(n).toContain({y: 9, x: 8}); 35 | expect(n.length).toBe(2); 36 | }); 37 | 38 | it('works along an edge', () => { 39 | let n = neighboringPoints(0, 3, 10); 40 | expect(n.length).toBe(3); 41 | expect(n).toContain({x: 0, y: 2}); 42 | expect(n).toContain({x: 0, y: 4}); 43 | expect(n).toContain({x: 1, y: 3}); 44 | }); 45 | }); 46 | 47 | describe('freedoms', () => { 48 | beforeEach(() => { 49 | jasmine.addCustomEqualityTester((a, b) => { 50 | if (_.isArray(a) && _.isArray(b)) { 51 | return a.length === b.length && _.intersectionWith(a, b, _.isEqual).length === a.length; 52 | } 53 | }); 54 | }); 55 | 56 | for (let i = 0; i < FREEDOM_TESTS.length; ++i) { 57 | let test = FREEDOM_TESTS[i]; 58 | it(test.test, () => { 59 | let {x, y} = test.check; 60 | let { board, expect: e } = parseBoard(test.board); 61 | let res = freedoms({board, size: board.length}, x, y); 62 | 63 | expect(res).toEqual(e); 64 | }); 65 | } 66 | }); 67 | 68 | describe('validatePlay', () => { 69 | for (let i = 0; i < VALID_PLAY_TESTS.length; ++i) { 70 | let test = VALID_PLAY_TESTS[i]; 71 | it(test.test, () => { 72 | let { color } = test.check; 73 | let { board, valid, invalid, ko } = parseBoard(test.board); 74 | 75 | let expectedResult = test.expect; 76 | let check = test.check; 77 | if (valid.length > 0) { 78 | expectedResult = true; 79 | check = valid[0]; 80 | } else if (invalid.length > 0) { 81 | expectedResult = false; 82 | check = invalid[0]; 83 | } 84 | 85 | let { x, y } = check; 86 | 87 | let res = validatePlay({ board, size: board.length, current_turn: color, ko }, x, y); 88 | 89 | expect(res).toEqual(expectedResult); 90 | }); 91 | } 92 | }); 93 | 94 | describe('play', () => { 95 | for (let i = 0; i < PLAY_TESTS.length; ++i) { 96 | let test = PLAY_TESTS[i]; 97 | it(test.test, () => { 98 | let { board: before, check: [{ x, y }] } = parseBoard(test.before); 99 | let { board: after, ko: expectedKo } = parseBoard(test.after); 100 | 101 | let { changes: res, ko } = play({board: before, size: before.length, current_turn: test.turn}, x, y); 102 | 103 | for (let i = 0; i < res.length; ++i) { 104 | let change = res[i]; 105 | before[change.y][change.x] = change.color; 106 | } 107 | 108 | expect(before).toEqual(after); 109 | expect(ko).toEqual(expectedKo); 110 | }); 111 | } 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /test/unit/specs/game/freedomDetection.data.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | test: 'Single unsurrounded stone', 4 | board: 'B?++++\n' + 5 | '?+++++\n' + 6 | '++++++\n' + 7 | '++++++\n' + 8 | '++++++\n' + 9 | '++++++\n', 10 | check: { x: 0, y: 0 } 11 | }, 12 | { 13 | test: 'Simple group line', 14 | board: 'BB?+++\n' + 15 | '??++++\n' + 16 | '++++++\n' + 17 | '++++++\n' + 18 | '++++++\n' + 19 | '++++++\n', 20 | check: { x: 0, y: 0 } 21 | }, 22 | { 23 | test: 'Basic group', 24 | board: 'BB?+++\n' + 25 | '?B?+++\n' + 26 | '+?++++\n' + 27 | '++++++\n' + 28 | '++++++\n' + 29 | '++++++\n', 30 | check: { x: 0, y: 0 } 31 | }, 32 | { 33 | test: 'Circular group', 34 | board: '?BBB?+\n' + 35 | '?B?B?+\n' + 36 | '?BBB?+\n' + 37 | '+???++\n' + 38 | '++++++\n' + 39 | '++++++\n', 40 | check: { x: 1, y: 0 } 41 | }, 42 | { 43 | test: 'Partially surrounded group', 44 | board: 'BBBB?+\n' + 45 | 'BBBW++\n' + 46 | '?WW+++\n' + 47 | '++++++\n' + 48 | '++++++\n' + 49 | '++++++\n', 50 | check: { x: 0, y: 0 } 51 | }, 52 | { 53 | test: 'Completely surrounded group', 54 | board: 'BBBBW+\n' + 55 | 'BBBW++\n' + 56 | 'WWW+++\n' + 57 | '++++++\n' + 58 | '++++++\n' + 59 | '++++++\n', 60 | check: { x: 0, y: 0 } 61 | } 62 | ]; 63 | -------------------------------------------------------------------------------- /test/unit/specs/game/getters/score.data.js: -------------------------------------------------------------------------------- 1 | 2 | import { BLACK, WHITE } from '@/game/color'; 3 | 4 | export default [ 5 | { 6 | test: 'Basic game', 7 | board: '+++WB+\n' + 8 | '++WB++\n' + 9 | '++WB++\n' + 10 | '++WB++\n' + 11 | '++WWB+\n' + 12 | '+++WB+', 13 | expect: { [BLACK]: 9, [WHITE]: 14 } 14 | }, 15 | { 16 | test: 'Basic game, with captures', 17 | board: '+++WB+\n' + 18 | '++WB++\n' + 19 | '++WB++\n' + 20 | '++WB++\n' + 21 | '++WWB+\n' + 22 | '+++WB+', 23 | captures: { [BLACK]: 2, [WHITE]: 3 }, 24 | expect: { [BLACK]: 11, [WHITE]: 17 } 25 | }, 26 | { 27 | test: 'Tied game, due to no owned territory', 28 | board: '+++WB+\n' + 29 | '++WB++\n' + 30 | '++++++\n' + 31 | '++WB++\n' + 32 | '++WWB+\n' + 33 | '+++WB+', 34 | expect: { [BLACK]: 0, [WHITE]: 0 } 35 | }, 36 | { 37 | test: 'Basic game, with eyes', 38 | board: '+++WB+\n' + 39 | '++WB++\n' + 40 | '+WWBB+\n' + 41 | 'W+WB+B\n' + 42 | '+WWWB+\n' + 43 | '+++WB+', 44 | expect: { [BLACK]: 7, [WHITE]: 11 } 45 | } 46 | ]; 47 | -------------------------------------------------------------------------------- /test/unit/specs/game/getters/score.spec.js: -------------------------------------------------------------------------------- 1 | 2 | import { getters } from '@/game/store'; 3 | import { BLACK, WHITE } from '@/game/color'; 4 | import { parseBoard } from '../helpers'; 5 | import SCORE_TESTS from './score.data'; 6 | 7 | const { score } = getters; 8 | 9 | describe('game score', () => { 10 | for (let i = 0; i < SCORE_TESTS.length; ++i) { 11 | let test = SCORE_TESTS[i]; 12 | it(test.test, () => { 13 | let { captures = { [BLACK]: 0, [WHITE]: 0 }, expect: expectedScore } = test; 14 | let { board } = parseBoard(test.board); 15 | 16 | let testScore = score({board, size: board.length, captures}); 17 | 18 | expect(testScore).toEqual(expectedScore); 19 | }); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /test/unit/specs/game/getters/waitingForRemoteOpponent.spec.js: -------------------------------------------------------------------------------- 1 | 2 | import { state, mutations, getters } from '@/game/store'; 3 | 4 | const { waitingForRemoteOpponent } = getters; 5 | const { new_local_game, new_remote_game, remote_opponent_accepted } = mutations; 6 | 7 | describe('waitingForRemoteOpponent getter', () => { 8 | describe('for a local game', () => { 9 | it('returns false', () => { 10 | let s = Object.assign({}, state); 11 | 12 | new_local_game(s); 13 | 14 | expect(waitingForRemoteOpponent(s)).toBe(false); 15 | }); 16 | }); 17 | 18 | describe('for a new remote game', () => { 19 | it('returns true', () => { 20 | let s = Object.assign({}, state); 21 | 22 | new_remote_game(s, { size: 9 }); 23 | 24 | expect(waitingForRemoteOpponent(s)).toBe(true); 25 | }); 26 | }); 27 | 28 | describe('for a remote game after the opponent accepts', () => { 29 | it('returns false', () => { 30 | let s = Object.assign({}, state); 31 | 32 | new_remote_game(s, { size: 9 }); 33 | 34 | remote_opponent_accepted(s, { opponentId: '123' }); 35 | 36 | expect(waitingForRemoteOpponent(s)).toBe(false); 37 | }); 38 | }) 39 | }); 40 | -------------------------------------------------------------------------------- /test/unit/specs/game/helpers.js: -------------------------------------------------------------------------------- 1 | import { BLACK, WHITE } from '@/game/color'; 2 | import { matrix } from '@/arrays'; 3 | 4 | export function parseBoard (s) { 5 | let valid = []; 6 | let invalid = []; 7 | let expect = []; 8 | let ko = null; 9 | 10 | let lines = s.split('\n'); 11 | let board = matrix(lines.length, lines.length); 12 | 13 | for (let i = 0; i < lines.length; ++i) { 14 | let line = lines[i]; 15 | for (let j = 0; j < line.length; ++j) { 16 | switch (line[j]) { 17 | case 'B': 18 | board[i][j] = BLACK; 19 | break; 20 | case 'W': 21 | board[i][j] = WHITE; 22 | break; 23 | case 'K': 24 | ko = { x: j, y: i }; 25 | break; 26 | case '?': 27 | expect.push({ x: j, y: i }); 28 | break; 29 | case '✓': 30 | valid.push({ x: j, y: i }); 31 | break; 32 | case '×': 33 | invalid.push({ x: j, y: i }); 34 | break; 35 | } 36 | } 37 | } 38 | 39 | return { board, check: expect, expect, valid, invalid, ko }; 40 | } 41 | -------------------------------------------------------------------------------- /test/unit/specs/game/play.data.js: -------------------------------------------------------------------------------- 1 | 2 | import { BLACK, WHITE } from '@/game/color'; 3 | 4 | export default [ 5 | { 6 | test: 'Empty board', 7 | before: '?+++++\n' + 8 | '++++++\n' + 9 | '++++++\n' + 10 | '++++++\n' + 11 | '++++++\n' + 12 | '++++++\n', 13 | after: 'B+++++\n' + 14 | '++++++\n' + 15 | '++++++\n' + 16 | '++++++\n' + 17 | '++++++\n' + 18 | '++++++\n', 19 | turn: BLACK 20 | }, 21 | { 22 | test: 'Single enemy to kill', 23 | before: 'B?++++\n' + 24 | 'W+++++\n' + 25 | '++++++\n' + 26 | '++++++\n' + 27 | '++++++\n' + 28 | '++++++\n', 29 | after: '+W++++\n' + 30 | 'W+++++\n' + 31 | '++++++\n' + 32 | '++++++\n' + 33 | '++++++\n' + 34 | '++++++\n', 35 | turn: WHITE 36 | }, 37 | { 38 | test: 'Single enemy to kill mid-board', 39 | before: '++++++\n' + 40 | '++++++\n' + 41 | '++W+++\n' + 42 | '+WBW++\n' + 43 | '++?+++\n' + 44 | '++++++\n', 45 | after: '++++++\n' + 46 | '++++++\n' + 47 | '++W+++\n' + 48 | '+W+W++\n' + 49 | '++W+++\n' + 50 | '++++++\n', 51 | turn: WHITE 52 | }, 53 | { 54 | test: 'Enemy group to kill', 55 | before: 'BBBW++\n' + 56 | 'WW?+++\n' + 57 | '++++++\n' + 58 | '++++++\n' + 59 | '++++++\n' + 60 | '++++++\n', 61 | after: '+++W++\n' + 62 | 'WWW+++\n' + 63 | '++++++\n' + 64 | '++++++\n' + 65 | '++++++\n' + 66 | '++++++\n', 67 | turn: WHITE 68 | }, 69 | { 70 | test: 'Avoiding suicide by capturing enemy', 71 | before: 'BBBW++\n' + 72 | 'WW?B++\n' + 73 | 'BBB+++\n' + 74 | '++++++\n' + 75 | '++++++\n' + 76 | '++++++\n', 77 | after: '+++W++\n' + 78 | 'WWWB++\n' + 79 | 'BBB+++\n' + 80 | '++++++\n' + 81 | '++++++\n' + 82 | '++++++\n', 83 | turn: WHITE 84 | }, 85 | { 86 | test: 'Capturing a stone and creating ko point', 87 | before: '+BW+++\n' + 88 | 'BW?W++\n' + 89 | '+BW+++\n' + 90 | '++++++\n' + 91 | '++++++\n' + 92 | '++++++\n', 93 | after: '+BW+++\n' + 94 | 'BKBW++\n' + 95 | '+BW+++\n' + 96 | '++++++\n' + 97 | '++++++\n' + 98 | '++++++\n', 99 | turn: BLACK 100 | } 101 | ]; 102 | -------------------------------------------------------------------------------- /test/unit/specs/game/store.spec.js: -------------------------------------------------------------------------------- 1 | import { state, mutations } from '@/game/store'; 2 | import { BLACK, WHITE } from '@/game/color'; 3 | const { 4 | player_turn, 5 | pass_turn, 6 | new_local_game, 7 | new_remote_game, 8 | join_remote_game, 9 | cancel_remote_game, 10 | remote_move, 11 | remote_opponent_accepted 12 | } = mutations; 13 | 14 | describe('game store', () => { 15 | describe('starting a new game', () => { 16 | var s; 17 | 18 | beforeAll(() => { 19 | s = Object.assign({}, state); 20 | 21 | new_local_game(s, 13); 22 | player_turn(s, { x: 0, y: 0 }); 23 | player_turn(s, { x: 0, y: 1 }); 24 | player_turn(s, { x: 10, y: 10 }); 25 | player_turn(s, { x: 1, y: 0 }); 26 | 27 | new_local_game(s, 19); 28 | }); 29 | 30 | it('has the specified size', 31 | () => expect(s.size).toBe(19)); 32 | 33 | it('has a game type of "local"', 34 | () => expect(s.gameType).toBe('local')); 35 | 36 | it('has 0 captures for both colors', () => { 37 | expect(s.captures[BLACK]).toBe(0); 38 | expect(s.captures[WHITE]).toBe(0); 39 | }); 40 | 41 | it('has no previous passes', 42 | () => expect(s.pass_last_turn).toBe(false)); 43 | 44 | it('is not done', 45 | () => expect(s.game_done).toBe(false)); 46 | 47 | it('has the correct number of rows', 48 | () => expect(s.board.length).toBe(19)); 49 | it('has the correct first turn', 50 | () => expect(s.current_turn).toBe(BLACK)); 51 | }); 52 | 53 | describe('game play', () => { 54 | describe('without an active game', () => { 55 | it('player turn does nothing', () => { 56 | let s = Object.assign({}, state); 57 | player_turn(s, { x: 1, y: 2 }); 58 | 59 | expect(s).toEqual(state); 60 | }); 61 | }); 62 | 63 | describe('with an active game', () => { 64 | let s = Object.assign({}, state); 65 | beforeEach(() => new_local_game(s, 19)); 66 | 67 | describe('player turn', () => { 68 | it('should update the board', () => { 69 | player_turn(s, { x: 1, y: 2 }); 70 | 71 | expect(s.board[2][1]).toBe(BLACK); 72 | expect(s.current_turn).toBe(WHITE); 73 | expect(s.pass_last_turn).toBe(false); 74 | }); 75 | }); 76 | 77 | describe('passing a turn', () => { 78 | it('should leave the board as is, and change the current turn', () => { 79 | pass_turn(s); 80 | 81 | // TODO: Validate no changes to board. 82 | expect(s.current_turn).toBe(WHITE); 83 | expect(s.pass_last_turn).toBe(true); 84 | }); 85 | }); 86 | 87 | describe('two consecutive passes', () => { 88 | it('should mark the game as done', () => { 89 | pass_turn(s); 90 | pass_turn(s); 91 | 92 | expect(s.current_turn).toBeNull(); 93 | expect(s.game_done).toBe(true); 94 | }) 95 | }); 96 | 97 | describe('player turn to capture a piece', () => { 98 | beforeEach(() => { 99 | player_turn(s, { x: 0, y: 0 }); 100 | player_turn(s, { x: 0, y: 1 }); 101 | player_turn(s, { x: 10, y: 10 }); 102 | }); 103 | 104 | it('should update the captures appropriately', () => { 105 | let turn = s.current_turn; 106 | player_turn(s, { x: 1, y: 0 }); 107 | 108 | expect(s.captures[turn]).toBe(1); 109 | }); 110 | }); 111 | 112 | describe('attempting to play an occupied location', () => { 113 | let s = Object.assign({}, state); 114 | beforeEach(() => { 115 | new_local_game(s, 19); 116 | player_turn(s, { x: 1, y: 1 }); 117 | }); 118 | 119 | it('should not update the board', () => { 120 | player_turn(s, { x: 1, y: 1 }); 121 | 122 | expect(s.board[1][1]).toBe(BLACK); 123 | expect(s.current_turn).toBe(WHITE); 124 | }); 125 | }); 126 | }); 127 | }); 128 | 129 | describe('starting a new remote game', () => { 130 | var s; 131 | 132 | beforeAll(() => { 133 | s = Object.assign({}, state); 134 | 135 | new_local_game(s, 13); 136 | player_turn(s, { x: 0, y: 0 }); 137 | player_turn(s, { x: 10, y: 10 }); 138 | player_turn(s, { x: 1, y: 0 }); 139 | 140 | new_remote_game(s, { size: 19, gameId: '1234', inviteId: '321', [BLACK]: '0123' }); 141 | }); 142 | 143 | it('has the specified size', 144 | () => expect(s.size).toBe(19)); 145 | 146 | it('has a game type of "remote"', 147 | () => expect(s.gameType).toBe('remote')); 148 | 149 | it('has 0 captures for both colors', () => { 150 | expect(s.captures[BLACK]).toBe(0); 151 | expect(s.captures[WHITE]).toBe(0); 152 | }); 153 | 154 | it('has no previous passes', 155 | () => expect(s.pass_last_turn).toBe(false)); 156 | 157 | it('is not done', 158 | () => expect(s.game_done).toBe(false)); 159 | 160 | it('has the correct number of rows', 161 | () => expect(s.board.length).toBe(19)); 162 | 163 | it('has the correct first turn', 164 | () => expect(s.current_turn).toBe(BLACK)); 165 | 166 | it('has the remote game id', 167 | () => expect(s.remoteGameId).toBe('1234')); 168 | 169 | it('has the remote invite id', 170 | () => expect(s.remoteInviteId).toBe('321')); 171 | 172 | it('has the correct black player ID', 173 | () => expect(s[BLACK]).toBe('0123')); 174 | }); 175 | 176 | describe('canceling a remote game', () => { 177 | var s; 178 | beforeAll(() => { 179 | s = Object.assign({}, state); 180 | 181 | new_remote_game(s, { size: 19, gameId: '1234', inviteId: '321', [BLACK]: '0123' }); 182 | 183 | cancel_remote_game(s); 184 | }); 185 | 186 | it('should have a placeholder game', 187 | () => expect(s.gameType).toEqual('placeholder')); 188 | 189 | it('should not have a remote game id', 190 | () => expect(s.remoteGameId).toBeNull()); 191 | 192 | it('should have no invite ID', 193 | () => expect(s.remoteInviteId).toBeNull()); 194 | 195 | it('should have no black player ID', 196 | () => expect(s[BLACK]).toBeNull()); 197 | 198 | it('should have no white player ID', 199 | () => expect(s[WHITE]).toBeNull()); 200 | 201 | it('should have no current turn', 202 | () => expect(s.current_turn).toBeNull()); 203 | 204 | it('should have the default size', 205 | () => expect(s.size).toBe(state.size)); 206 | }); 207 | 208 | describe('joining a remote game', () => { 209 | var s; 210 | 211 | beforeAll(() => { 212 | s = Object.assign({}, state); 213 | 214 | new_local_game(s, 13); 215 | player_turn(s, { x: 0, y: 0 }); 216 | player_turn(s, { x: 10, y: 10 }); 217 | player_turn(s, { x: 1, y: 0 }); 218 | 219 | join_remote_game(s, { size: 19, gameId: '1234', [BLACK]: '0123', [WHITE]: '3210' }); 220 | }); 221 | 222 | it('has the specified size', 223 | () => expect(s.size).toBe(19)); 224 | 225 | it('has a game type of "remote"', 226 | () => expect(s.gameType).toBe('remote')); 227 | 228 | it('has 0 captures for both colors', () => { 229 | expect(s.captures[BLACK]).toBe(0); 230 | expect(s.captures[WHITE]).toBe(0); 231 | }); 232 | 233 | it('has no previous passes', 234 | () => expect(s.pass_last_turn).toBe(false)); 235 | 236 | it('is not done', 237 | () => expect(s.game_done).toBe(false)); 238 | 239 | it('has the correct number of rows', 240 | () => expect(s.board.length).toBe(19)); 241 | 242 | it('has the correct first turn', 243 | () => expect(s.current_turn).toBe(BLACK)); 244 | 245 | it('has the remote game id', 246 | () => expect(s.remoteGameId).toBe('1234')); 247 | 248 | it('has the correct black player ID', 249 | () => expect(s[BLACK]).toBe('0123')); 250 | 251 | it('has the correct white player ID', 252 | () => expect(s[WHITE]).toBe('3210')); 253 | }); 254 | 255 | describe('an opponent accepting your remote game invitation', () => { 256 | var s; 257 | 258 | beforeAll(() => { 259 | s = Object.assign({}, state); 260 | 261 | new_remote_game(s, { size: 19, gameId: '1234', [BLACK]: '0123' }); 262 | 263 | remote_opponent_accepted(s, { opponentId: '4321' }); 264 | }); 265 | 266 | it('updates the white player ID', 267 | () => expect(s[WHITE]).toBe('4321')); 268 | }); 269 | 270 | describe('playing a remote move', () => { 271 | let s = null; 272 | 273 | beforeEach(() => { 274 | s = Object.assign({}, state); 275 | 276 | new_remote_game(s, { size: 9, gameId: '123', inviteId: '321' }); 277 | 278 | player_turn(s, { x: 0, y: 0 }); 279 | remote_move(s, '123'); 280 | }); 281 | 282 | it('stores the last remote move id', 283 | () => expect(s.lastRemoteMove).toBe('123')); 284 | }); 285 | }); 286 | -------------------------------------------------------------------------------- /test/unit/specs/game/validatePlay.data.js: -------------------------------------------------------------------------------- 1 | import { BLACK, WHITE } from '@/game/color'; 2 | 3 | export default [ 4 | { 5 | test: 'Empty board', 6 | board: '✓+++++\n' + 7 | '++++++\n' + 8 | '++++++\n' + 9 | '++++++\n' + 10 | '++++++\n' + 11 | '++++++\n', 12 | check: { color: BLACK } 13 | }, 14 | { 15 | test: 'Same color neighbor', 16 | board: 'B✓++++\n' + 17 | '++++++\n' + 18 | '++++++\n' + 19 | '++++++\n' + 20 | '++++++\n' + 21 | '++++++\n', 22 | check: { color: BLACK }, 23 | expect: true 24 | }, 25 | { 26 | test: 'Occupied point', 27 | board: 'W+++++\n' + 28 | '++++++\n' + 29 | '++++++\n' + 30 | '++++++\n' + 31 | '++++++\n' + 32 | '++++++\n', 33 | check: { x: 0, y: 0, color: BLACK }, 34 | expect: false 35 | }, 36 | { 37 | test: 'Ko point', 38 | board: '++++++\n' + 39 | '++++++\n' + 40 | '++WB++\n' + 41 | '+WBKB+\n' + 42 | '++WW++\n' + 43 | '++++++\n', 44 | check: { x: 3, y: 3, color: WHITE }, 45 | expect: false 46 | }, 47 | { 48 | test: 'Surrounded corner', 49 | board: '×W++++\n' + 50 | 'W+++++\n' + 51 | '++++++\n' + 52 | '++++++\n' + 53 | '+++B++\n' + 54 | '++++++\n', 55 | check: { color: BLACK } 56 | }, 57 | { 58 | test: 'Surrounded Middle', 59 | board: '+W++++\n' + 60 | 'W+++++\n' + 61 | '++BBB+\n' + 62 | 'W+B×B+\n' + 63 | '++BBB+\n' + 64 | '++++++\n', 65 | check: { color: WHITE } 66 | }, 67 | { 68 | test: 'Surrounded middle of like color', 69 | board: '+W++++\n' + 70 | 'W+++++\n' + 71 | '++BBB+\n' + 72 | 'W+B✓B+\n' + 73 | '++BBB+\n' + 74 | '++++++\n', 75 | check: { color: BLACK } 76 | }, 77 | { 78 | test: 'Surrounded middle with friendly alive neighbor', 79 | board: '+W++++\n' + 80 | 'W+++++\n' + 81 | '++BBB+\n' + 82 | 'W+B✓W+\n' + 83 | '++BBB+\n' + 84 | '++++++\n', 85 | check: { color: WHITE } 86 | }, 87 | { 88 | test: 'Surrounded middle with friendly surrounded neighbor', 89 | board: '+W++++\n' + 90 | 'W+++++\n' + 91 | '++BBB+\n' + 92 | 'W+B×WB\n' + 93 | '++BBB+\n' + 94 | '++++++\n', 95 | check: { color: WHITE } 96 | }, 97 | { 98 | test: 'Surrounded middle with multiple friendly surrounded neighbor', 99 | board: '+W+BB+\n' + 100 | 'W+BWWB\n' + 101 | '++BWB+\n' + 102 | 'W+B×WB\n' + 103 | '++BBB+\n' + 104 | '++++++\n', 105 | check: { color: WHITE } 106 | }, 107 | { 108 | test: 'Surrounded corner with vulnerable enemy neighbor', 109 | board: '✓WB+++\n' + 110 | 'WB++++\n' + 111 | '++++++\n' + 112 | '++++++\n' + 113 | '++++++\n' + 114 | '++++++\n', 115 | check: { color: BLACK } 116 | } 117 | ]; 118 | --------------------------------------------------------------------------------