├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── TODO.txt ├── app.js ├── npm-shrinkwrap.json ├── package.json ├── public ├── css │ ├── dist │ │ ├── all.css │ │ └── all.min.css │ └── src │ │ ├── jquery.qtip.less │ │ ├── reset.less │ │ └── style.less ├── img │ ├── favicon.ico │ ├── tiles │ │ ├── ball_1.png │ │ ├── ball_2.png │ │ ├── ball_3.png │ │ ├── ball_4.png │ │ ├── ball_5.png │ │ ├── ball_6.png │ │ ├── ball_7.png │ │ ├── ball_8.png │ │ ├── ball_9.png │ │ ├── bamboo_1.png │ │ ├── bamboo_2.png │ │ ├── bamboo_3.png │ │ ├── bamboo_4.png │ │ ├── bamboo_5.png │ │ ├── bamboo_6.png │ │ ├── bamboo_7.png │ │ ├── bamboo_8.png │ │ ├── bamboo_9.png │ │ ├── character_1.png │ │ ├── character_2.png │ │ ├── character_3.png │ │ ├── character_4.png │ │ ├── character_5.png │ │ ├── character_6.png │ │ ├── character_7.png │ │ ├── character_8.png │ │ ├── character_9.png │ │ ├── dragon_green.png │ │ ├── dragon_red.png │ │ ├── dragon_white.png │ │ ├── flower_bamboo.png │ │ ├── flower_chrysanthemum.png │ │ ├── flower_orchid.png │ │ ├── flower_plum.png │ │ ├── season_fall.png │ │ ├── season_spring.png │ │ ├── season_summer.png │ │ ├── season_winter.png │ │ ├── wind_east.png │ │ ├── wind_north.png │ │ ├── wind_south.png │ │ └── wind_west.png │ └── tiles_old │ │ ├── ball_1.png │ │ ├── ball_2.png │ │ ├── ball_3.png │ │ ├── ball_4.png │ │ ├── ball_5.png │ │ ├── ball_6.png │ │ ├── ball_7.png │ │ ├── ball_8.png │ │ ├── ball_9.png │ │ ├── bamboo_1.png │ │ ├── bamboo_2.png │ │ ├── bamboo_3.png │ │ ├── bamboo_4.png │ │ ├── bamboo_5.png │ │ ├── bamboo_6.png │ │ ├── bamboo_7.png │ │ ├── bamboo_8.png │ │ ├── bamboo_9.png │ │ ├── character_1.png │ │ ├── character_2.png │ │ ├── character_3.png │ │ ├── character_4.png │ │ ├── character_5.png │ │ ├── character_6.png │ │ ├── character_7.png │ │ ├── character_8.png │ │ ├── character_9.png │ │ ├── dragon_green.png │ │ ├── dragon_red.png │ │ ├── dragon_white.png │ │ ├── flower_bamboo.png │ │ ├── flower_chrysanthemum.png │ │ ├── flower_orchid.png │ │ ├── flower_plum.png │ │ ├── season_fall.png │ │ ├── season_spring.png │ │ ├── season_summer.png │ │ ├── season_winter.png │ │ ├── wind_east.png │ │ ├── wind_north.png │ │ ├── wind_south.png │ │ └── wind_west.png ├── js │ ├── dist │ │ ├── all.js │ │ └── all.min.js │ ├── global │ │ ├── jquery.min.js │ │ ├── jquery.qtip.min.js │ │ ├── socket.io.min.js │ │ ├── swig.min.js │ │ └── underscore-min.js │ └── src │ │ └── client.js └── sound │ ├── jingle.aiff │ ├── jingle.mp3 │ ├── jingle.ogg │ ├── jingle.wav │ ├── tense-challenge.aiff │ ├── tense-challenge.mp3 │ ├── tense-challenge.ogg │ ├── tense-challenge.wav │ ├── tile-down.aiff │ ├── tile-down.mp3 │ ├── tile-down.ogg │ └── tile-down.wav ├── scripts ├── pre-commit.sh └── run-tests.sh ├── server ├── ami.js ├── config.js ├── connect-githubhook.js ├── crypto.js ├── db.js ├── mahjong.js ├── models.js ├── routes.js └── shanten.js ├── shared ├── mahjong_util.js └── shared.js ├── test └── test.js └── views ├── base.html ├── game.html ├── home.html ├── lobby.html └── partials ├── board.html ├── board_simulation.html ├── discard_tiles.html └── tile.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | .gaedata/ 4 | *#* 5 | .ropeproject 6 | node_modules 7 | deploy 8 | .tern-port 9 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | module.exports = function(grunt) { 3 | var port = grunt.option('port') || 3000, 4 | js_src_files = ['public/js/global/**/*.js', 5 | 'shared/**/*.js', 6 | 'public/js/src/**/*.js'], 7 | less_src_files = ['public/css/src/**/*.less']; 8 | 9 | // Project configuration 10 | grunt.initConfig({ 11 | pkg: grunt.file.readJSON('package.json'), 12 | meta: { 13 | banner: 14 | '/*!\n' + 15 | ' * mahjong.js <%= pkg.version %> (<%= grunt.template.today("yyyy-mm-dd, HH:MM") %>)\n' + 16 | ' * http://gleitzman.com/apps/mahjong\n' + 17 | ' * MIT licensed\n' + 18 | ' *\n' + 19 | ' * Copyright (C) 2013 Benjamin Gleitzman, http://gleitzman.com\n' + 20 | ' */' 21 | }, 22 | 23 | concat: { 24 | options: { 25 | separator: ';\n' 26 | }, 27 | dist: { 28 | src: js_src_files, 29 | dest: 'public/js/dist/all.js' 30 | } 31 | }, 32 | 33 | uglify: { 34 | options: { 35 | banner: '<%= meta.banner %>\n' 36 | }, 37 | build: { 38 | src: js_src_files, 39 | dest: 'public/js/dist/all.min.js' 40 | } 41 | }, 42 | 43 | less: { 44 | development: { 45 | files: { 46 | 'public/css/dist/all.css': less_src_files 47 | } 48 | }, 49 | production: { 50 | options: { 51 | cleancss: true 52 | }, 53 | files: { 54 | 'public/css/dist/all.min.css': less_src_files 55 | } 56 | } 57 | }, 58 | 59 | mochaTest: { 60 | test: { 61 | options: { 62 | reporter: 'spec' 63 | }, 64 | src: ['test/**/*.js'] 65 | } 66 | }, 67 | 68 | concurrent: { 69 | compress: ['less', 'concat', 'uglify'], 70 | start: { 71 | tasks: ['mochaTest', 'nodemon', 'watch'], 72 | options: { 73 | logConcurrentOutput: true 74 | } 75 | } 76 | }, 77 | 78 | nodemon: { 79 | dev: { 80 | options: { 81 | nodeArgs: ['--port', port] 82 | } 83 | } 84 | }, 85 | 86 | watch: { 87 | files: ['public/**/*.*', 88 | 'shared/**/*.*', 89 | 'views/partials/**/*.*', 90 | '!**/dist/**'], // ignore dist folder 91 | tasks: ['concurrent:compress'] 92 | } 93 | 94 | }); 95 | 96 | // Dependencies 97 | grunt.loadNpmTasks('grunt-contrib-less'); 98 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 99 | grunt.loadNpmTasks('grunt-contrib-concat'); 100 | grunt.loadNpmTasks('grunt-contrib-uglify'); 101 | grunt.loadNpmTasks('grunt-contrib-watch'); 102 | grunt.loadNpmTasks('grunt-mocha-test'); 103 | grunt.loadNpmTasks('grunt-nodemon'); 104 | grunt.loadNpmTasks('grunt-concurrent'); 105 | 106 | // Run tests 107 | grunt.registerTask('test', [ 'mochaTest' ] ); 108 | 109 | // Default tasks 110 | grunt.registerTask('default', ['concurrent:start']); 111 | }; 112 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Benjamin Gleitzman (gleitz@mit.edu) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![mahjong tiles](https://dl.dropboxusercontent.com/u/101688/website/img/mahjong-tiles.jpg) 2 | 3 | # Cock-eyed Mahjong (Riichi Mahjong) 4 | 5 | #### Application for simulating, analyzing, and playing [Three-Player mahjong](https://corp.mahjongclub.com/3-player-riichi). #### 6 | 7 | Example 8 | ------- 9 | 10 | For an example of the mahjong application and AI visit [http://gleitzman.com/apps/mahjong](http://gleitzman.com/apps/mahjong). 11 | 12 | Usage 13 | ----- 14 | 15 | Install dependencies with `npm update`. 16 | 17 | Start the application with 18 | 19 | node app.js 20 | 21 | Then visit [http://localhost:3000](http://localhost:3000). 22 | 23 | To analyze a hand visit [http://localhost:3000/analyze/1p 123456789s BGR9](http://localhost:3000/analyze/1p%20123456789s%20BGR9). 24 | 25 | Development 26 | ------ 27 | 28 | `sudo docker run -d --restart unless-stopped -p 27027-27029:27017-27019 --name mongodb-2.6.12 mongo:2.6.12` 29 | Install dev dependencies with `npm install` and start the application with `grunt`. 30 | 31 | 32 | Author 33 | ------ 34 | 35 | - Benjamin Gleitzman ([@gleitz](http://github.com/gleitz)) 36 | 37 | 38 | Notes 39 | ----- 40 | 41 | - Special thanks to Sebastian Heuchler for shanten calculation. 42 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | - Add the winds 2 | - Enhance mobile layout 3 | - Make website responsive 4 | 5 | - allow kan (use game 52bc5ba205ef4692650ebbdd/ 52bcb8c417c8e10bd1554771) 6 | - Reconstruct the rest of the board http://www.indiegames.com/images/timw/mahjong2a.jpg 7 | - Make game_id always come before player_id 8 | - Break rendering down into two phases that share a common template 9 | - Add production TODOs and detection 10 | - switch to rendered templates 11 | - add reach 12 | - detect broken game ids 13 | 14 | - count down the time you have to pon 15 | - scrolling text window with historical movements 16 | - figure out why game over man appears when clicking fast 17 | - disable animation mobile 18 | - advanced pon for computer (when you have a dragon you can pon anything) 19 | - set up automatic pull with testing 20 | - make seats go in the correct order 21 | - load images at start, some amount of time after page loads 22 | - make pon button appear 23 | - rename computers 24 | - add sound effect toggle 25 | - more prominent you are the winner 26 | - don't show placeholder when someone is thinking about poning 27 | 28 | Notes 29 | To duplicate a row 30 | db.games.find({_id: ObjectId("52bc5ba205ef4692650ebbdd")}).forEach(function(x) { x._id = new ObjectId(); db.games.insert(x); print("" + x._id); }) 31 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /*global __dirname require process */ 2 | 3 | /* 4 | * Server-side application for simulating a mahjong game. 5 | * Usage: node app.js 6 | * Then visit http://localhost:3000/game 7 | */ 8 | var argv = require('optimist').argv, 9 | config = require('./server/config'), 10 | cgh = require('./server/connect-githubhook'), 11 | crypto = require('./server/crypto'), 12 | db = require('./server/db'), 13 | express = require('express'), 14 | path = require('path'), 15 | shared = require('./shared/shared'), 16 | swig = require('swig'); 17 | 18 | var MongoStore = require('connect-mongo')(express) 19 | 20 | require('dotenv').config() 21 | 22 | var app = express(), 23 | server = require('http').createServer(app), 24 | io = require('socket.io').listen(server), 25 | cookieParser = express.cookieParser(config.EXPRESS_COOKIE_SECRET); 26 | 27 | io.set('log level',0); 28 | 29 | // Connect to the DB and then start the application 30 | db.init(function(error) { 31 | if (error) { 32 | throw error; 33 | } 34 | 35 | var sessionStore = new MongoStore({db: db.session, auto_reconnect: true/*, username: process.env.DB_USER, password: process.env.DB_PASS*/}); 36 | 37 | // TODO(gleitz): disable in production 38 | // io.set('log level', 1); // reduce logging 39 | 40 | app.configure(function(){ 41 | app.use(express['static'](__dirname + '/public')); 42 | app.use(express.bodyParser()); 43 | app.use(express.methodOverride()); 44 | app.use(cookieParser); 45 | app.use(express.session({ 46 | store: sessionStore, 47 | secret: config.EXPRESS_SESSION_SECRET 48 | })); 49 | app.use(app.router); 50 | app.use(express.favicon(path.join(__dirname, 'public/img/favicon.ico'))); 51 | app.use(express.errorHandler({dumpExceptions: true, 52 | showStack: true})); 53 | 54 | // github hook reloading 55 | var github_hook_path = '/' + config.GITHUBHOOK_SECRET, 56 | github_hook_obj = {}; 57 | github_hook_obj[github_hook_path] = {url: 'https://github.com/gleitz/mahjong', 58 | branch: 'master'} 59 | app.use(cgh(github_hook_obj, 60 | function(repo, payload) { 61 | console.log('Post-receive trigger. Exiting in 1 second'); 62 | setTimeout(function() { 63 | process.exit(1); 64 | }, 1000); 65 | })); 66 | }); 67 | 68 | // Swig templating 69 | app.engine('html', swig.renderFile); 70 | app.set('view engine', 'html'); 71 | app.set('views', __dirname + '/views'); 72 | shared.augmentSwig(swig); 73 | 74 | // Disable the cache 75 | // TODO: remove in production 76 | swig.setDefaults({ cache: false }); 77 | app.set('view cache', false); 78 | 79 | // Add the application routes 80 | require('./server/routes').addRoutes(app); 81 | // Add socket.io triggers 82 | require('./server/routes').addSockets(io); 83 | 84 | // Socket.io session configuration 85 | io.set('authorization', function (data, callback) { 86 | if (data && data.query && data.query.token) { 87 | var session_id = crypto.decrypt(data.query.token); 88 | sessionStore.get(session_id, function (error, session) { 89 | // Add the session_id. This will show up in 90 | // socket.handshake.session_id. 91 | data.session_id = session_id; 92 | if (error) { 93 | callback('ERROR', false); 94 | } else if (!session) { 95 | callback('NO_SESSION', false); 96 | } else { 97 | // Add the session. This will show up in 98 | // socket.handshake.session. 99 | data.session = session; 100 | data.session.id = session_id; 101 | callback(null, true); 102 | } 103 | }); 104 | } else { 105 | callback('NO_TOKEN', false); 106 | } 107 | }); 108 | 109 | var port = argv.port || 10003; 110 | server.listen(port); 111 | console.log('listening on ' + port); 112 | }); 113 | -------------------------------------------------------------------------------- /npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mahjong", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "connect": { 6 | "version": "2.11.0", 7 | "from": "connect@2.11.0", 8 | "resolved": "https://registry.npmjs.org/connect/-/connect-2.11.0.tgz", 9 | "dependencies": { 10 | "qs": { 11 | "version": "0.6.5", 12 | "from": "qs@0.6.5", 13 | "resolved": "https://registry.npmjs.org/qs/-/qs-0.6.5.tgz" 14 | }, 15 | "cookie-signature": { 16 | "version": "1.0.1", 17 | "from": "cookie-signature@1.0.1", 18 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.1.tgz" 19 | }, 20 | "buffer-crc32": { 21 | "version": "0.2.1", 22 | "from": "buffer-crc32@0.2.1", 23 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.1.tgz" 24 | }, 25 | "cookie": { 26 | "version": "0.1.0", 27 | "from": "cookie@0.1.0", 28 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz" 29 | }, 30 | "send": { 31 | "version": "0.1.4", 32 | "from": "send@0.1.4", 33 | "resolved": "https://registry.npmjs.org/send/-/send-0.1.4.tgz", 34 | "dependencies": { 35 | "mime": { 36 | "version": "1.2.11", 37 | "from": "mime@>=1.2.9 <1.3.0", 38 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz" 39 | }, 40 | "range-parser": { 41 | "version": "0.0.4", 42 | "from": "range-parser@0.0.4", 43 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-0.0.4.tgz" 44 | } 45 | } 46 | }, 47 | "bytes": { 48 | "version": "0.2.1", 49 | "from": "bytes@0.2.1", 50 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-0.2.1.tgz" 51 | }, 52 | "fresh": { 53 | "version": "0.2.0", 54 | "from": "fresh@0.2.0", 55 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.0.tgz" 56 | }, 57 | "pause": { 58 | "version": "0.0.1", 59 | "from": "pause@0.0.1", 60 | "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz" 61 | }, 62 | "uid2": { 63 | "version": "0.0.3", 64 | "from": "uid2@0.0.3", 65 | "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz" 66 | }, 67 | "debug": { 68 | "version": "4.3.1", 69 | "from": "debug@*", 70 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 71 | "dependencies": { 72 | "ms": { 73 | "version": "2.1.2", 74 | "from": "ms@2.1.2", 75 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" 76 | } 77 | } 78 | }, 79 | "methods": { 80 | "version": "0.0.1", 81 | "from": "methods@0.0.1", 82 | "resolved": "https://registry.npmjs.org/methods/-/methods-0.0.1.tgz" 83 | }, 84 | "raw-body": { 85 | "version": "0.0.3", 86 | "from": "raw-body@0.0.3", 87 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-0.0.3.tgz" 88 | }, 89 | "negotiator": { 90 | "version": "0.3.0", 91 | "from": "negotiator@0.3.0", 92 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.3.0.tgz" 93 | }, 94 | "multiparty": { 95 | "version": "2.2.0", 96 | "from": "multiparty@2.2.0", 97 | "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-2.2.0.tgz", 98 | "dependencies": { 99 | "readable-stream": { 100 | "version": "1.1.14", 101 | "from": "readable-stream@>=1.1.9 <1.2.0", 102 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 103 | "dependencies": { 104 | "core-util-is": { 105 | "version": "1.0.2", 106 | "from": "core-util-is@>=1.0.0 <1.1.0", 107 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" 108 | }, 109 | "isarray": { 110 | "version": "0.0.1", 111 | "from": "isarray@0.0.1", 112 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" 113 | }, 114 | "string_decoder": { 115 | "version": "0.10.31", 116 | "from": "string_decoder@>=0.10.0 <0.11.0", 117 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" 118 | }, 119 | "inherits": { 120 | "version": "2.0.4", 121 | "from": "inherits@>=2.0.1 <2.1.0", 122 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" 123 | } 124 | } 125 | }, 126 | "stream-counter": { 127 | "version": "0.2.0", 128 | "from": "stream-counter@>=0.2.0 <0.3.0", 129 | "resolved": "https://registry.npmjs.org/stream-counter/-/stream-counter-0.2.0.tgz" 130 | } 131 | } 132 | } 133 | } 134 | }, 135 | "connect-mongo": { 136 | "version": "0.3.3", 137 | "from": "connect-mongo@>=0.3.3 <0.4.0", 138 | "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-0.3.3.tgz", 139 | "dependencies": { 140 | "mongodb": { 141 | "version": "1.2.14", 142 | "from": "mongodb@>=1.2.0 <1.3.0", 143 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-1.2.14.tgz", 144 | "dependencies": { 145 | "bson": { 146 | "version": "0.1.8", 147 | "from": "bson@0.1.8", 148 | "resolved": "https://registry.npmjs.org/bson/-/bson-0.1.8.tgz" 149 | } 150 | } 151 | } 152 | } 153 | }, 154 | "crypto-js": { 155 | "version": "3.1.8", 156 | "from": "crypto-js@>=3.1.2 <3.2.0", 157 | "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.8.tgz" 158 | }, 159 | "dotenv": { 160 | "version": "2.0.0", 161 | "from": "dotenv@>=2.0.0 <3.0.0", 162 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-2.0.0.tgz" 163 | }, 164 | "express": { 165 | "version": "3.4.8", 166 | "from": "express@>=3.4.4 <3.5.0", 167 | "resolved": "https://registry.npmjs.org/express/-/express-3.4.8.tgz", 168 | "dependencies": { 169 | "connect": { 170 | "version": "2.12.0", 171 | "from": "connect@2.12.0", 172 | "resolved": "https://registry.npmjs.org/connect/-/connect-2.12.0.tgz", 173 | "dependencies": { 174 | "batch": { 175 | "version": "0.5.0", 176 | "from": "batch@0.5.0", 177 | "resolved": "https://registry.npmjs.org/batch/-/batch-0.5.0.tgz" 178 | }, 179 | "qs": { 180 | "version": "0.6.6", 181 | "from": "qs@0.6.6", 182 | "resolved": "https://registry.npmjs.org/qs/-/qs-0.6.6.tgz" 183 | }, 184 | "bytes": { 185 | "version": "0.2.1", 186 | "from": "bytes@0.2.1", 187 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-0.2.1.tgz" 188 | }, 189 | "pause": { 190 | "version": "0.0.1", 191 | "from": "pause@0.0.1", 192 | "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz" 193 | }, 194 | "uid2": { 195 | "version": "0.0.3", 196 | "from": "uid2@0.0.3", 197 | "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz" 198 | }, 199 | "raw-body": { 200 | "version": "1.1.2", 201 | "from": "raw-body@1.1.2", 202 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.2.tgz" 203 | }, 204 | "negotiator": { 205 | "version": "0.3.0", 206 | "from": "negotiator@0.3.0", 207 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.3.0.tgz" 208 | }, 209 | "multiparty": { 210 | "version": "2.2.0", 211 | "from": "multiparty@2.2.0", 212 | "resolved": "https://registry.npmjs.org/multiparty/-/multiparty-2.2.0.tgz", 213 | "dependencies": { 214 | "readable-stream": { 215 | "version": "1.1.14", 216 | "from": "readable-stream@>=1.1.9 <1.2.0", 217 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", 218 | "dependencies": { 219 | "core-util-is": { 220 | "version": "1.0.2", 221 | "from": "core-util-is@>=1.0.0 <1.1.0", 222 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" 223 | }, 224 | "isarray": { 225 | "version": "0.0.1", 226 | "from": "isarray@0.0.1", 227 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" 228 | }, 229 | "string_decoder": { 230 | "version": "0.10.31", 231 | "from": "string_decoder@>=0.10.0 <0.11.0", 232 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" 233 | }, 234 | "inherits": { 235 | "version": "2.0.4", 236 | "from": "inherits@>=2.0.1 <2.1.0", 237 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" 238 | } 239 | } 240 | }, 241 | "stream-counter": { 242 | "version": "0.2.0", 243 | "from": "stream-counter@>=0.2.0 <0.3.0", 244 | "resolved": "https://registry.npmjs.org/stream-counter/-/stream-counter-0.2.0.tgz" 245 | } 246 | } 247 | } 248 | } 249 | }, 250 | "commander": { 251 | "version": "1.3.2", 252 | "from": "commander@1.3.2", 253 | "resolved": "https://registry.npmjs.org/commander/-/commander-1.3.2.tgz", 254 | "dependencies": { 255 | "keypress": { 256 | "version": "0.1.0", 257 | "from": "keypress@>=0.1.0 <0.2.0", 258 | "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.1.0.tgz" 259 | } 260 | } 261 | }, 262 | "range-parser": { 263 | "version": "0.0.4", 264 | "from": "range-parser@0.0.4", 265 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-0.0.4.tgz" 266 | }, 267 | "mkdirp": { 268 | "version": "0.3.5", 269 | "from": "mkdirp@0.3.5", 270 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" 271 | }, 272 | "cookie": { 273 | "version": "0.1.0", 274 | "from": "cookie@0.1.0", 275 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.0.tgz" 276 | }, 277 | "buffer-crc32": { 278 | "version": "0.2.1", 279 | "from": "buffer-crc32@0.2.1", 280 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.1.tgz" 281 | }, 282 | "fresh": { 283 | "version": "0.2.0", 284 | "from": "fresh@0.2.0", 285 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.0.tgz" 286 | }, 287 | "methods": { 288 | "version": "0.1.0", 289 | "from": "methods@0.1.0", 290 | "resolved": "https://registry.npmjs.org/methods/-/methods-0.1.0.tgz" 291 | }, 292 | "send": { 293 | "version": "0.1.4", 294 | "from": "send@0.1.4", 295 | "resolved": "https://registry.npmjs.org/send/-/send-0.1.4.tgz", 296 | "dependencies": { 297 | "mime": { 298 | "version": "1.2.11", 299 | "from": "mime@>=1.2.9 <1.3.0", 300 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz" 301 | } 302 | } 303 | }, 304 | "cookie-signature": { 305 | "version": "1.0.1", 306 | "from": "cookie-signature@1.0.1", 307 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.1.tgz" 308 | }, 309 | "merge-descriptors": { 310 | "version": "0.0.1", 311 | "from": "merge-descriptors@0.0.1", 312 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.1.tgz" 313 | }, 314 | "debug": { 315 | "version": "0.8.1", 316 | "from": "debug@>=0.7.3 <1.0.0", 317 | "resolved": "https://registry.npmjs.org/debug/-/debug-0.8.1.tgz" 318 | } 319 | } 320 | }, 321 | "mocha": { 322 | "version": "1.13.0", 323 | "from": "mocha@>=1.13.0 <1.14.0", 324 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-1.13.0.tgz", 325 | "dependencies": { 326 | "commander": { 327 | "version": "0.6.1", 328 | "from": "commander@0.6.1", 329 | "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz" 330 | }, 331 | "growl": { 332 | "version": "1.7.0", 333 | "from": "growl@>=1.7.0 <1.8.0", 334 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.7.0.tgz" 335 | }, 336 | "jade": { 337 | "version": "0.26.3", 338 | "from": "jade@0.26.3", 339 | "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", 340 | "dependencies": { 341 | "mkdirp": { 342 | "version": "0.3.0", 343 | "from": "mkdirp@0.3.0", 344 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" 345 | } 346 | } 347 | }, 348 | "diff": { 349 | "version": "1.0.7", 350 | "from": "diff@1.0.7", 351 | "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz" 352 | }, 353 | "debug": { 354 | "version": "4.3.1", 355 | "from": "debug@*", 356 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 357 | "dependencies": { 358 | "ms": { 359 | "version": "2.1.2", 360 | "from": "ms@2.1.2", 361 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" 362 | } 363 | } 364 | }, 365 | "mkdirp": { 366 | "version": "0.3.5", 367 | "from": "mkdirp@0.3.5", 368 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz" 369 | }, 370 | "glob": { 371 | "version": "3.2.3", 372 | "from": "glob@3.2.3", 373 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz", 374 | "dependencies": { 375 | "minimatch": { 376 | "version": "0.2.14", 377 | "from": "minimatch@>=0.2.11 <0.3.0", 378 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", 379 | "dependencies": { 380 | "lru-cache": { 381 | "version": "2.7.3", 382 | "from": "lru-cache@>=2.0.0 <3.0.0", 383 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" 384 | }, 385 | "sigmund": { 386 | "version": "1.0.1", 387 | "from": "sigmund@>=1.0.0 <1.1.0", 388 | "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" 389 | } 390 | } 391 | }, 392 | "graceful-fs": { 393 | "version": "2.0.3", 394 | "from": "graceful-fs@>=2.0.0 <2.1.0", 395 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz" 396 | }, 397 | "inherits": { 398 | "version": "2.0.4", 399 | "from": "inherits@>=2.0.0 <3.0.0", 400 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" 401 | } 402 | } 403 | } 404 | } 405 | }, 406 | "mongodb": { 407 | "version": "1.3.23", 408 | "from": "mongodb@>=1.3.19 <1.4.0", 409 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-1.3.23.tgz", 410 | "dependencies": { 411 | "bson": { 412 | "version": "0.2.5", 413 | "from": "bson@0.2.5", 414 | "resolved": "https://registry.npmjs.org/bson/-/bson-0.2.5.tgz" 415 | }, 416 | "kerberos": { 417 | "version": "0.0.3", 418 | "from": "kerberos@0.0.3", 419 | "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.3.tgz" 420 | } 421 | } 422 | }, 423 | "moniker": { 424 | "version": "0.1.2", 425 | "from": "moniker@>=0.1.2 <0.2.0", 426 | "resolved": "https://registry.npmjs.org/moniker/-/moniker-0.1.2.tgz" 427 | }, 428 | "optimist": { 429 | "version": "0.6.1", 430 | "from": "optimist@>=0.6.0 <0.7.0", 431 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", 432 | "dependencies": { 433 | "wordwrap": { 434 | "version": "0.0.3", 435 | "from": "wordwrap@>=0.0.2 <0.1.0", 436 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" 437 | }, 438 | "minimist": { 439 | "version": "0.0.10", 440 | "from": "minimist@>=0.0.1 <0.1.0", 441 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" 442 | } 443 | } 444 | }, 445 | "q": { 446 | "version": "0.9.7", 447 | "from": "q@>=0.9.7 <0.10.0", 448 | "resolved": "https://registry.npmjs.org/q/-/q-0.9.7.tgz" 449 | }, 450 | "socket.io": { 451 | "version": "0.9.19", 452 | "from": "socket.io@>=0.9.16 <0.10.0", 453 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-0.9.19.tgz", 454 | "dependencies": { 455 | "socket.io-client": { 456 | "version": "0.9.16", 457 | "from": "socket.io-client@0.9.16", 458 | "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-0.9.16.tgz", 459 | "dependencies": { 460 | "uglify-js": { 461 | "version": "1.2.5", 462 | "from": "uglify-js@1.2.5", 463 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-1.2.5.tgz" 464 | }, 465 | "ws": { 466 | "version": "0.4.32", 467 | "from": "ws@>=0.4.0 <0.5.0", 468 | "resolved": "https://registry.npmjs.org/ws/-/ws-0.4.32.tgz", 469 | "dependencies": { 470 | "commander": { 471 | "version": "2.1.0", 472 | "from": "commander@>=2.1.0 <2.2.0", 473 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz" 474 | }, 475 | "nan": { 476 | "version": "1.0.0", 477 | "from": "nan@>=1.0.0 <1.1.0", 478 | "resolved": "https://registry.npmjs.org/nan/-/nan-1.0.0.tgz" 479 | }, 480 | "tinycolor": { 481 | "version": "0.0.1", 482 | "from": "tinycolor@>=0.0.0 <1.0.0", 483 | "resolved": "https://registry.npmjs.org/tinycolor/-/tinycolor-0.0.1.tgz" 484 | }, 485 | "options": { 486 | "version": "0.0.6", 487 | "from": "options@>=0.0.5", 488 | "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz" 489 | } 490 | } 491 | }, 492 | "xmlhttprequest": { 493 | "version": "1.4.2", 494 | "from": "xmlhttprequest@1.4.2", 495 | "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.4.2.tgz" 496 | }, 497 | "active-x-obfuscator": { 498 | "version": "0.0.1", 499 | "from": "active-x-obfuscator@0.0.1", 500 | "resolved": "https://registry.npmjs.org/active-x-obfuscator/-/active-x-obfuscator-0.0.1.tgz", 501 | "dependencies": { 502 | "zeparser": { 503 | "version": "0.0.5", 504 | "from": "zeparser@0.0.5", 505 | "resolved": "https://registry.npmjs.org/zeparser/-/zeparser-0.0.5.tgz" 506 | } 507 | } 508 | } 509 | } 510 | }, 511 | "policyfile": { 512 | "version": "0.0.4", 513 | "from": "policyfile@0.0.4", 514 | "resolved": "https://registry.npmjs.org/policyfile/-/policyfile-0.0.4.tgz" 515 | }, 516 | "base64id": { 517 | "version": "0.1.0", 518 | "from": "base64id@0.1.0", 519 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-0.1.0.tgz" 520 | }, 521 | "redis": { 522 | "version": "0.7.3", 523 | "from": "redis@0.7.3", 524 | "resolved": "https://registry.npmjs.org/redis/-/redis-0.7.3.tgz" 525 | } 526 | } 527 | }, 528 | "swig": { 529 | "version": "1.1.0", 530 | "from": "swig@>=1.1.0 <1.2.0", 531 | "resolved": "https://registry.npmjs.org/swig/-/swig-1.1.0.tgz", 532 | "dependencies": { 533 | "uglify-js": { 534 | "version": "2.4.0", 535 | "from": "uglify-js@2.4.0", 536 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.0.tgz", 537 | "dependencies": { 538 | "async": { 539 | "version": "0.2.10", 540 | "from": "async@>=0.2.6 <0.3.0", 541 | "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz" 542 | }, 543 | "source-map": { 544 | "version": "0.1.43", 545 | "from": "source-map@>=0.1.7 <0.2.0", 546 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", 547 | "dependencies": { 548 | "amdefine": { 549 | "version": "1.0.1", 550 | "from": "amdefine@>=0.0.4", 551 | "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz" 552 | } 553 | } 554 | }, 555 | "optimist": { 556 | "version": "0.3.7", 557 | "from": "optimist@>=0.3.5 <0.4.0", 558 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", 559 | "dependencies": { 560 | "wordwrap": { 561 | "version": "0.0.3", 562 | "from": "wordwrap@>=0.0.2 <0.1.0", 563 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" 564 | } 565 | } 566 | }, 567 | "uglify-to-browserify": { 568 | "version": "1.0.2", 569 | "from": "uglify-to-browserify@>=1.0.0 <1.1.0", 570 | "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz" 571 | } 572 | } 573 | }, 574 | "optimist": { 575 | "version": "0.6.0", 576 | "from": "optimist@0.6.0", 577 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.0.tgz", 578 | "dependencies": { 579 | "wordwrap": { 580 | "version": "0.0.3", 581 | "from": "wordwrap@>=0.0.2 <0.1.0", 582 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" 583 | }, 584 | "minimist": { 585 | "version": "0.0.10", 586 | "from": "minimist@>=0.0.1 <0.1.0", 587 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" 588 | } 589 | } 590 | } 591 | } 592 | }, 593 | "underscore": { 594 | "version": "1.5.2", 595 | "from": "underscore@>=1.5.2 <1.6.0", 596 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.5.2.tgz" 597 | } 598 | } 599 | } 600 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mahjong", 3 | "version": "0.0.1", 4 | "description": "Application for simulating, analyzing, and playing a game of three-player mahjong", 5 | "main": "app.js", 6 | "dependencies": { 7 | "connect": "2.11.0", 8 | "connect-mongo": "^0.4.0", 9 | "crypto-js": "~3.1.2", 10 | "dotenv": "^2.0.0", 11 | "express": "~3.4.4", 12 | "mocha": "~1.13.0", 13 | "mongodb": "~1.3.19", 14 | "moniker": "~0.1.2", 15 | "optimist": "~0.6.0", 16 | "q": "~0.9.7", 17 | "socket.io": "~0.9.16", 18 | "swig": "~1.1.0", 19 | "underscore": "~1.5.2" 20 | }, 21 | "devDependencies": { 22 | "grunt": "~0.4.1", 23 | "grunt-cli": "^1.3.2", 24 | "grunt-concurrent": "~0.4.1", 25 | "grunt-contrib-concat": "~0.3.0", 26 | "grunt-contrib-cssmin": "~0.6.2", 27 | "grunt-contrib-jshint": "~0.6.3", 28 | "grunt-contrib-less": "~0.8.2", 29 | "grunt-contrib-uglify": "~0.2.2", 30 | "grunt-contrib-watch": "~0.5.3", 31 | "grunt-mocha-test": "~0.7.0", 32 | "grunt-nodemon": "~0.1.1" 33 | }, 34 | "scripts": { 35 | "test": "mocha", 36 | "grunt": "grunt" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/gleitz/mahjong.git" 41 | }, 42 | "keywords": [ 43 | "mahjong", 44 | "tiles", 45 | "mahjong ai" 46 | ], 47 | "author": "Benjamin Gleitzman", 48 | "license": "MIT", 49 | "bugs": { 50 | "url": "https://github.com/gleitz/mahjong/issues" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/css/dist/all.css: -------------------------------------------------------------------------------- 1 | /* 2 | * qTip2 - Pretty powerful tooltips - v2.2.0 3 | * http://qtip2.com 4 | * 5 | * Copyright (c) 2013 Craig Michael Thompson 6 | * Released under the MIT, GPL licenses 7 | * http://jquery.org/license 8 | * 9 | * Date: Sun Dec 15 2013 11:29 EST-0500 10 | * Plugins: None 11 | * Styles: basic css3 12 | */ 13 | .qtip { 14 | position: absolute; 15 | left: -28000px; 16 | top: -28000px; 17 | display: none; 18 | max-width: 280px; 19 | min-width: 50px; 20 | font-size: 10.5px; 21 | line-height: 12px; 22 | direction: ltr; 23 | box-shadow: none; 24 | padding: 0; 25 | } 26 | .qtip-content { 27 | position: relative; 28 | padding: 5px 9px; 29 | overflow: hidden; 30 | text-align: left; 31 | word-wrap: break-word; 32 | } 33 | .qtip-titlebar { 34 | position: relative; 35 | padding: 5px 35px 5px 10px; 36 | overflow: hidden; 37 | border-width: 0 0 1px; 38 | font-weight: bold; 39 | } 40 | .qtip-titlebar + .qtip-content { 41 | border-top-width: 0 !important; 42 | } 43 | /* Default close button class */ 44 | .qtip-close { 45 | position: absolute; 46 | right: -9px; 47 | top: -9px; 48 | cursor: pointer; 49 | outline: medium none; 50 | border-width: 1px; 51 | border-style: solid; 52 | border-color: transparent; 53 | } 54 | .qtip-titlebar .qtip-close { 55 | right: 4px; 56 | top: 50%; 57 | margin-top: -9px; 58 | } 59 | * html .qtip-titlebar .qtip-close { 60 | top: 16px; 61 | } 62 | /* IE fix */ 63 | .qtip-titlebar .ui-icon, 64 | .qtip-icon .ui-icon { 65 | display: block; 66 | text-indent: -1000em; 67 | direction: ltr; 68 | } 69 | .qtip-icon, 70 | .qtip-icon .ui-icon { 71 | -moz-border-radius: 3px; 72 | -webkit-border-radius: 3px; 73 | border-radius: 3px; 74 | text-decoration: none; 75 | } 76 | .qtip-icon .ui-icon { 77 | width: 18px; 78 | height: 14px; 79 | line-height: 14px; 80 | text-align: center; 81 | text-indent: 0; 82 | font: normal bold 10px/13px Tahoma, sans-serif; 83 | color: inherit; 84 | background: transparent none no-repeat -100em -100em; 85 | } 86 | /* Applied to 'focused' tooltips e.g. most recently displayed/interacted with */ 87 | /* Applied on hover of tooltips i.e. added/removed on mouseenter/mouseleave respectively */ 88 | /* Default tooltip style */ 89 | .qtip-default { 90 | border-width: 1px; 91 | border-style: solid; 92 | border-color: #F1D031; 93 | background-color: #FFFFA3; 94 | color: #555; 95 | } 96 | .qtip-default .qtip-titlebar { 97 | background-color: #FFEF93; 98 | } 99 | .qtip-default .qtip-icon { 100 | border-color: #CCC; 101 | background: #F1F1F1; 102 | color: #777; 103 | } 104 | .qtip-default .qtip-titlebar .qtip-close { 105 | border-color: #AAA; 106 | color: #111; 107 | } 108 | /*! Light tooltip style */ 109 | .qtip-light { 110 | background-color: white; 111 | border-color: #E2E2E2; 112 | color: #454545; 113 | } 114 | .qtip-light .qtip-titlebar { 115 | background-color: #f1f1f1; 116 | } 117 | /*! Dark tooltip style */ 118 | .qtip-dark { 119 | background-color: #505050; 120 | border-color: #303030; 121 | color: #f3f3f3; 122 | } 123 | .qtip-dark .qtip-titlebar { 124 | background-color: #404040; 125 | } 126 | .qtip-dark .qtip-icon { 127 | border-color: #444; 128 | } 129 | .qtip-dark .qtip-titlebar .ui-state-hover { 130 | border-color: #303030; 131 | } 132 | /*! Cream tooltip style */ 133 | .qtip-cream { 134 | background-color: #FBF7AA; 135 | border-color: #F9E98E; 136 | color: #A27D35; 137 | } 138 | .qtip-cream .qtip-titlebar { 139 | background-color: #F0DE7D; 140 | } 141 | .qtip-cream .qtip-close .qtip-icon { 142 | background-position: -82px 0; 143 | } 144 | /*! Red tooltip style */ 145 | .qtip-red { 146 | background-color: #F78B83; 147 | border-color: #D95252; 148 | color: #912323; 149 | } 150 | .qtip-red .qtip-titlebar { 151 | background-color: #F06D65; 152 | } 153 | .qtip-red .qtip-close .qtip-icon { 154 | background-position: -102px 0; 155 | } 156 | .qtip-red .qtip-icon { 157 | border-color: #D95252; 158 | } 159 | .qtip-red .qtip-titlebar .ui-state-hover { 160 | border-color: #D95252; 161 | } 162 | /*! Green tooltip style */ 163 | .qtip-green { 164 | background-color: #CAED9E; 165 | border-color: #90D93F; 166 | color: #3F6219; 167 | } 168 | .qtip-green .qtip-titlebar { 169 | background-color: #B0DE78; 170 | } 171 | .qtip-green .qtip-close .qtip-icon { 172 | background-position: -42px 0; 173 | } 174 | /*! Blue tooltip style */ 175 | .qtip-blue { 176 | background-color: #E5F6FE; 177 | border-color: #ADD9ED; 178 | color: #5E99BD; 179 | } 180 | .qtip-blue .qtip-titlebar { 181 | background-color: #D0E9F5; 182 | } 183 | .qtip-blue .qtip-close .qtip-icon { 184 | background-position: -2px 0; 185 | } 186 | .qtip-shadow { 187 | -webkit-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); 188 | -moz-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); 189 | box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); 190 | } 191 | /* Add rounded corners to your tooltips in: FF3+, Chrome 2+, Opera 10.6+, IE9+, Safari 2+ */ 192 | .qtip-rounded, 193 | .qtip-tipsy, 194 | .qtip-bootstrap { 195 | -moz-border-radius: 5px; 196 | -webkit-border-radius: 5px; 197 | border-radius: 5px; 198 | } 199 | .qtip-rounded .qtip-titlebar { 200 | -moz-border-radius: 4px 4px 0 0; 201 | -webkit-border-radius: 4px 4px 0 0; 202 | border-radius: 4px 4px 0 0; 203 | } 204 | /* Youtube tooltip style */ 205 | .qtip-youtube { 206 | -moz-border-radius: 2px; 207 | -webkit-border-radius: 2px; 208 | border-radius: 2px; 209 | -webkit-box-shadow: 0 0 3px #333; 210 | -moz-box-shadow: 0 0 3px #333; 211 | box-shadow: 0 0 3px #333; 212 | color: white; 213 | border-width: 0; 214 | background: #4A4A4A; 215 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #4a4a4a), color-stop(100%, #000000)); 216 | background-image: -webkit-linear-gradient(top, #4a4a4a 0, #000000 100%); 217 | background-image: -moz-linear-gradient(top, #4a4a4a 0, #000000 100%); 218 | background-image: -ms-linear-gradient(top, #4a4a4a 0, #000000 100%); 219 | background-image: -o-linear-gradient(top, #4a4a4a 0, #000000 100%); 220 | } 221 | .qtip-youtube .qtip-titlebar { 222 | background-color: #4A4A4A; 223 | background-color: rgba(0, 0, 0, 0); 224 | } 225 | .qtip-youtube .qtip-content { 226 | padding: .75em; 227 | font: 12px arial, sans-serif; 228 | filter: progid:DXImageTransform.Microsoft.Gradient(GradientType=0, StartColorStr=#4a4a4a, EndColorStr=#000000); 229 | -ms-filter: "progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#4a4a4a,EndColorStr=#000000);"; 230 | } 231 | .qtip-youtube .qtip-icon { 232 | border-color: #222; 233 | } 234 | .qtip-youtube .qtip-titlebar .ui-state-hover { 235 | border-color: #303030; 236 | } 237 | /* jQuery TOOLS Tooltip style */ 238 | .qtip-jtools { 239 | background: #232323; 240 | background: rgba(0, 0, 0, 0.7); 241 | background-image: -webkit-gradient(linear, left top, left bottom, from(#717171), to(#232323)); 242 | background-image: -moz-linear-gradient(top, #717171, #232323); 243 | background-image: -webkit-linear-gradient(top, #717171, #232323); 244 | background-image: -ms-linear-gradient(top, #717171, #232323); 245 | background-image: -o-linear-gradient(top, #717171, #232323); 246 | border: 2px solid #ddd; 247 | border: 2px solid #f1f1f1; 248 | -moz-border-radius: 2px; 249 | -webkit-border-radius: 2px; 250 | border-radius: 2px; 251 | -webkit-box-shadow: 0 0 12px #333; 252 | -moz-box-shadow: 0 0 12px #333; 253 | box-shadow: 0 0 12px #333; 254 | } 255 | /* IE Specific */ 256 | .qtip-jtools .qtip-titlebar { 257 | background-color: transparent; 258 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171, endColorstr=#4a4a4a); 259 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A)"; 260 | } 261 | .qtip-jtools .qtip-content { 262 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#4a4a4a, endColorstr=#232323); 263 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323)"; 264 | } 265 | .qtip-jtools .qtip-titlebar, 266 | .qtip-jtools .qtip-content { 267 | background: transparent; 268 | color: white; 269 | border: 0 dashed transparent; 270 | } 271 | .qtip-jtools .qtip-icon { 272 | border-color: #555; 273 | } 274 | .qtip-jtools .qtip-titlebar .ui-state-hover { 275 | border-color: #333; 276 | } 277 | /* Cluetip style */ 278 | .qtip-cluetip { 279 | -webkit-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); 280 | -moz-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); 281 | box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); 282 | background-color: #D9D9C2; 283 | color: #111; 284 | border: 0 dashed transparent; 285 | } 286 | .qtip-cluetip .qtip-titlebar { 287 | background-color: #87876A; 288 | color: white; 289 | border: 0 dashed transparent; 290 | } 291 | .qtip-cluetip .qtip-icon { 292 | border-color: #808064; 293 | } 294 | .qtip-cluetip .qtip-titlebar .ui-state-hover { 295 | border-color: #696952; 296 | color: #696952; 297 | } 298 | /* Tipsy style */ 299 | .qtip-tipsy { 300 | background: black; 301 | background: rgba(0, 0, 0, 0.87); 302 | color: white; 303 | border: 0 solid transparent; 304 | font-size: 11px; 305 | font-family: 'Lucida Grande', sans-serif; 306 | font-weight: bold; 307 | line-height: 16px; 308 | text-shadow: 0 1px black; 309 | } 310 | .qtip-tipsy .qtip-titlebar { 311 | padding: 6px 35px 0 10px; 312 | background-color: transparent; 313 | } 314 | .qtip-tipsy .qtip-content { 315 | padding: 6px 10px; 316 | } 317 | .qtip-tipsy .qtip-icon { 318 | border-color: #222; 319 | text-shadow: none; 320 | } 321 | .qtip-tipsy .qtip-titlebar .ui-state-hover { 322 | border-color: #303030; 323 | } 324 | /* Tipped style */ 325 | .qtip-tipped { 326 | border: 3px solid #959FA9; 327 | -moz-border-radius: 3px; 328 | -webkit-border-radius: 3px; 329 | border-radius: 3px; 330 | background-color: #F9F9F9; 331 | color: #454545; 332 | font-weight: normal; 333 | font-family: serif; 334 | } 335 | .qtip-tipped .qtip-titlebar { 336 | border-bottom-width: 0; 337 | color: white; 338 | background: #3A79B8; 339 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3a79b8), to(#2e629d)); 340 | background-image: -webkit-linear-gradient(top, #3a79b8, #2e629d); 341 | background-image: -moz-linear-gradient(top, #3a79b8, #2e629d); 342 | background-image: -ms-linear-gradient(top, #3a79b8, #2e629d); 343 | background-image: -o-linear-gradient(top, #3a79b8, #2e629d); 344 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#3a79b8, endColorstr=#2e629d); 345 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)"; 346 | } 347 | .qtip-tipped .qtip-icon { 348 | border: 2px solid #285589; 349 | background: #285589; 350 | } 351 | .qtip-tipped .qtip-icon .ui-icon { 352 | background-color: #FBFBFB; 353 | color: #555; 354 | } 355 | /** 356 | * Twitter Bootstrap style. 357 | * 358 | * Tested with IE 8, IE 9, Chrome 18, Firefox 9, Opera 11. 359 | * Does not work with IE 7. 360 | */ 361 | .qtip-bootstrap { 362 | /** Taken from Bootstrap body */ 363 | font-size: 14px; 364 | line-height: 20px; 365 | color: #333333; 366 | /** Taken from Bootstrap .popover */ 367 | padding: 1px; 368 | background-color: #ffffff; 369 | border: 1px solid #ccc; 370 | border: 1px solid rgba(0, 0, 0, 0.2); 371 | -webkit-border-radius: 6px; 372 | -moz-border-radius: 6px; 373 | border-radius: 6px; 374 | -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); 375 | -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); 376 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); 377 | -webkit-background-clip: padding-box; 378 | -moz-background-clip: padding; 379 | background-clip: padding-box; 380 | } 381 | .qtip-bootstrap .qtip-titlebar { 382 | /** Taken from Bootstrap .popover-title */ 383 | padding: 8px 14px; 384 | margin: 0; 385 | font-size: 14px; 386 | font-weight: normal; 387 | line-height: 18px; 388 | background-color: #f7f7f7; 389 | border-bottom: 1px solid #ebebeb; 390 | -webkit-border-radius: 5px 5px 0 0; 391 | -moz-border-radius: 5px 5px 0 0; 392 | border-radius: 5px 5px 0 0; 393 | } 394 | .qtip-bootstrap .qtip-titlebar .qtip-close { 395 | /** 396 | * Overrides qTip2: 397 | * .qtip-titlebar .qtip-close{ 398 | * [...] 399 | * right: 4px; 400 | * top: 50%; 401 | * [...] 402 | * border-style: solid; 403 | * } 404 | */ 405 | right: 11px; 406 | top: 45%; 407 | border-style: none; 408 | } 409 | .qtip-bootstrap .qtip-content { 410 | /** Taken from Bootstrap .popover-content */ 411 | padding: 9px 14px; 412 | } 413 | .qtip-bootstrap .qtip-icon { 414 | /** 415 | * Overrides qTip2: 416 | * .qtip-default .qtip-icon { 417 | * border-color: #CCC; 418 | * background: #F1F1F1; 419 | * color: #777; 420 | * } 421 | */ 422 | background: transparent; 423 | } 424 | .qtip-bootstrap .qtip-icon .ui-icon { 425 | /** 426 | * Overrides qTip2: 427 | * .qtip-icon .ui-icon{ 428 | * width: 18px; 429 | * height: 14px; 430 | * } 431 | */ 432 | width: auto; 433 | height: auto; 434 | /* Taken from Bootstrap .close */ 435 | float: right; 436 | font-size: 20px; 437 | font-weight: bold; 438 | line-height: 18px; 439 | color: #000000; 440 | text-shadow: 0 1px 0 #ffffff; 441 | opacity: 0.2; 442 | filter: alpha(opacity=20); 443 | } 444 | .qtip-bootstrap .qtip-icon .ui-icon:hover { 445 | /* Taken from Bootstrap .close:hover */ 446 | color: #000000; 447 | text-decoration: none; 448 | cursor: pointer; 449 | opacity: 0.4; 450 | filter: alpha(opacity=40); 451 | } 452 | /* IE9 fix - removes all filters */ 453 | .qtip:not(.ie9haxors) div.qtip-content, 454 | .qtip:not(.ie9haxors) div.qtip-titlebar { 455 | filter: none; 456 | -ms-filter: none; 457 | } 458 | 459 | /* 460 | YUI Recent CSS copyright Yahoo! Inc. All rights reserved. 461 | Code licensed under the BSD License: 462 | http://developer.yahoo.net/yui/license.txt 463 | version: 2.5.2 464 | */ 465 | html { 466 | color: #000; 467 | width: 100%; 468 | font-family: "Lucida Grande", 'Helvetica Neue', 'Helvetica', 'Arial'; 469 | } 470 | body, 471 | div, 472 | dl, 473 | dt, 474 | dd, 475 | ul, 476 | ol, 477 | li, 478 | h1, 479 | h2, 480 | h3, 481 | h4, 482 | h5, 483 | h6, 484 | pre, 485 | code, 486 | form, 487 | fieldset, 488 | legend, 489 | input, 490 | textarea, 491 | p, 492 | blockquote, 493 | th, 494 | td { 495 | margin: 0; 496 | padding: 0; 497 | } 498 | table { 499 | border-collapse: collapse; 500 | border-spacing: 0; 501 | } 502 | fieldset, 503 | img { 504 | border: 0; 505 | } 506 | address, 507 | caption, 508 | cite, 509 | code, 510 | dfn, 511 | em, 512 | strong, 513 | th, 514 | var { 515 | font-style: normal; 516 | font-weight: normal; 517 | } 518 | li { 519 | list-style: none; 520 | } 521 | caption, 522 | th { 523 | text-align: left; 524 | } 525 | h1, 526 | h2, 527 | h3, 528 | h4, 529 | h5, 530 | h6 { 531 | font-size: 100%; 532 | font-weight: 300; 533 | font-family: "Lucida Grande", 'Helvetica Neue', 'Helvetica', 'Arial'; 534 | } 535 | q:before, 536 | q:after { 537 | content: ''; 538 | } 539 | abbr, 540 | acronym { 541 | border: 0; 542 | font-variant: normal; 543 | } 544 | /* to preserve line-height and selector appearance */ 545 | sup { 546 | vertical-align: text-top; 547 | } 548 | sub { 549 | vertical-align: text-bottom; 550 | } 551 | input, 552 | textarea, 553 | select { 554 | font-family: inherit; 555 | font-size: inherit; 556 | font-weight: inherit; 557 | } 558 | /*to enable resizing for IE*/ 559 | input, 560 | textarea, 561 | select { 562 | *font-size: 100%; 563 | } 564 | /*because legend doesn't inherit in IE */ 565 | legend { 566 | color: #000; 567 | } 568 | b, 569 | u, 570 | s, 571 | i { 572 | font-style: normal; 573 | font-weight: normal; 574 | text-decoration: none; 575 | } 576 | 577 | h1, 578 | h2 { 579 | font-size: 30px; 580 | line-height: 40px; 581 | } 582 | h3 { 583 | font-size: 18px; 584 | } 585 | body { 586 | padding: 50px; 587 | background-color: #275001; 588 | color: #FFFBE9; 589 | } 590 | body.mobile { 591 | padding: 30px 2px; 592 | } 593 | a { 594 | color: #00B7FF; 595 | } 596 | a.tile-holder { 597 | color: #00B7FF; 598 | background: #FFFBE9; 599 | padding: 4px; 600 | border-radius: 8px; 601 | margin: 4px; 602 | -moz-box-shadow: 4px 4px 0px #000; 603 | -webkit-box-shadow: 4px 4px 0px #000; 604 | box-shadow: 4px 4px 0px #000; 605 | width: 55px; 606 | height: 75px; 607 | } 608 | .mobile a.tile-holder { 609 | margin: 1px; 610 | } 611 | .other-player a.tile-holder { 612 | width: 30.555555555555557px; 613 | height: 41.666666666666664px; 614 | padding: 3px; 615 | margin: 2px; 616 | } 617 | .mobile .other-player a.tile-holder { 618 | width: 19.18518px; 619 | height: 25.9259px; 620 | padding: 3px; 621 | margin: 1px; 622 | } 623 | .mobile .other-player .hand-tiles { 624 | display: none; 625 | } 626 | .tile-row { 627 | font-size: 71px; 628 | } 629 | .other-player .tile-row { 630 | font-size: 35.5px; 631 | } 632 | .mobile a.tile-holder { 633 | width: 24.66666px; 634 | height: 33.3333px; 635 | } 636 | h3 a.tile-holder { 637 | width: 13.2px; 638 | height: 18px; 639 | padding: 3px; 640 | margin: 0px; 641 | border-radius: 4px; 642 | } 643 | .mobile .tile-row { 644 | font-size: 53px; 645 | } 646 | a.selected { 647 | border: 4px solid #FF0000; 648 | margin-top: 2px; 649 | margin-left: 2px; 650 | padding: 1px; 651 | } 652 | .mobile a.selected { 653 | margin: 2px 2px; 654 | padding: 2px; 655 | } 656 | .left { 657 | float: left; 658 | } 659 | .right { 660 | float: right; 661 | } 662 | .clr { 663 | overflow: hidden; 664 | zoom: 1; 665 | } 666 | .small { 667 | font-size: 12px; 668 | } 669 | .dim { 670 | color: #999999; 671 | } 672 | .br-top { 673 | padding-top: 8px; 674 | } 675 | .br { 676 | padding-bottom: 8px; 677 | } 678 | .hide { 679 | display: none; 680 | } 681 | .tile { 682 | width: 55px; 683 | height: 75px; 684 | background-size: 55px 75px; 685 | background-repeat: no-repeat; 686 | } 687 | .other-player .tile { 688 | width: 30.555555555555557px; 689 | height: 41.666666666666664px; 690 | background-size: 29.555555555555557px 40.666666666666664px; 691 | } 692 | .mobile .other-player .tile { 693 | width: 19.18518px; 694 | height: 25.9259px; 695 | background-size: 18.18518px 24.9259px; 696 | } 697 | .mobile .tile { 698 | width: 24.66666px; 699 | height: 33.3333px; 700 | background-size: 24.66666px 33.3333px; 701 | } 702 | h3 .tile { 703 | width: 13.2px; 704 | height: 18px; 705 | background-size: 13.2px 18px; 706 | } 707 | .padded { 708 | padding: 8px; 709 | } 710 | .br-top { 711 | margin-top: 8px; 712 | } 713 | .br { 714 | margin-bottom: 8px; 715 | } 716 | .center { 717 | text-align: center; 718 | } 719 | .tile-8 { 720 | background-image: url('../../img/tiles/bamboo_9.png'); 721 | } 722 | .tile-7 { 723 | background-image: url('../../img/tiles/bamboo_8.png'); 724 | } 725 | .tile-6 { 726 | background-image: url('../../img/tiles/bamboo_7.png'); 727 | } 728 | .tile-5 { 729 | background-image: url('../../img/tiles/bamboo_6.png'); 730 | } 731 | .tile-4 { 732 | background-image: url('../../img/tiles/bamboo_5.png'); 733 | } 734 | .tile-3 { 735 | background-image: url('../../img/tiles/bamboo_4.png'); 736 | } 737 | .tile-2 { 738 | background-image: url('../../img/tiles/bamboo_3.png'); 739 | } 740 | .tile-1 { 741 | background-image: url('../../img/tiles/bamboo_2.png'); 742 | } 743 | .tile-0 { 744 | background-image: url('../../img/tiles/bamboo_1.png'); 745 | } 746 | .tile-17 { 747 | background-image: url('../../img/tiles/ball_9.png'); 748 | } 749 | .tile-16 { 750 | background-image: url('../../img/tiles/ball_8.png'); 751 | } 752 | .tile-15 { 753 | background-image: url('../../img/tiles/ball_7.png'); 754 | } 755 | .tile-14 { 756 | background-image: url('../../img/tiles/ball_6.png'); 757 | } 758 | .tile-13 { 759 | background-image: url('../../img/tiles/ball_5.png'); 760 | } 761 | .tile-12 { 762 | background-image: url('../../img/tiles/ball_4.png'); 763 | } 764 | .tile-11 { 765 | background-image: url('../../img/tiles/ball_3.png'); 766 | } 767 | .tile-10 { 768 | background-image: url('../../img/tiles/ball_2.png'); 769 | } 770 | .tile-9 { 771 | background-image: url('../../img/tiles/ball_1.png'); 772 | } 773 | .tile-18 { 774 | background-image: url(../../img/tiles/wind_east.png); 775 | } 776 | .tile-19 { 777 | background-image: url(../../img/tiles/wind_south.png); 778 | } 779 | .tile-20 { 780 | background-image: url(../../img/tiles/wind_west.png); 781 | } 782 | .tile-21 { 783 | background-image: url(../../img/tiles/wind_north.png); 784 | } 785 | .tile-22 { 786 | background-image: url(../../img/tiles/dragon_white.png); 787 | } 788 | .tile-23 { 789 | background-image: url(../../img/tiles/dragon_green.png); 790 | } 791 | .tile-24 { 792 | background-image: url(../../img/tiles/dragon_red.png); 793 | } 794 | .tile-25 { 795 | background-image: url(../../img/tiles/character_1.png); 796 | } 797 | .tile-26 { 798 | background-image: url(../../img/tiles/character_9.png); 799 | } 800 | .tile-placeholder { 801 | background-image: url(../../img/tiles/flower_plum.png); 802 | } 803 | .tile-holder.hidden { 804 | background-color: #d48623; 805 | } 806 | .hidden .tile { 807 | background-image: none; 808 | } 809 | .last-tile { 810 | margin-left: .5em; 811 | } 812 | .discard-tiles { 813 | width: 80%; 814 | } 815 | .other-player .discard-tiles { 816 | width: 100%; 817 | margin: 0; 818 | } 819 | .other-player { 820 | width: 50%; 821 | float: left; 822 | } 823 | .this-player { 824 | position: absolute; 825 | bottom: 1em; 826 | } 827 | #notifications { 828 | position: absolute; 829 | left: 10px; 830 | top: 10px; 831 | } 832 | #player-info { 833 | position: absolute; 834 | right: 10px; 835 | top: 10px; 836 | } 837 | .rotated-tile { 838 | -webkit-transform: rotate(90deg); 839 | -webkit-transform-origin: 35% 50%; 840 | margin-left: 28px; 841 | } 842 | .side { 843 | float: left; 844 | margin-left: .5em; 845 | } 846 | .side a, 847 | .other-player a { 848 | cursor: default; 849 | } 850 | .buttons { 851 | float: left; 852 | font-size: 32px; 853 | } 854 | .mobile .name { 855 | display: none; 856 | } 857 | #audio { 858 | display: none; 859 | } 860 | #msg-other { 861 | margin-left: 8px; 862 | } 863 | .qtip-mahjong { 864 | background-color: #d48623; 865 | border-color: #000; 866 | color: #fff9e5; 867 | } 868 | .qtip-mahjong .qtip-content { 869 | text-align: center; 870 | } 871 | .qtip-mahjong .qtip-close { 872 | border-color: #000; 873 | background: #fff9e5; 874 | } 875 | .shadowed { 876 | text-shadow: 2px 2px #000; 877 | } 878 | .no-decoration { 879 | text-decoration: none; 880 | } 881 | .clickable { 882 | cursor: pointer; 883 | } 884 | -------------------------------------------------------------------------------- /public/css/dist/all.min.css: -------------------------------------------------------------------------------- 1 | .qtip{position:absolute;left:-28000px;top:-28000px;display:none;max-width:280px;min-width:50px;font-size:10.5px;line-height:12px;direction:ltr;box-shadow:none;padding:0}.qtip-content{position:relative;padding:5px 9px;overflow:hidden;text-align:left;word-wrap:break-word}.qtip-titlebar{position:relative;padding:5px 35px 5px 10px;overflow:hidden;border-width:0 0 1px;font-weight:700}.qtip-titlebar+.qtip-content{border-top-width:0!important}.qtip-close{position:absolute;right:-9px;top:-9px;cursor:pointer;outline:medium none;border-width:1px;border-style:solid;border-color:transparent}.qtip-titlebar .qtip-close{right:4px;top:50%;margin-top:-9px}* html .qtip-titlebar .qtip-close{top:16px}.qtip-titlebar .ui-icon,.qtip-icon .ui-icon{display:block;text-indent:-1000em;direction:ltr}.qtip-icon,.qtip-icon .ui-icon{-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;text-decoration:none}.qtip-icon .ui-icon{width:18px;height:14px;line-height:14px;text-align:center;text-indent:0;font:normal bold 10px/13px Tahoma,sans-serif;color:inherit;background:transparent none no-repeat -100em -100em}.qtip-default{border-width:1px;border-style:solid;border-color:#F1D031;background-color:#FFFFA3;color:#555}.qtip-default .qtip-titlebar{background-color:#FFEF93}.qtip-default .qtip-icon{border-color:#CCC;background:#F1F1F1;color:#777}.qtip-default .qtip-titlebar .qtip-close{border-color:#AAA;color:#111}/*! Light tooltip style */.qtip-light{background-color:#fff;border-color:#E2E2E2;color:#454545}.qtip-light .qtip-titlebar{background-color:#f1f1f1}/*! Dark tooltip style */.qtip-dark{background-color:#505050;border-color:#303030;color:#f3f3f3}.qtip-dark .qtip-titlebar{background-color:#404040}.qtip-dark .qtip-icon{border-color:#444}.qtip-dark .qtip-titlebar .ui-state-hover{border-color:#303030}/*! Cream tooltip style */.qtip-cream{background-color:#FBF7AA;border-color:#F9E98E;color:#A27D35}.qtip-cream .qtip-titlebar{background-color:#F0DE7D}.qtip-cream .qtip-close .qtip-icon{background-position:-82px 0}/*! Red tooltip style */.qtip-red{background-color:#F78B83;border-color:#D95252;color:#912323}.qtip-red .qtip-titlebar{background-color:#F06D65}.qtip-red .qtip-close .qtip-icon{background-position:-102px 0}.qtip-red .qtip-icon{border-color:#D95252}.qtip-red .qtip-titlebar .ui-state-hover{border-color:#D95252}/*! Green tooltip style */.qtip-green{background-color:#CAED9E;border-color:#90D93F;color:#3F6219}.qtip-green .qtip-titlebar{background-color:#B0DE78}.qtip-green .qtip-close .qtip-icon{background-position:-42px 0}/*! Blue tooltip style */.qtip-blue{background-color:#E5F6FE;border-color:#ADD9ED;color:#5E99BD}.qtip-blue .qtip-titlebar{background-color:#D0E9F5}.qtip-blue .qtip-close .qtip-icon{background-position:-2px 0}.qtip-shadow{-webkit-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);-moz-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);box-shadow:1px 1px 3px 1px rgba(0,0,0,.15)}.qtip-rounded,.qtip-tipsy,.qtip-bootstrap{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.qtip-rounded .qtip-titlebar{-moz-border-radius:4px 4px 0 0;-webkit-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.qtip-youtube{-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 3px #333;-moz-box-shadow:0 0 3px #333;box-shadow:0 0 3px #333;color:#fff;border-width:0;background:#4A4A4A;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(0,#4a4a4a),color-stop(100%,#000));background-image:-webkit-linear-gradient(top,#4a4a4a 0,#000 100%);background-image:-moz-linear-gradient(top,#4a4a4a 0,#000 100%);background-image:-ms-linear-gradient(top,#4a4a4a 0,#000 100%);background-image:-o-linear-gradient(top,#4a4a4a 0,#000 100%)}.qtip-youtube .qtip-titlebar{background-color:#4A4A4A;background-color:rgba(0,0,0,0)}.qtip-youtube .qtip-content{padding:.75em;font:12px arial,sans-serif;filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0, StartColorStr=#4a4a4a, EndColorStr=#000000);-ms-filter:"progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#4a4a4a,EndColorStr=#000000);"}.qtip-youtube .qtip-icon{border-color:#222}.qtip-youtube .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-jtools{background:#232323;background:rgba(0,0,0,.7);background-image:-webkit-gradient(linear,left top,left bottom,from(#717171),to(#232323));background-image:-moz-linear-gradient(top,#717171,#232323);background-image:-webkit-linear-gradient(top,#717171,#232323);background-image:-ms-linear-gradient(top,#717171,#232323);background-image:-o-linear-gradient(top,#717171,#232323);border:2px solid #ddd;border:2px solid #f1f1f1;-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 12px #333;-moz-box-shadow:0 0 12px #333;box-shadow:0 0 12px #333}.qtip-jtools .qtip-titlebar{background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171, endColorstr=#4a4a4a);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A)"}.qtip-jtools .qtip-content{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4a4a4a, endColorstr=#232323);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323)"}.qtip-jtools .qtip-titlebar,.qtip-jtools .qtip-content{background:0 0;color:#fff;border:0 dashed transparent}.qtip-jtools .qtip-icon{border-color:#555}.qtip-jtools .qtip-titlebar .ui-state-hover{border-color:#333}.qtip-cluetip{-webkit-box-shadow:4px 4px 5px rgba(0,0,0,.4);-moz-box-shadow:4px 4px 5px rgba(0,0,0,.4);box-shadow:4px 4px 5px rgba(0,0,0,.4);background-color:#D9D9C2;color:#111;border:0 dashed transparent}.qtip-cluetip .qtip-titlebar{background-color:#87876A;color:#fff;border:0 dashed transparent}.qtip-cluetip .qtip-icon{border-color:#808064}.qtip-cluetip .qtip-titlebar .ui-state-hover{border-color:#696952;color:#696952}.qtip-tipsy{background:#000;background:rgba(0,0,0,.87);color:#fff;border:0 solid transparent;font-size:11px;font-family:'Lucida Grande',sans-serif;font-weight:700;line-height:16px;text-shadow:0 1px #000}.qtip-tipsy .qtip-titlebar{padding:6px 35px 0 10px;background-color:transparent}.qtip-tipsy .qtip-content{padding:6px 10px}.qtip-tipsy .qtip-icon{border-color:#222;text-shadow:none}.qtip-tipsy .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-tipped{border:3px solid #959FA9;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-color:#F9F9F9;color:#454545;font-weight:400;font-family:serif}.qtip-tipped .qtip-titlebar{border-bottom-width:0;color:#fff;background:#3A79B8;background-image:-webkit-gradient(linear,left top,left bottom,from(#3a79b8),to(#2e629d));background-image:-webkit-linear-gradient(top,#3a79b8,#2e629d);background-image:-moz-linear-gradient(top,#3a79b8,#2e629d);background-image:-ms-linear-gradient(top,#3a79b8,#2e629d);background-image:-o-linear-gradient(top,#3a79b8,#2e629d);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3a79b8, endColorstr=#2e629d);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)"}.qtip-tipped .qtip-icon{border:2px solid #285589;background:#285589}.qtip-tipped .qtip-icon .ui-icon{background-color:#FBFBFB;color:#555}.qtip-bootstrap{font-size:14px;line-height:20px;color:#333;padding:1px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.qtip-bootstrap .qtip-titlebar{padding:8px 14px;margin:0;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.qtip-bootstrap .qtip-titlebar .qtip-close{right:11px;top:45%;border-style:none}.qtip-bootstrap .qtip-content{padding:9px 14px}.qtip-bootstrap .qtip-icon{background:0 0}.qtip-bootstrap .qtip-icon .ui-icon{width:auto;height:auto;float:right;font-size:20px;font-weight:700;line-height:18px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.qtip-bootstrap .qtip-icon .ui-icon:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}.qtip:not(.ie9haxors) div.qtip-content,.qtip:not(.ie9haxors) div.qtip-titlebar{filter:none;-ms-filter:none}html{color:#000;width:100%;font-family:"Lucida Grande",'Helvetica Neue',Helvetica,Arial}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0}table{border-collapse:collapse;border-spacing:0}fieldset,img{border:0}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:400}li{list-style:none}caption,th{text-align:left}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:300;font-family:"Lucida Grande",'Helvetica Neue',Helvetica,Arial}q:before,q:after{content:''}abbr,acronym{border:0;font-variant:normal}sup{vertical-align:text-top}sub{vertical-align:text-bottom}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit}input,textarea,select{*font-size:100%}legend{color:#000}b,u,s,i{font-style:normal;font-weight:400;text-decoration:none}h1,h2{font-size:30px;line-height:40px}h3{font-size:18px}body{padding:50px;background-color:#275001;color:#FFFBE9}body.mobile{padding:30px 2px}a{color:#00B7FF}a.tile-holder{color:#00B7FF;background:#FFFBE9;padding:4px;border-radius:8px;margin:4px;-moz-box-shadow:4px 4px 0 #000;-webkit-box-shadow:4px 4px 0 #000;box-shadow:4px 4px 0 #000;width:55px;height:75px}.mobile a.tile-holder{margin:1px}.other-player a.tile-holder{width:30.555555555555557px;height:41.666666666666664px;padding:3px;margin:2px}.mobile .other-player a.tile-holder{width:19.18518px;height:25.9259px;padding:3px;margin:1px}.mobile .other-player .hand-tiles{display:none}.tile-row{font-size:71px}.other-player .tile-row{font-size:35.5px}.mobile a.tile-holder{width:24.66666px;height:33.3333px}h3 a.tile-holder{width:13.2px;height:18px;padding:3px;margin:0;border-radius:4px}.mobile .tile-row{font-size:53px}a.selected{border:4px solid red;margin-top:2px;margin-left:2px;padding:1px}.mobile a.selected{margin:2px;padding:2px}.left{float:left}.right{float:right}.clr{overflow:hidden;zoom:1}.small{font-size:12px}.dim{color:#999}.br-top{padding-top:8px}.br{padding-bottom:8px}.hide{display:none}.tile{width:55px;height:75px;background-size:55px 75px;background-repeat:no-repeat}.other-player .tile{width:30.555555555555557px;height:41.666666666666664px;background-size:29.555555555555557px 40.666666666666664px}.mobile .other-player .tile{width:19.18518px;height:25.9259px;background-size:18.18518px 24.9259px}.mobile .tile{width:24.66666px;height:33.3333px;background-size:24.66666px 33.3333px}h3 .tile{width:13.2px;height:18px;background-size:13.2px 18px}.padded{padding:8px}.br-top{margin-top:8px}.br{margin-bottom:8px}.center{text-align:center}.tile-8{background-image:url(../../img/tiles/bamboo_9.png)}.tile-7{background-image:url(../../img/tiles/bamboo_8.png)}.tile-6{background-image:url(../../img/tiles/bamboo_7.png)}.tile-5{background-image:url(../../img/tiles/bamboo_6.png)}.tile-4{background-image:url(../../img/tiles/bamboo_5.png)}.tile-3{background-image:url(../../img/tiles/bamboo_4.png)}.tile-2{background-image:url(../../img/tiles/bamboo_3.png)}.tile-1{background-image:url(../../img/tiles/bamboo_2.png)}.tile-0{background-image:url(../../img/tiles/bamboo_1.png)}.tile-17{background-image:url(../../img/tiles/ball_9.png)}.tile-16{background-image:url(../../img/tiles/ball_8.png)}.tile-15{background-image:url(../../img/tiles/ball_7.png)}.tile-14{background-image:url(../../img/tiles/ball_6.png)}.tile-13{background-image:url(../../img/tiles/ball_5.png)}.tile-12{background-image:url(../../img/tiles/ball_4.png)}.tile-11{background-image:url(../../img/tiles/ball_3.png)}.tile-10{background-image:url(../../img/tiles/ball_2.png)}.tile-9{background-image:url(../../img/tiles/ball_1.png)}.tile-18{background-image:url(../../img/tiles/wind_east.png)}.tile-19{background-image:url(../../img/tiles/wind_south.png)}.tile-20{background-image:url(../../img/tiles/wind_west.png)}.tile-21{background-image:url(../../img/tiles/wind_north.png)}.tile-22{background-image:url(../../img/tiles/dragon_white.png)}.tile-23{background-image:url(../../img/tiles/dragon_green.png)}.tile-24{background-image:url(../../img/tiles/dragon_red.png)}.tile-25{background-image:url(../../img/tiles/character_1.png)}.tile-26{background-image:url(../../img/tiles/character_9.png)}.tile-placeholder{background-image:url(../../img/tiles/flower_plum.png)}.tile-holder.hidden{background-color:#d48623}.hidden .tile{background-image:none}.last-tile{margin-left:.5em}.discard-tiles{width:80%}.other-player .discard-tiles{width:100%;margin:0}.other-player{width:50%;float:left}.this-player{position:absolute;bottom:1em}#notifications{position:absolute;left:10px;top:10px}#player-info{position:absolute;right:10px;top:10px}.rotated-tile{-webkit-transform:rotate(90deg);-webkit-transform-origin:35% 50%;margin-left:28px}.side{float:left;margin-left:.5em}.side a,.other-player a{cursor:default}.buttons{float:left;font-size:32px}.mobile .name{display:none}#audio{display:none}#msg-other{margin-left:8px}.qtip-mahjong{background-color:#d48623;border-color:#000;color:#fff9e5}.qtip-mahjong .qtip-content{text-align:center}.qtip-mahjong .qtip-close{border-color:#000;background:#fff9e5}.shadowed{text-shadow:2px 2px #000}.no-decoration{text-decoration:none}.clickable{cursor:pointer} -------------------------------------------------------------------------------- /public/css/src/jquery.qtip.less: -------------------------------------------------------------------------------- 1 | /* 2 | * qTip2 - Pretty powerful tooltips - v2.2.0 3 | * http://qtip2.com 4 | * 5 | * Copyright (c) 2013 Craig Michael Thompson 6 | * Released under the MIT, GPL licenses 7 | * http://jquery.org/license 8 | * 9 | * Date: Sun Dec 15 2013 11:29 EST-0500 10 | * Plugins: None 11 | * Styles: basic css3 12 | */ 13 | .qtip{ 14 | position: absolute; 15 | left: -28000px; 16 | top: -28000px; 17 | display: none; 18 | 19 | max-width: 280px; 20 | min-width: 50px; 21 | 22 | font-size: 10.5px; 23 | line-height: 12px; 24 | 25 | direction: ltr; 26 | 27 | box-shadow: none; 28 | padding: 0; 29 | } 30 | 31 | .qtip-content{ 32 | position: relative; 33 | padding: 5px 9px; 34 | overflow: hidden; 35 | 36 | text-align: left; 37 | word-wrap: break-word; 38 | } 39 | 40 | .qtip-titlebar{ 41 | position: relative; 42 | padding: 5px 35px 5px 10px; 43 | overflow: hidden; 44 | 45 | border-width: 0 0 1px; 46 | font-weight: bold; 47 | } 48 | 49 | .qtip-titlebar + .qtip-content{ border-top-width: 0 !important; } 50 | 51 | /* Default close button class */ 52 | .qtip-close{ 53 | position: absolute; 54 | right: -9px; top: -9px; 55 | 56 | cursor: pointer; 57 | outline: medium none; 58 | 59 | border-width: 1px; 60 | border-style: solid; 61 | border-color: transparent; 62 | } 63 | 64 | .qtip-titlebar .qtip-close{ 65 | right: 4px; top: 50%; 66 | margin-top: -9px; 67 | } 68 | 69 | * html .qtip-titlebar .qtip-close{ top: 16px; } /* IE fix */ 70 | 71 | .qtip-titlebar .ui-icon, 72 | .qtip-icon .ui-icon{ 73 | display: block; 74 | text-indent: -1000em; 75 | direction: ltr; 76 | } 77 | 78 | .qtip-icon, .qtip-icon .ui-icon{ 79 | -moz-border-radius: 3px; 80 | -webkit-border-radius: 3px; 81 | border-radius: 3px; 82 | text-decoration: none; 83 | } 84 | 85 | .qtip-icon .ui-icon{ 86 | width: 18px; 87 | height: 14px; 88 | 89 | line-height: 14px; 90 | text-align: center; 91 | text-indent: 0; 92 | font: normal bold 10px/13px Tahoma,sans-serif; 93 | 94 | color: inherit; 95 | background: transparent none no-repeat -100em -100em; 96 | } 97 | 98 | /* Applied to 'focused' tooltips e.g. most recently displayed/interacted with */ 99 | .qtip-focus{} 100 | 101 | /* Applied on hover of tooltips i.e. added/removed on mouseenter/mouseleave respectively */ 102 | .qtip-hover{} 103 | 104 | /* Default tooltip style */ 105 | .qtip-default{ 106 | border-width: 1px; 107 | border-style: solid; 108 | border-color: #F1D031; 109 | 110 | background-color: #FFFFA3; 111 | color: #555; 112 | } 113 | 114 | .qtip-default .qtip-titlebar{ 115 | background-color: #FFEF93; 116 | } 117 | 118 | .qtip-default .qtip-icon{ 119 | border-color: #CCC; 120 | background: #F1F1F1; 121 | color: #777; 122 | } 123 | 124 | .qtip-default .qtip-titlebar .qtip-close{ 125 | border-color: #AAA; 126 | color: #111; 127 | } 128 | 129 | 130 | 131 | /*! Light tooltip style */ 132 | .qtip-light{ 133 | background-color: white; 134 | border-color: #E2E2E2; 135 | color: #454545; 136 | } 137 | 138 | .qtip-light .qtip-titlebar{ 139 | background-color: #f1f1f1; 140 | } 141 | 142 | 143 | /*! Dark tooltip style */ 144 | .qtip-dark{ 145 | background-color: #505050; 146 | border-color: #303030; 147 | color: #f3f3f3; 148 | } 149 | 150 | .qtip-dark .qtip-titlebar{ 151 | background-color: #404040; 152 | } 153 | 154 | .qtip-dark .qtip-icon{ 155 | border-color: #444; 156 | } 157 | 158 | .qtip-dark .qtip-titlebar .ui-state-hover{ 159 | border-color: #303030; 160 | } 161 | 162 | 163 | /*! Cream tooltip style */ 164 | .qtip-cream{ 165 | background-color: #FBF7AA; 166 | border-color: #F9E98E; 167 | color: #A27D35; 168 | } 169 | 170 | .qtip-cream .qtip-titlebar{ 171 | background-color: #F0DE7D; 172 | } 173 | 174 | .qtip-cream .qtip-close .qtip-icon{ 175 | background-position: -82px 0; 176 | } 177 | 178 | 179 | /*! Red tooltip style */ 180 | .qtip-red{ 181 | background-color: #F78B83; 182 | border-color: #D95252; 183 | color: #912323; 184 | } 185 | 186 | .qtip-red .qtip-titlebar{ 187 | background-color: #F06D65; 188 | } 189 | 190 | .qtip-red .qtip-close .qtip-icon{ 191 | background-position: -102px 0; 192 | } 193 | 194 | .qtip-red .qtip-icon{ 195 | border-color: #D95252; 196 | } 197 | 198 | .qtip-red .qtip-titlebar .ui-state-hover{ 199 | border-color: #D95252; 200 | } 201 | 202 | 203 | /*! Green tooltip style */ 204 | .qtip-green{ 205 | background-color: #CAED9E; 206 | border-color: #90D93F; 207 | color: #3F6219; 208 | } 209 | 210 | .qtip-green .qtip-titlebar{ 211 | background-color: #B0DE78; 212 | } 213 | 214 | .qtip-green .qtip-close .qtip-icon{ 215 | background-position: -42px 0; 216 | } 217 | 218 | 219 | /*! Blue tooltip style */ 220 | .qtip-blue{ 221 | background-color: #E5F6FE; 222 | border-color: #ADD9ED; 223 | color: #5E99BD; 224 | } 225 | 226 | .qtip-blue .qtip-titlebar{ 227 | background-color: #D0E9F5; 228 | } 229 | 230 | .qtip-blue .qtip-close .qtip-icon{ 231 | background-position: -2px 0; 232 | } 233 | 234 | 235 | 236 | .qtip-shadow{ 237 | -webkit-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); 238 | -moz-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); 239 | box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); 240 | } 241 | 242 | /* Add rounded corners to your tooltips in: FF3+, Chrome 2+, Opera 10.6+, IE9+, Safari 2+ */ 243 | .qtip-rounded, 244 | .qtip-tipsy, 245 | .qtip-bootstrap{ 246 | -moz-border-radius: 5px; 247 | -webkit-border-radius: 5px; 248 | border-radius: 5px; 249 | } 250 | 251 | .qtip-rounded .qtip-titlebar{ 252 | -moz-border-radius: 4px 4px 0 0; 253 | -webkit-border-radius: 4px 4px 0 0; 254 | border-radius: 4px 4px 0 0; 255 | } 256 | 257 | /* Youtube tooltip style */ 258 | .qtip-youtube{ 259 | -moz-border-radius: 2px; 260 | -webkit-border-radius: 2px; 261 | border-radius: 2px; 262 | 263 | -webkit-box-shadow: 0 0 3px #333; 264 | -moz-box-shadow: 0 0 3px #333; 265 | box-shadow: 0 0 3px #333; 266 | 267 | color: white; 268 | border-width: 0; 269 | 270 | background: #4A4A4A; 271 | background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0,#4A4A4A),color-stop(100%,black)); 272 | background-image: -webkit-linear-gradient(top,#4A4A4A 0,black 100%); 273 | background-image: -moz-linear-gradient(top,#4A4A4A 0,black 100%); 274 | background-image: -ms-linear-gradient(top,#4A4A4A 0,black 100%); 275 | background-image: -o-linear-gradient(top,#4A4A4A 0,black 100%); 276 | } 277 | 278 | .qtip-youtube .qtip-titlebar{ 279 | background-color: #4A4A4A; 280 | background-color: rgba(0,0,0,0); 281 | } 282 | 283 | .qtip-youtube .qtip-content{ 284 | padding: .75em; 285 | font: 12px arial,sans-serif; 286 | 287 | filter: progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#4a4a4a,EndColorStr=#000000); 288 | -ms-filter: "progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#4a4a4a,EndColorStr=#000000);"; 289 | } 290 | 291 | .qtip-youtube .qtip-icon{ 292 | border-color: #222; 293 | } 294 | 295 | .qtip-youtube .qtip-titlebar .ui-state-hover{ 296 | border-color: #303030; 297 | } 298 | 299 | 300 | /* jQuery TOOLS Tooltip style */ 301 | .qtip-jtools{ 302 | background: #232323; 303 | background: rgba(0, 0, 0, 0.7); 304 | background-image: -webkit-gradient(linear, left top, left bottom, from(#717171), to(#232323)); 305 | background-image: -moz-linear-gradient(top, #717171, #232323); 306 | background-image: -webkit-linear-gradient(top, #717171, #232323); 307 | background-image: -ms-linear-gradient(top, #717171, #232323); 308 | background-image: -o-linear-gradient(top, #717171, #232323); 309 | 310 | border: 2px solid #ddd; 311 | border: 2px solid rgba(241,241,241,1); 312 | 313 | -moz-border-radius: 2px; 314 | -webkit-border-radius: 2px; 315 | border-radius: 2px; 316 | 317 | -webkit-box-shadow: 0 0 12px #333; 318 | -moz-box-shadow: 0 0 12px #333; 319 | box-shadow: 0 0 12px #333; 320 | } 321 | 322 | /* IE Specific */ 323 | .qtip-jtools .qtip-titlebar{ 324 | background-color: transparent; 325 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A); 326 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A)"; 327 | } 328 | .qtip-jtools .qtip-content{ 329 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323); 330 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323)"; 331 | } 332 | 333 | .qtip-jtools .qtip-titlebar, 334 | .qtip-jtools .qtip-content{ 335 | background: transparent; 336 | color: white; 337 | border: 0 dashed transparent; 338 | } 339 | 340 | .qtip-jtools .qtip-icon{ 341 | border-color: #555; 342 | } 343 | 344 | .qtip-jtools .qtip-titlebar .ui-state-hover{ 345 | border-color: #333; 346 | } 347 | 348 | 349 | /* Cluetip style */ 350 | .qtip-cluetip{ 351 | -webkit-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); 352 | -moz-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); 353 | box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); 354 | 355 | background-color: #D9D9C2; 356 | color: #111; 357 | border: 0 dashed transparent; 358 | } 359 | 360 | .qtip-cluetip .qtip-titlebar{ 361 | background-color: #87876A; 362 | color: white; 363 | border: 0 dashed transparent; 364 | } 365 | 366 | .qtip-cluetip .qtip-icon{ 367 | border-color: #808064; 368 | } 369 | 370 | .qtip-cluetip .qtip-titlebar .ui-state-hover{ 371 | border-color: #696952; 372 | color: #696952; 373 | } 374 | 375 | 376 | /* Tipsy style */ 377 | .qtip-tipsy{ 378 | background: black; 379 | background: rgba(0, 0, 0, .87); 380 | 381 | color: white; 382 | border: 0 solid transparent; 383 | 384 | font-size: 11px; 385 | font-family: 'Lucida Grande', sans-serif; 386 | font-weight: bold; 387 | line-height: 16px; 388 | text-shadow: 0 1px black; 389 | } 390 | 391 | .qtip-tipsy .qtip-titlebar{ 392 | padding: 6px 35px 0 10px; 393 | background-color: transparent; 394 | } 395 | 396 | .qtip-tipsy .qtip-content{ 397 | padding: 6px 10px; 398 | } 399 | 400 | .qtip-tipsy .qtip-icon{ 401 | border-color: #222; 402 | text-shadow: none; 403 | } 404 | 405 | .qtip-tipsy .qtip-titlebar .ui-state-hover{ 406 | border-color: #303030; 407 | } 408 | 409 | 410 | /* Tipped style */ 411 | .qtip-tipped{ 412 | border: 3px solid #959FA9; 413 | 414 | -moz-border-radius: 3px; 415 | -webkit-border-radius: 3px; 416 | border-radius: 3px; 417 | 418 | background-color: #F9F9F9; 419 | color: #454545; 420 | 421 | font-weight: normal; 422 | font-family: serif; 423 | } 424 | 425 | .qtip-tipped .qtip-titlebar{ 426 | border-bottom-width: 0; 427 | 428 | color: white; 429 | background: #3A79B8; 430 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3A79B8), to(#2E629D)); 431 | background-image: -webkit-linear-gradient(top, #3A79B8, #2E629D); 432 | background-image: -moz-linear-gradient(top, #3A79B8, #2E629D); 433 | background-image: -ms-linear-gradient(top, #3A79B8, #2E629D); 434 | background-image: -o-linear-gradient(top, #3A79B8, #2E629D); 435 | filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D); 436 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)"; 437 | } 438 | 439 | .qtip-tipped .qtip-icon{ 440 | border: 2px solid #285589; 441 | background: #285589; 442 | } 443 | 444 | .qtip-tipped .qtip-icon .ui-icon{ 445 | background-color: #FBFBFB; 446 | color: #555; 447 | } 448 | 449 | 450 | /** 451 | * Twitter Bootstrap style. 452 | * 453 | * Tested with IE 8, IE 9, Chrome 18, Firefox 9, Opera 11. 454 | * Does not work with IE 7. 455 | */ 456 | .qtip-bootstrap{ 457 | /** Taken from Bootstrap body */ 458 | font-size: 14px; 459 | line-height: 20px; 460 | color: #333333; 461 | 462 | /** Taken from Bootstrap .popover */ 463 | padding: 1px; 464 | background-color: #ffffff; 465 | border: 1px solid #ccc; 466 | border: 1px solid rgba(0, 0, 0, 0.2); 467 | -webkit-border-radius: 6px; 468 | -moz-border-radius: 6px; 469 | border-radius: 6px; 470 | -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); 471 | -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); 472 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); 473 | -webkit-background-clip: padding-box; 474 | -moz-background-clip: padding; 475 | background-clip: padding-box; 476 | } 477 | 478 | .qtip-bootstrap .qtip-titlebar{ 479 | /** Taken from Bootstrap .popover-title */ 480 | padding: 8px 14px; 481 | margin: 0; 482 | font-size: 14px; 483 | font-weight: normal; 484 | line-height: 18px; 485 | background-color: #f7f7f7; 486 | border-bottom: 1px solid #ebebeb; 487 | -webkit-border-radius: 5px 5px 0 0; 488 | -moz-border-radius: 5px 5px 0 0; 489 | border-radius: 5px 5px 0 0; 490 | } 491 | 492 | .qtip-bootstrap .qtip-titlebar .qtip-close{ 493 | /** 494 | * Overrides qTip2: 495 | * .qtip-titlebar .qtip-close{ 496 | * [...] 497 | * right: 4px; 498 | * top: 50%; 499 | * [...] 500 | * border-style: solid; 501 | * } 502 | */ 503 | right: 11px; 504 | top: 45%; 505 | border-style: none; 506 | } 507 | 508 | .qtip-bootstrap .qtip-content{ 509 | /** Taken from Bootstrap .popover-content */ 510 | padding: 9px 14px; 511 | } 512 | 513 | .qtip-bootstrap .qtip-icon{ 514 | /** 515 | * Overrides qTip2: 516 | * .qtip-default .qtip-icon { 517 | * border-color: #CCC; 518 | * background: #F1F1F1; 519 | * color: #777; 520 | * } 521 | */ 522 | background: transparent; 523 | } 524 | 525 | .qtip-bootstrap .qtip-icon .ui-icon{ 526 | /** 527 | * Overrides qTip2: 528 | * .qtip-icon .ui-icon{ 529 | * width: 18px; 530 | * height: 14px; 531 | * } 532 | */ 533 | width: auto; 534 | height: auto; 535 | 536 | /* Taken from Bootstrap .close */ 537 | float: right; 538 | font-size: 20px; 539 | font-weight: bold; 540 | line-height: 18px; 541 | color: #000000; 542 | text-shadow: 0 1px 0 #ffffff; 543 | opacity: 0.2; 544 | filter: alpha(opacity=20); 545 | } 546 | 547 | .qtip-bootstrap .qtip-icon .ui-icon:hover{ 548 | /* Taken from Bootstrap .close:hover */ 549 | color: #000000; 550 | text-decoration: none; 551 | cursor: pointer; 552 | opacity: 0.4; 553 | filter: alpha(opacity=40); 554 | } 555 | 556 | 557 | /* IE9 fix - removes all filters */ 558 | .qtip:not(.ie9haxors) div.qtip-content, 559 | .qtip:not(.ie9haxors) div.qtip-titlebar{ 560 | filter: none; 561 | -ms-filter: none; 562 | } 563 | 564 | -------------------------------------------------------------------------------- /public/css/src/reset.less: -------------------------------------------------------------------------------- 1 | /* 2 | YUI Recent CSS copyright Yahoo! Inc. All rights reserved. 3 | Code licensed under the BSD License: 4 | http://developer.yahoo.net/yui/license.txt 5 | version: 2.5.2 6 | */ 7 | html{color:#000;width:100%;font-family: "Lucida Grande", 'Helvetica Neue', 'Helvetica', 'Arial';} 8 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;} 9 | table{border-collapse:collapse;border-spacing:0;} 10 | fieldset,img{border:0;} 11 | address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;} 12 | li{list-style:none;} 13 | caption,th{text-align:left;} 14 | h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:300;font-family: "Lucida Grande", 'Helvetica Neue', 'Helvetica', 'Arial';} 15 | q:before,q:after{content:'';} 16 | abbr,acronym {border:0;font-variant:normal;} 17 | /* to preserve line-height and selector appearance */ 18 | sup {vertical-align:text-top;} 19 | sub {vertical-align:text-bottom;} 20 | input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;} 21 | /*to enable resizing for IE*/ 22 | input,textarea,select{*font-size:100%;} 23 | /*because legend doesn't inherit in IE */ 24 | legend{color:#000;} 25 | b,u,s,i { font-style:normal; font-weight:normal; text-decoration:none; } -------------------------------------------------------------------------------- /public/css/src/style.less: -------------------------------------------------------------------------------- 1 | @tile-background-color: #D48623; 2 | @tile-front-color: #FFF9E5; 3 | 4 | @tile-height: 75px; 5 | @tile-height-other: @tile-height*5/9; 6 | @tile-height-mobile: 33.3333px; 7 | @tile-height-other-mobile: @tile-height-mobile*7/9; 8 | @tile-height-text: 18px; 9 | 10 | @tile-width: 55px; 11 | @tile-width-other: @tile-width*5/9; 12 | @tile-width-mobile: 24.66666px; 13 | @tile-width-other-mobile: @tile-width-mobile*7/9; 14 | @tile-font-height: 71px; 15 | @tile-font-height-mobile: 53px; 16 | @tile-width-text: 13.2px; 17 | 18 | h1, 19 | h2 { 20 | font-size: 30px; 21 | line-height: 40px; 22 | } 23 | 24 | h3 { 25 | font-size: 18px; 26 | } 27 | 28 | 29 | body { 30 | padding: 50px; 31 | background-color: #275001; 32 | color: #FFFBE9; 33 | } 34 | 35 | body.mobile { 36 | padding: 30px 2px; 37 | } 38 | 39 | a { 40 | color: #00B7FF; 41 | } 42 | 43 | a.tile-holder { 44 | color: #00B7FF; 45 | background: #FFFBE9; 46 | padding: 4px; 47 | border-radius: 8px; 48 | margin: 4px; 49 | -moz-box-shadow: 4px 4px 0px #000; 50 | -webkit-box-shadow: 4px 4px 0px #000; 51 | box-shadow: 4px 4px 0px #000; 52 | width: @tile-width; 53 | height: @tile-height; 54 | } 55 | 56 | .mobile a.tile-holder { 57 | margin: 1px; 58 | } 59 | 60 | #player-tiles .tile-holder:hover { 61 | // margin-top: -8px; 62 | } 63 | 64 | .other-player a.tile-holder { 65 | width: @tile-width-other; 66 | height: @tile-height-other; 67 | padding: 3px; 68 | margin: 2px; 69 | } 70 | 71 | .mobile .other-player a.tile-holder { 72 | width: @tile-width-other-mobile; 73 | height: @tile-height-other-mobile; 74 | padding: 3px; 75 | margin: 1px; 76 | } 77 | 78 | .mobile .other-player .hand-tiles { 79 | display: none; 80 | } 81 | 82 | .tile-row { font-size: @tile-font-height; } 83 | 84 | .other-player .tile-row { font-size: @tile-font-height/2; } 85 | 86 | .mobile a.tile-holder { 87 | width: @tile-width-mobile; 88 | height: @tile-height-mobile; 89 | } 90 | 91 | h3 a.tile-holder { 92 | width: @tile-width-text; 93 | height: @tile-height-text; 94 | padding: 3px; 95 | margin: 0px; 96 | border-radius: 4px; 97 | } 98 | 99 | .mobile .tile-row { font-size: @tile-font-height-mobile; } 100 | 101 | a.selected { 102 | border: 4px solid #FF0000; 103 | margin-top: 2px; 104 | margin-left: 2px; 105 | padding: 1px; 106 | } 107 | 108 | .mobile a.selected { 109 | margin: 2px 2px; 110 | padding: 2px; 111 | } 112 | 113 | .left { float: left; } 114 | .right { float: right; } 115 | .clr {overflow:hidden; zoom:1} 116 | .small{font-size:12px} 117 | .dim{color:#999} 118 | .br-top {padding-top: 8px;} 119 | .br{ padding-bottom: 8px;} 120 | .hide { display: none; } 121 | .tile { width: @tile-width; 122 | height: @tile-height; 123 | background-size: @tile-width @tile-height; 124 | background-repeat: no-repeat;} 125 | .other-player .tile { 126 | width: @tile-width-other; 127 | height: @tile-height-other; 128 | background-size: (@tile-width-other - 1) (@tile-height-other - 1); 129 | } 130 | .mobile .other-player .tile { 131 | width: @tile-width-other-mobile; 132 | height: @tile-height-other-mobile; 133 | background-size: (@tile-width-other-mobile - 1) (@tile-height-other-mobile - 1); 134 | } 135 | .mobile .tile { width: @tile-width-mobile; height: @tile-height-mobile; background-size: @tile-width-mobile @tile-height-mobile; } 136 | h3 .tile { width: @tile-width-text; height: @tile-height-text; background-size: @tile-width-text @tile-height-text; } 137 | .padded { padding: 8px; } 138 | .br-top { margin-top: 8px; } 139 | .br { margin-bottom: 8px; } 140 | 141 | .center { text-align: center; } 142 | 143 | // Create bamboo and ball tiles 0-17 144 | @num-tiles-in-suit: 9; 145 | @bamboo: bamboo; 146 | @ball: ball; 147 | .makeTile(@type; @tile-value; @i) { 148 | .tile-@{tile-value} { 149 | background-image: url('../../img/tiles/@{type}_@{i}.png'); 150 | } 151 | } 152 | .makeTiles(@type; @tile-value; @i) when (@i > 0) { 153 | .makeTile(@type; @tile-value; @i); 154 | .makeTiles(@type; (@tile-value - 1); (@i - 1)); 155 | } 156 | .makeTiles(@bamboo; (@num-tiles-in-suit - 1); @num-tiles-in-suit); 157 | .makeTiles(@ball; (@num-tiles-in-suit - 1 + 9); @num-tiles-in-suit); 158 | 159 | .tile-18 { background-image: url(../../img/tiles/wind_east.png); } 160 | .tile-19 { background-image: url(../../img/tiles/wind_south.png); } 161 | .tile-20 { background-image: url(../../img/tiles/wind_west.png); } 162 | .tile-21 { background-image: url(../../img/tiles/wind_north.png); } 163 | .tile-22 { background-image: url(../../img/tiles/dragon_white.png); } 164 | .tile-23 { background-image: url(../../img/tiles/dragon_green.png); } 165 | .tile-24 { background-image: url(../../img/tiles/dragon_red.png); } 166 | .tile-25 { background-image: url(../../img/tiles/character_1.png); } 167 | .tile-26 { background-image: url(../../img/tiles/character_9.png); } 168 | .tile-placeholder { background-image: url(../../img/tiles/flower_plum.png); } 169 | .tile-holder.hidden { background-color: @tile-background-color; } 170 | .hidden .tile { background-image: none; } 171 | 172 | 173 | .last-tile { margin-left: .5em; } 174 | .discard-tiles { width: 80%; } 175 | .other-player .discard-tiles { 176 | width: 100%; 177 | margin: 0; 178 | } 179 | .other-player { 180 | width: 50%; 181 | float: left; 182 | } 183 | .this-player { 184 | position: absolute; 185 | bottom: 1em; 186 | } 187 | #notifications { 188 | position: absolute; left: 10px; top: 10px; 189 | } 190 | #player-info { 191 | position: absolute; right: 10px; top: 10px; 192 | } 193 | .rotated-tile { 194 | // TODO(gleitz): use this 195 | -webkit-transform: rotate(90deg); 196 | -webkit-transform-origin: 35% 50%; 197 | margin-left: 28px; 198 | } 199 | .side { 200 | float: left; 201 | margin-left: .5em; 202 | } 203 | .side a, .other-player a { 204 | cursor: default; 205 | } 206 | .buttons { 207 | float: left; 208 | font-size: 32px; 209 | } 210 | 211 | .mobile .name { 212 | display: none; 213 | } 214 | 215 | #audio { 216 | display: none; 217 | } 218 | 219 | #msg-other { 220 | margin-left: 8px; 221 | } 222 | 223 | .qtip-mahjong { 224 | background-color: @tile-background-color; 225 | border-color: #000; 226 | color: @tile-front-color; 227 | } 228 | 229 | .qtip-mahjong .qtip-content { 230 | text-align: center; 231 | } 232 | 233 | .qtip-mahjong .qtip-close { 234 | border-color: #000; 235 | background: @tile-front-color; 236 | } 237 | 238 | .shadowed { 239 | text-shadow: 2px 2px #000; 240 | } 241 | 242 | .no-decoration { 243 | text-decoration: none; 244 | } 245 | 246 | .clickable { 247 | cursor: pointer; 248 | } -------------------------------------------------------------------------------- /public/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/favicon.ico -------------------------------------------------------------------------------- /public/img/tiles/ball_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/ball_1.png -------------------------------------------------------------------------------- /public/img/tiles/ball_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/ball_2.png -------------------------------------------------------------------------------- /public/img/tiles/ball_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/ball_3.png -------------------------------------------------------------------------------- /public/img/tiles/ball_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/ball_4.png -------------------------------------------------------------------------------- /public/img/tiles/ball_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/ball_5.png -------------------------------------------------------------------------------- /public/img/tiles/ball_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/ball_6.png -------------------------------------------------------------------------------- /public/img/tiles/ball_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/ball_7.png -------------------------------------------------------------------------------- /public/img/tiles/ball_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/ball_8.png -------------------------------------------------------------------------------- /public/img/tiles/ball_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/ball_9.png -------------------------------------------------------------------------------- /public/img/tiles/bamboo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/bamboo_1.png -------------------------------------------------------------------------------- /public/img/tiles/bamboo_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/bamboo_2.png -------------------------------------------------------------------------------- /public/img/tiles/bamboo_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/bamboo_3.png -------------------------------------------------------------------------------- /public/img/tiles/bamboo_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/bamboo_4.png -------------------------------------------------------------------------------- /public/img/tiles/bamboo_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/bamboo_5.png -------------------------------------------------------------------------------- /public/img/tiles/bamboo_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/bamboo_6.png -------------------------------------------------------------------------------- /public/img/tiles/bamboo_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/bamboo_7.png -------------------------------------------------------------------------------- /public/img/tiles/bamboo_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/bamboo_8.png -------------------------------------------------------------------------------- /public/img/tiles/bamboo_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/bamboo_9.png -------------------------------------------------------------------------------- /public/img/tiles/character_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/character_1.png -------------------------------------------------------------------------------- /public/img/tiles/character_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/character_2.png -------------------------------------------------------------------------------- /public/img/tiles/character_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/character_3.png -------------------------------------------------------------------------------- /public/img/tiles/character_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/character_4.png -------------------------------------------------------------------------------- /public/img/tiles/character_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/character_5.png -------------------------------------------------------------------------------- /public/img/tiles/character_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/character_6.png -------------------------------------------------------------------------------- /public/img/tiles/character_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/character_7.png -------------------------------------------------------------------------------- /public/img/tiles/character_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/character_8.png -------------------------------------------------------------------------------- /public/img/tiles/character_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/character_9.png -------------------------------------------------------------------------------- /public/img/tiles/dragon_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/dragon_green.png -------------------------------------------------------------------------------- /public/img/tiles/dragon_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/dragon_red.png -------------------------------------------------------------------------------- /public/img/tiles/dragon_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/dragon_white.png -------------------------------------------------------------------------------- /public/img/tiles/flower_bamboo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/flower_bamboo.png -------------------------------------------------------------------------------- /public/img/tiles/flower_chrysanthemum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/flower_chrysanthemum.png -------------------------------------------------------------------------------- /public/img/tiles/flower_orchid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/flower_orchid.png -------------------------------------------------------------------------------- /public/img/tiles/flower_plum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/flower_plum.png -------------------------------------------------------------------------------- /public/img/tiles/season_fall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/season_fall.png -------------------------------------------------------------------------------- /public/img/tiles/season_spring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/season_spring.png -------------------------------------------------------------------------------- /public/img/tiles/season_summer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/season_summer.png -------------------------------------------------------------------------------- /public/img/tiles/season_winter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/season_winter.png -------------------------------------------------------------------------------- /public/img/tiles/wind_east.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/wind_east.png -------------------------------------------------------------------------------- /public/img/tiles/wind_north.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/wind_north.png -------------------------------------------------------------------------------- /public/img/tiles/wind_south.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/wind_south.png -------------------------------------------------------------------------------- /public/img/tiles/wind_west.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles/wind_west.png -------------------------------------------------------------------------------- /public/img/tiles_old/ball_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/ball_1.png -------------------------------------------------------------------------------- /public/img/tiles_old/ball_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/ball_2.png -------------------------------------------------------------------------------- /public/img/tiles_old/ball_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/ball_3.png -------------------------------------------------------------------------------- /public/img/tiles_old/ball_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/ball_4.png -------------------------------------------------------------------------------- /public/img/tiles_old/ball_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/ball_5.png -------------------------------------------------------------------------------- /public/img/tiles_old/ball_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/ball_6.png -------------------------------------------------------------------------------- /public/img/tiles_old/ball_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/ball_7.png -------------------------------------------------------------------------------- /public/img/tiles_old/ball_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/ball_8.png -------------------------------------------------------------------------------- /public/img/tiles_old/ball_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/ball_9.png -------------------------------------------------------------------------------- /public/img/tiles_old/bamboo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/bamboo_1.png -------------------------------------------------------------------------------- /public/img/tiles_old/bamboo_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/bamboo_2.png -------------------------------------------------------------------------------- /public/img/tiles_old/bamboo_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/bamboo_3.png -------------------------------------------------------------------------------- /public/img/tiles_old/bamboo_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/bamboo_4.png -------------------------------------------------------------------------------- /public/img/tiles_old/bamboo_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/bamboo_5.png -------------------------------------------------------------------------------- /public/img/tiles_old/bamboo_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/bamboo_6.png -------------------------------------------------------------------------------- /public/img/tiles_old/bamboo_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/bamboo_7.png -------------------------------------------------------------------------------- /public/img/tiles_old/bamboo_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/bamboo_8.png -------------------------------------------------------------------------------- /public/img/tiles_old/bamboo_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/bamboo_9.png -------------------------------------------------------------------------------- /public/img/tiles_old/character_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/character_1.png -------------------------------------------------------------------------------- /public/img/tiles_old/character_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/character_2.png -------------------------------------------------------------------------------- /public/img/tiles_old/character_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/character_3.png -------------------------------------------------------------------------------- /public/img/tiles_old/character_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/character_4.png -------------------------------------------------------------------------------- /public/img/tiles_old/character_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/character_5.png -------------------------------------------------------------------------------- /public/img/tiles_old/character_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/character_6.png -------------------------------------------------------------------------------- /public/img/tiles_old/character_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/character_7.png -------------------------------------------------------------------------------- /public/img/tiles_old/character_8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/character_8.png -------------------------------------------------------------------------------- /public/img/tiles_old/character_9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/character_9.png -------------------------------------------------------------------------------- /public/img/tiles_old/dragon_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/dragon_green.png -------------------------------------------------------------------------------- /public/img/tiles_old/dragon_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/dragon_red.png -------------------------------------------------------------------------------- /public/img/tiles_old/dragon_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/dragon_white.png -------------------------------------------------------------------------------- /public/img/tiles_old/flower_bamboo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/flower_bamboo.png -------------------------------------------------------------------------------- /public/img/tiles_old/flower_chrysanthemum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/flower_chrysanthemum.png -------------------------------------------------------------------------------- /public/img/tiles_old/flower_orchid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/flower_orchid.png -------------------------------------------------------------------------------- /public/img/tiles_old/flower_plum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/flower_plum.png -------------------------------------------------------------------------------- /public/img/tiles_old/season_fall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/season_fall.png -------------------------------------------------------------------------------- /public/img/tiles_old/season_spring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/season_spring.png -------------------------------------------------------------------------------- /public/img/tiles_old/season_summer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/season_summer.png -------------------------------------------------------------------------------- /public/img/tiles_old/season_winter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/season_winter.png -------------------------------------------------------------------------------- /public/img/tiles_old/wind_east.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/wind_east.png -------------------------------------------------------------------------------- /public/img/tiles_old/wind_north.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/wind_north.png -------------------------------------------------------------------------------- /public/img/tiles_old/wind_south.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/wind_south.png -------------------------------------------------------------------------------- /public/img/tiles_old/wind_west.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/img/tiles_old/wind_west.png -------------------------------------------------------------------------------- /public/js/global/underscore-min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.5.2 2 | // http://underscorejs.org 3 | // (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | (function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,h=e.reduce,v=e.reduceRight,g=e.filter,d=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,w=Object.keys,_=i.bind,j=function(n){return n instanceof j?n:this instanceof j?(this._wrapped=n,void 0):new j(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=j),exports._=j):n._=j,j.VERSION="1.5.2";var A=j.each=j.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a=j.keys(n),u=0,i=a.length;i>u;u++)if(t.call(e,n[a[u]],a[u],n)===r)return};j.map=j.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e.push(t.call(r,n,u,i))}),e)};var E="Reduce of empty array with no initial value";j.reduce=j.foldl=j.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduce===h)return e&&(t=j.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(E);return r},j.reduceRight=j.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduceRight===v)return e&&(t=j.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=j.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(E);return r},j.find=j.detect=function(n,t,r){var e;return O(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},j.filter=j.select=function(n,t,r){var e=[];return null==n?e:g&&n.filter===g?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&e.push(n)}),e)},j.reject=function(n,t,r){return j.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},j.every=j.all=function(n,t,e){t||(t=j.identity);var u=!0;return null==n?u:d&&n.every===d?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var O=j.some=j.any=function(n,t,e){t||(t=j.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};j.contains=j.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:O(n,function(n){return n===t})},j.invoke=function(n,t){var r=o.call(arguments,2),e=j.isFunction(t);return j.map(n,function(n){return(e?t:n[t]).apply(n,r)})},j.pluck=function(n,t){return j.map(n,function(n){return n[t]})},j.where=function(n,t,r){return j.isEmpty(t)?r?void 0:[]:j[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},j.findWhere=function(n,t){return j.where(n,t,!0)},j.max=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.max.apply(Math,n);if(!t&&j.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>e.computed&&(e={value:n,computed:a})}),e.value},j.min=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.min.apply(Math,n);if(!t&&j.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;ae||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={},i=null==r?j.identity:k(r);return A(t,function(r,a){var o=i.call(e,r,a,t);n(u,o,r)}),u}};j.groupBy=F(function(n,t,r){(j.has(n,t)?n[t]:n[t]=[]).push(r)}),j.indexBy=F(function(n,t,r){n[t]=r}),j.countBy=F(function(n,t){j.has(n,t)?n[t]++:n[t]=1}),j.sortedIndex=function(n,t,r,e){r=null==r?j.identity:k(r);for(var u=r.call(e,t),i=0,a=n.length;a>i;){var o=i+a>>>1;r.call(e,n[o])=0})})},j.difference=function(n){var t=c.apply(e,o.call(arguments,1));return j.filter(n,function(n){return!j.contains(t,n)})},j.zip=function(){for(var n=j.max(j.pluck(arguments,"length").concat(0)),t=new Array(n),r=0;n>r;r++)t[r]=j.pluck(arguments,""+r);return t},j.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},j.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=j.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},j.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},j.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=new Array(e);e>u;)i[u++]=n,n+=r;return i};var R=function(){};j.bind=function(n,t){var r,e;if(_&&n.bind===_)return _.apply(n,o.call(arguments,1));if(!j.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));R.prototype=n.prototype;var u=new R;R.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},j.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},j.bindAll=function(n){var t=o.call(arguments,1);if(0===t.length)throw new Error("bindAll must be passed function names");return A(t,function(t){n[t]=j.bind(n[t],n)}),n},j.memoize=function(n,t){var r={};return t||(t=j.identity),function(){var e=t.apply(this,arguments);return j.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},j.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},j.defer=function(n){return j.delay.apply(j,[n,1].concat(o.call(arguments,1)))},j.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var c=function(){o=r.leading===!1?0:new Date,a=null,i=n.apply(e,u)};return function(){var l=new Date;o||r.leading!==!1||(o=l);var f=t-(l-o);return e=this,u=arguments,0>=f?(clearTimeout(a),a=null,o=l,i=n.apply(e,u)):a||r.trailing===!1||(a=setTimeout(c,f)),i}},j.debounce=function(n,t,r){var e,u,i,a,o;return function(){i=this,u=arguments,a=new Date;var c=function(){var l=new Date-a;t>l?e=setTimeout(c,t-l):(e=null,r||(o=n.apply(i,u)))},l=r&&!e;return e||(e=setTimeout(c,t)),l&&(o=n.apply(i,u)),o}},j.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},j.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},j.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},j.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},j.keys=w||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)j.has(n,r)&&t.push(r);return t},j.values=function(n){for(var t=j.keys(n),r=t.length,e=new Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},j.pairs=function(n){for(var t=j.keys(n),r=t.length,e=new Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},j.invert=function(n){for(var t={},r=j.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},j.functions=j.methods=function(n){var t=[];for(var r in n)j.isFunction(n[r])&&t.push(r);return t.sort()},j.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},j.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},j.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)j.contains(r,u)||(t[u]=n[u]);return t},j.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]===void 0&&(n[r]=t[r])}),n},j.clone=function(n){return j.isObject(n)?j.isArray(n)?n.slice():j.extend({},n):n},j.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof j&&(n=n._wrapped),t instanceof j&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==String(t);case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;var a=n.constructor,o=t.constructor;if(a!==o&&!(j.isFunction(a)&&a instanceof a&&j.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c=0,f=!0;if("[object Array]"==u){if(c=n.length,f=c==t.length)for(;c--&&(f=S(n[c],t[c],r,e)););}else{for(var s in n)if(j.has(n,s)&&(c++,!(f=j.has(t,s)&&S(n[s],t[s],r,e))))break;if(f){for(s in t)if(j.has(t,s)&&!c--)break;f=!c}}return r.pop(),e.pop(),f};j.isEqual=function(n,t){return S(n,t,[],[])},j.isEmpty=function(n){if(null==n)return!0;if(j.isArray(n)||j.isString(n))return 0===n.length;for(var t in n)if(j.has(n,t))return!1;return!0},j.isElement=function(n){return!(!n||1!==n.nodeType)},j.isArray=x||function(n){return"[object Array]"==l.call(n)},j.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){j["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),j.isArguments(arguments)||(j.isArguments=function(n){return!(!n||!j.has(n,"callee"))}),"function"!=typeof/./&&(j.isFunction=function(n){return"function"==typeof n}),j.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},j.isNaN=function(n){return j.isNumber(n)&&n!=+n},j.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},j.isNull=function(n){return null===n},j.isUndefined=function(n){return n===void 0},j.has=function(n,t){return f.call(n,t)},j.noConflict=function(){return n._=t,this},j.identity=function(n){return n},j.times=function(n,t,r){for(var e=Array(Math.max(0,n)),u=0;n>u;u++)e[u]=t.call(r,u);return e},j.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var I={escape:{"&":"&","<":"<",">":">",'"':""","'":"'"}};I.unescape=j.invert(I.escape);var T={escape:new RegExp("["+j.keys(I.escape).join("")+"]","g"),unescape:new RegExp("("+j.keys(I.unescape).join("|")+")","g")};j.each(["escape","unescape"],function(n){j[n]=function(t){return null==t?"":(""+t).replace(T[n],function(t){return I[n][t]})}}),j.result=function(n,t){if(null==n)return void 0;var r=n[t];return j.isFunction(r)?r.call(n):r},j.mixin=function(n){A(j.functions(n),function(t){var r=j[t]=n[t];j.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(j,n))}})};var N=0;j.uniqueId=function(n){var t=++N+"";return n?n+t:t},j.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;j.template=function(n,t,r){var e;r=j.defaults({},r,j.templateSettings);var u=new RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(D,function(n){return"\\"+B[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=new Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,j);var c=function(n){return e.call(this,n,j)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},j.chain=function(n){return j(n).chain()};var z=function(n){return this._chain?j(n).chain():n};j.mixin(j),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];j.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];j.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),j.extend(j.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this); 6 | //# sourceMappingURL=underscore-min.map -------------------------------------------------------------------------------- /public/js/src/client.js: -------------------------------------------------------------------------------- 1 | /*global jQuery swig window shared io _ */ 2 | 3 | var INIT = (function ($, undefined) { 4 | 5 | var cfg = { 6 | previous_moves: [], 7 | clickEvent : /(iPad|iPhone)/i.test(navigator.userAgent) ? 'touchend' : 'click' 8 | }, 9 | board_tpl, 10 | socket, 11 | can_play = false; 12 | 13 | // touch navigation 14 | function pushMove() { 15 | if (cfg.previous_moves.length === 1) { 16 | cfg.previous_moves.shift(); 17 | } 18 | cfg.previous_moves.push(new Date().getTime()); 19 | } 20 | 21 | function clearMoves() { 22 | cfg.previous_moves = []; 23 | } 24 | 25 | function recentMove() { 26 | var current_time = new Date().getTime(); 27 | for (var i in cfg.previous_moves) { 28 | if ((current_time - cfg.previous_moves[i]) / 1000.0 < 0.4) { 29 | clearMoves(); 30 | return true; 31 | } 32 | } 33 | clearMoves(); 34 | return false; 35 | } 36 | 37 | $.fn.fastClick = function(selector, callback) { 38 | this.delegate(selector, cfg.clickEvent, function (evt) { 39 | if (!recentMove()) { 40 | callback.call(this, evt); 41 | } 42 | }); 43 | return this; 44 | }; 45 | 46 | function ajax(params) { 47 | params = $.extend( 48 | { 49 | type: 'GET', 50 | dataType: 'json' 51 | }, 52 | params 53 | ); 54 | return $.ajax(params); 55 | } 56 | 57 | function updateHand(data) { 58 | socket.emit('discard', data); 59 | } 60 | 61 | function hideTooltips() { 62 | $('.qtip').each(function(){ 63 | $(this).remove(); 64 | }); 65 | } 66 | 67 | var blinkInterval; 68 | function clearBlinkTitle() { 69 | clearInterval(blinkInterval); 70 | } 71 | function blinkTitle() { 72 | clearBlinkTitle(); 73 | var isOldTitle = true; 74 | var oldTitle = "Cock-eyed Mahjong"; 75 | var newTitle = "YOUR TURN"; 76 | function changeTitle() { 77 | document.title = isOldTitle ? oldTitle : newTitle; 78 | isOldTitle = !isOldTitle; 79 | } 80 | blinkInterval = setInterval(changeTitle, 1000); 81 | $(window).focus(function () { 82 | clearInterval(blinkInterval); 83 | $("title").text(oldTitle); 84 | }); 85 | } 86 | 87 | function markWinner(player_id) { 88 | var msg; 89 | if (player_id <= 1) { 90 | msg = 'Computer ' + player_id.toString() + ' is the winner!'; 91 | } else if (player_id == cfg.player._id) { 92 | msg = 'You are the winner!'; 93 | } else { 94 | var player = shared.getPlayer(cfg.players, player_id); 95 | msg = player.name + " is the winner!" 96 | } 97 | $('.player-' + player_id + ' a.tile-holder.hidden').removeClass('hidden'); 98 | $('.msg').text(msg); 99 | $('#play-again').removeClass('hide'); 100 | can_play = false; 101 | clearBlinkTitle(); 102 | } 103 | 104 | function notifyTurn(player_id) { 105 | var msg; 106 | if (player_id == cfg.player._id) { 107 | msg = 'Your turn'; 108 | } else if (shared.isComputer(player_id)) { 109 | msg = 'Computer ' + player_id + '\'s turn'; 110 | } else { 111 | msg = cfg.player_map[player_id].name + '\'s turn'; 112 | } 113 | $('.msg').text(msg); 114 | } 115 | 116 | function playSound(type) { 117 | var sound = $('#' + type).get(0); 118 | sound.pause(); 119 | sound.currentTime = 0; 120 | sound.play(); 121 | } 122 | 123 | function enablePlayer() { 124 | can_play = true; 125 | $('#player-tiles a.hidden').removeClass('hidden'); 126 | blinkTitle(); 127 | } 128 | 129 | function revealHiddenTiles() { 130 | $('a.tile-holder.hidden').removeClass('hidden'); 131 | } 132 | 133 | function renderBoard(data) { 134 | //TODO(gleitz): extend automatically 135 | // or only refresh parts of the page 136 | data.base_path = cfg.base_path; 137 | var rendered = swig.renderFile('board.html', data); 138 | $('#board').html(rendered); 139 | if (cfg.isOpen) { 140 | revealHiddenTiles(); 141 | } 142 | } 143 | 144 | function clearNotifications() { 145 | $('.msg').text(""); 146 | } 147 | 148 | function describeTile(tile_num) { 149 | return cfg.tile_info[tile_num]; 150 | } 151 | 152 | function initialize(local_cfg) { 153 | $.extend(cfg, local_cfg); 154 | } 155 | 156 | function initializeSwig() { 157 | // swig initialization 158 | board_tpl = $('#board_tpl').html(); 159 | if (board_tpl) { 160 | var templates = {'discard_tiles.html': $('#discard_tiles_tpl').html(), 161 | 'board.html': $('#board_tpl').html()}; 162 | swig.setDefaults({loader: swig.loaders.Memory(templates)}); 163 | } 164 | shared.augmentSwig(swig); 165 | } 166 | 167 | $(function () { 168 | initializeSwig(); 169 | 170 | if (cfg.isOpen) { 171 | revealHiddenTiles(); 172 | } 173 | 174 | if (cfg.mobile) { 175 | $('body').addClass('mobile'); 176 | } 177 | if (cfg.game && cfg.player && cfg.game.current_player_id == cfg.player._id) { 178 | enablePlayer(); 179 | } 180 | if (cfg.game && shared.exists(cfg.game.winner_id) && 181 | cfg.game.current_player_id == cfg.game.winner_id) { 182 | markWinner(cfg.game.winner_id); 183 | } else if (cfg.game && cfg.game.current_player_id && !cfg.isLobby) { 184 | notifyTurn(cfg.game.current_player_id); 185 | } 186 | if (cfg.isLobby) { 187 | var url = window.location.href; 188 | url.replace('play', 'game'); 189 | $('#start-label').val(url); 190 | } 191 | $('body').fastClick('a.start', function(evt) { 192 | evt.preventDefault(); 193 | socket.emit('start_game', {game_id: cfg.game_id}); 194 | return false; 195 | }); 196 | $('body').fastClick('#play-again', function(evt) { 197 | evt.preventDefault(); 198 | socket.emit('play_again', {game_id: cfg.game_id}); 199 | return false; 200 | }); 201 | $('body').fastClick('.qtip-pon', function(evt) { 202 | evt.preventDefault(); 203 | socket.emit('pon', {game_id: cfg.game_id}); 204 | hideTooltips(); 205 | return false; 206 | }); 207 | $('body').fastClick('.qtip-ron', function(evt) { 208 | evt.preventDefault(); 209 | socket.emit('ron', {game_id: cfg.game_id}); 210 | hideTooltips(); 211 | return false; 212 | }); 213 | $('body').fastClick('div.tile', function(evt) { 214 | evt.preventDefault(); 215 | var $this = $(this); 216 | if (!can_play) { 217 | return false; 218 | } 219 | if ($this.closest('div.side').length || 220 | $this.closest('.qtip').length) { 221 | // cannot throw tile you've pon'd, kan'd 222 | return false; 223 | } 224 | can_play = false; 225 | clearNotifications(); 226 | var $t = $(this), 227 | $a = $t.closest('a'), 228 | tile; 229 | if ($a.closest('#player-tiles').length > 0) { 230 | tile = $(this); 231 | } else { 232 | tile = $('#player-tiles').find('div.tile-'+$a.data('tile')+':last'); 233 | } 234 | clearBlinkTitle(); 235 | tile.fadeOut('slow', function() { 236 | updateHand({game_id: cfg.game_id, 237 | tile: $a.data('tile')}); 238 | }); 239 | }); 240 | 241 | var infoTimeout; 242 | $('body').on('mouseenter mouseleave', 'a.tile-holder', function (evt) { 243 | evt.preventDefault(); 244 | var $this = $(this), 245 | tile_num = $this.data('tile'); 246 | if ($this.hasClass('hidden')) { 247 | return false; 248 | } 249 | clearTimeout(infoTimeout); 250 | if (evt.type === 'mouseenter') { 251 | $('#msg-other').text(describeTile(tile_num)); 252 | } else { 253 | infoTimeout = setTimeout(function() { 254 | $('#msg-other').text(''); 255 | }, 300); 256 | } 257 | return false; 258 | }); 259 | 260 | $('body').on('mouseenter mouseleave', '#player-tiles a.tile-holder', function (evt) { 261 | evt.preventDefault(); 262 | var $this = $(this); 263 | if ($this.closest('div.side').length) { 264 | // disallow throwing tiles you've pon'd, kan'd 265 | return false; 266 | } 267 | if (evt.type === 'mouseenter') { 268 | if (can_play) { 269 | $this.stop().animate({marginTop: '-8px'}, 100); 270 | } 271 | } else { 272 | $this.stop().animate({marginTop: '4px'}, 300); 273 | } 274 | return false; 275 | }); 276 | 277 | // initialize socket.io 278 | var socket_resource = (cfg.base_path + '/socket.io').slice(1); 279 | socket = io.connect('/?token=' + 280 | cfg.socketIo.token, {resource: socket_resource}); 281 | socket.on('connect', function() { 282 | if (cfg.game_id) { 283 | socket.emit('room', cfg.game_id); 284 | } 285 | if (cfg.isLobby) { 286 | socket.emit('join_lobby', {game_id: cfg.game_id}); 287 | } 288 | }); 289 | socket.on('player_joined', function(data) { 290 | var player_str = []; 291 | _.each(data.players, function(player) { 292 | player_str.push($('
  • ', {text: player.name}).clone().wrap('
    ').parent().html()); 293 | }); 294 | $('.players').html(player_str.join(' ')); 295 | }); 296 | socket.on('update', function(data) { 297 | data.player = {_id: cfg.player._id, 298 | name: cfg.player.name}; 299 | hideTooltips(); 300 | renderBoard(data); 301 | if (shared.exists(data.game.winner_id) && 302 | data.game.current_player_id == data.game.winner_id) { 303 | markWinner(data.game.winner_id); 304 | } else { 305 | notifyTurn(data.game.current_player_id); 306 | } 307 | if (data.action) { 308 | playSound(data.action); 309 | } 310 | if (data.game.current_player_id == cfg.player._id) { 311 | enablePlayer(); 312 | } 313 | if (shared.exists(data.can_ron_player_id) && 314 | data.can_ron_player_id == cfg.player._id) { 315 | var $from_tile = $('div.player-' + data.can_ron_from_player_id + ' div.discard-tiles').find('a[data-tile="' + data.can_ron_tile + '"]').last(); 316 | var tile_str = shared.renderTile(data.can_ron_tile); 317 | $from_tile.qtip({ 318 | content: {text: tile_str + '

    Ron!

    ', 319 | button: true}, 320 | style: {classes: 'qtip-ron qtip-mahjong qtip-rounded qtip-shadow'}, 321 | position: { 322 | my: 'center center', // Position my top left... 323 | at: 'center center' // at the bottom right of... 324 | }, 325 | show: true 326 | }); 327 | } else if (shared.exists(data.can_pon_player_id) && 328 | data.can_pon_player_id == cfg.player._id) { 329 | var $from_tile = $('div.player-' + data.can_pon_from_player_id + ' div.discard-tiles').find('a[data-tile="' + data.can_pon_tile + '"]').last(); 330 | var tile_str = shared.renderTile(data.can_pon_tile); 331 | $from_tile.qtip({ 332 | content: {text: tile_str + '

    Pon!

    ', 333 | button: true}, 334 | style: {classes: 'qtip-pon qtip-mahjong qtip-rounded qtip-shadow'}, 335 | position: { 336 | my: 'center center', // Position my top left... 337 | at: 'center center' // at the bottom right of... 338 | }, 339 | events: { 340 | hide: function() { 341 | socket.emit('pon_dismiss', {game_id: cfg.game_id}); 342 | } 343 | }, 344 | show: true 345 | }); 346 | } 347 | if (!data.msg) { 348 | // TODO(gleitz): re-enable suggestions 349 | // $('#player-tiles').find('div.tile-' + data.recommended.discard_tile + ':last').closest('a').addClass('selected'); 350 | } 351 | }); 352 | socket.on('start_game', function(data) { 353 | window.location = cfg.base_path + '/game/' + data.game_id; 354 | }); 355 | socket.on('game_over', function() { 356 | $('.msg').text("Game over, man. No more tiles"); 357 | $('#play-again').removeClass('hide'); 358 | }); 359 | // highlight the current tile to throw 360 | if (cfg.isSimulation && !cfg.msg) { 361 | //TODO(gleitz): allow enabling this option 362 | // $('#player-tiles').find('div.tile-' + cfg.recommended.discard_tile + ':last').closest('a').addClass('selected'); 363 | } 364 | 365 | $('body').bind('touchmove', pushMove); 366 | setTimeout(function() { window.scrollTo(0, 1); }, 0); 367 | }); 368 | 369 | 370 | return { 371 | cfg: cfg, 372 | initialize: initialize 373 | }; 374 | 375 | })(jQuery); 376 | -------------------------------------------------------------------------------- /public/sound/jingle.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/sound/jingle.aiff -------------------------------------------------------------------------------- /public/sound/jingle.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/sound/jingle.mp3 -------------------------------------------------------------------------------- /public/sound/jingle.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/sound/jingle.ogg -------------------------------------------------------------------------------- /public/sound/jingle.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/sound/jingle.wav -------------------------------------------------------------------------------- /public/sound/tense-challenge.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/sound/tense-challenge.aiff -------------------------------------------------------------------------------- /public/sound/tense-challenge.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/sound/tense-challenge.mp3 -------------------------------------------------------------------------------- /public/sound/tense-challenge.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/sound/tense-challenge.ogg -------------------------------------------------------------------------------- /public/sound/tense-challenge.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/sound/tense-challenge.wav -------------------------------------------------------------------------------- /public/sound/tile-down.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/sound/tile-down.aiff -------------------------------------------------------------------------------- /public/sound/tile-down.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/sound/tile-down.mp3 -------------------------------------------------------------------------------- /public/sound/tile-down.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/sound/tile-down.ogg -------------------------------------------------------------------------------- /public/sound/tile-down.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gleitz/mahjong/60115a6177482045c0d3e848269ed1d5080f90b1/public/sound/tile-down.wav -------------------------------------------------------------------------------- /scripts/pre-commit.sh: -------------------------------------------------------------------------------- 1 | # pre-commit.sh 2 | git stash -q --keep-index 3 | ./scripts/run-tests.sh 4 | RESULT=$? 5 | git stash pop -q 6 | [ $RESULT -ne 0 ] && exit 1 7 | exit 0 8 | -------------------------------------------------------------------------------- /scripts/run-tests.sh: -------------------------------------------------------------------------------- 1 | grunt less && grunt test 2 | -------------------------------------------------------------------------------- /server/ami.js: -------------------------------------------------------------------------------- 1 | /*global require module */ 2 | 3 | /* 4 | * AMI: Artificial Mahjong Intelligence 5 | */ 6 | 7 | var mahjong = require('./mahjong'), 8 | mahjong_util = require('../shared/mahjong_util'), 9 | Q = require('q'), 10 | shanten = require('./shanten'), 11 | _ = require('underscore'); 12 | 13 | module.exports.checkMahjong = function(hand, callback) { 14 | //TODO(gleitz): check how to create callback 15 | // perhaps use setTimeout / nextTick 16 | var deferred = Q.defer(); 17 | var is_mahjong = mahjong.checkRegularMahjong(hand); 18 | deferred.resolve(is_mahjong); 19 | return deferred.promise.nodeify(callback) 20 | }; 21 | 22 | module.exports.getDiscard = function(hand, thrown, callback) { 23 | var deferred = Q.defer(); 24 | setTimeout(function() { 25 | var obj = mahjong.main(hand.slice(0)), 26 | recommended = mahjong.findBestDiscard(hand, _.union(thrown, obj.discard)), 27 | i, 28 | inter, 29 | best_waits, 30 | test_hand, 31 | shanten_number; 32 | if (obj.shanten === 0) { 33 | best_waits = mahjong.findBestDiscardWait(hand); 34 | if (best_waits.length > 0) { 35 | inter = _.intersection(best_waits, [recommended.discard]); 36 | if (inter.length === 0) { 37 | test_hand = hand.slice(0); 38 | test_hand[best_waits[0]] -= 1; 39 | shanten_number = shanten.shantenGeneralized(test_hand); 40 | if (shanten_number > 0) { 41 | best_waits = [recommended.discard]; 42 | } 43 | } else { 44 | best_waits = inter; 45 | } 46 | } else { 47 | best_waits = [recommended.discard]; 48 | } 49 | //TODO: take the one with the lowest score 50 | recommended.discard = best_waits[0]; 51 | } else if (obj.shanten === 1) { 52 | var best_discard = [], 53 | num_waits = 0; 54 | for (i=0; i num_waits) { 73 | best_discard = [throw_tile]; 74 | num_waits = total_waits.length; 75 | } 76 | } 77 | if (best_discard.length > 0) { 78 | inter = _.intersection(best_discard, [recommended.discard]); 79 | if (inter.length === 0) { 80 | test_hand = hand.slice(0); 81 | test_hand[best_discard[0]] -= 1; 82 | shanten_number = shanten.shantenGeneralized(test_hand); 83 | if (shanten_number > 1) { 84 | best_discard = [recommended.discard]; 85 | } 86 | } 87 | } else { 88 | best_discard = [recommended.discard]; 89 | } 90 | //TODO: take the one with the lowest score 91 | recommended.discard = best_discard[0]; 92 | } 93 | deferred.resolve({obj: obj, 94 | recommended: recommended}); 95 | }, 0); 96 | return deferred.promise.nodeify(callback) 97 | }; 98 | 99 | module.exports.shouldPon = function(seat, tile) { 100 | // if (module.exports.canPon(seat, tile)) { 101 | // return true; 102 | // } 103 | if (_.contains([21, 22, 23, 24], tile)) { 104 | return true; 105 | } 106 | return false; 107 | }; 108 | module.exports.canRon = function(seat, tile) { 109 | var new_hand = seat.hand.slice(0); 110 | new_hand[tile] += 1; 111 | try { 112 | return mahjong.checkRegularMahjong(new_hand); 113 | } 114 | catch (e) { 115 | return false; 116 | } 117 | }; 118 | module.exports.canPon = function(seat, tile) { 119 | if (!_.contains(seat.side, tile) && seat.hand[tile] >= 2) { 120 | return true; 121 | } 122 | return false; 123 | }; 124 | module.exports.canKan = function(seat, tile) { 125 | return false; 126 | //TODO(gleitz): fill this in 127 | if (!_.contains(seat.side, tile) && seat.hand[tile] >= 2) { 128 | return true; 129 | } 130 | return false; 131 | }; 132 | -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | /*global module */ 2 | 3 | module.exports = { 4 | SOCKET_IO_NAMESPACE: 'game', 5 | CRYPTO_AES_SECRET: 'fhqwhgads', 6 | EXPRESS_SESSION_SECRET: 'fhqwhgads', 7 | EXPRESS_COOKIE_SECRET: 'fhqwhgads', 8 | GITHUBHOOK_SECRET: 'fhqwhgads' 9 | }; 10 | -------------------------------------------------------------------------------- /server/connect-githubhook.js: -------------------------------------------------------------------------------- 1 | /*global module*/ 2 | var cgh = function (sites, callback) { 3 | var self = this; 4 | self.callback = callback; 5 | self.sites = sites; 6 | 7 | return function cgh(req, res, next) { 8 | if (!req.body || req.method !== 'POST') { 9 | return next(); 10 | } 11 | if (Object.keys(self.sites).indexOf(req.url) === -1 || 12 | req.header('x-github-event') !== 'push') { 13 | return next(); 14 | } 15 | var payload; 16 | payload = typeof req.body.payload === 'object' ? 17 | req.body.payload : JSON.parse(req.body.payload); 18 | var site_data = self.sites[req.url], 19 | site_url = site_data.url, 20 | site_branch = site_data.branch; 21 | if (payload.repository.url === site_url && 22 | payload.ref.indexOf(site_branch) != -1) { 23 | res.send({ result: 'ok' }, 200); 24 | callback(payload.repository.name, payload); 25 | } else { 26 | return next(); 27 | } 28 | } 29 | }; 30 | 31 | module.exports = cgh; 32 | -------------------------------------------------------------------------------- /server/crypto.js: -------------------------------------------------------------------------------- 1 | var config = require('./config'), 2 | crypto = require('crypto'); 3 | 4 | function encrypt(text){ 5 | var cipher = crypto.createCipher('aes-256-cbc', 6 | config.CRYPTO_AES_SECRET); 7 | var crypted = cipher.update(text, 'utf-8', 'hex'); 8 | crypted += cipher.final('hex'); 9 | return crypted; 10 | } 11 | 12 | function decrypt(text){ 13 | var decipher = crypto.createDecipher('aes-256-cbc', 14 | config.CRYPTO_AES_SECRET); 15 | var dec = decipher.update(text, 'hex', 'utf-8'); 16 | dec += decipher.final('utf-8'); 17 | return dec; 18 | } 19 | 20 | function md5(text){ 21 | var alg = crypto.createHash('md5'); 22 | var encry = alg.update(text).digest('hex'); 23 | return encry; 24 | } 25 | 26 | function sha256(text){ 27 | var alg = crypto.createHash('sha256'); 28 | var encry = alg.update(text).digest('hex'); 29 | return encry; 30 | } 31 | 32 | module.exports.encrypt = encrypt; 33 | module.exports.decrypt = decrypt; 34 | module.exports.md5 = md5; 35 | module.exports.sha256 = sha256; 36 | -------------------------------------------------------------------------------- /server/db.js: -------------------------------------------------------------------------------- 1 | /*global module process*/ 2 | 3 | /* 4 | * Master connection to MongoDB 5 | */ 6 | 7 | var MongoClient = require('mongodb').MongoClient; 8 | 9 | require('dotenv').config(); 10 | 11 | module.exports.init = function (callback) { 12 | // var user_and_pass = process.env.DB_USER + ":" + process.env.DB_PASS; 13 | MongoClient.connect("mongodb://localhost:27027", function(err, client) { 14 | module.exports.client = client.db('mahjong'); 15 | module.exports.players = module.exports.client.collection('players'); 16 | module.exports.games = module.exports.client.collection('games'); 17 | module.exports.session = client.db('session'); 18 | 19 | if (typeof(callback) == 'function') { 20 | callback(); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /server/mahjong.js: -------------------------------------------------------------------------------- 1 | /*global console require module */ 2 | 3 | /* 4 | * Functions for simulating a game of mahjong 5 | * and checking if a mahjong has been achieved. 6 | */ 7 | 8 | var shanten = require('./shanten'); 9 | var mahjong_util = require('../shared/mahjong_util'); 10 | 11 | var vals = mahjong_util.vals; 12 | 13 | var checkRegularMahjongNoPairHonor = function (hist, beg, end) { 14 | // for honors, check triplets only 15 | for (var i = beg; i <= end; i++) { 16 | if ((hist[i] % 3) !== 0) { 17 | return false; 18 | } 19 | } 20 | return true; 21 | }, 22 | checkRegularMahjongNoPairColor = function (init_hist, init_beg, init_end) { 23 | var queue = [], 24 | process = function (hist, index, beg, end) { 25 | if (mahjong_util.sum(hist.slice(beg, end+1)) === 0) { 26 | return true; 27 | } 28 | for (var i = beg; i <= end; i++) { 29 | if (mahjong_util.sum(hist.slice(index, i)) > 0) { 30 | return false; 31 | } 32 | var count = hist[i], 33 | copy; 34 | if (count > 0) { 35 | if (i + 2 <= end) { 36 | if (hist[i+1] > 0 && hist[i+2] > 0) { 37 | copy = hist.slice(0); 38 | copy[i] -= 1; 39 | copy[i+1] -= 1; 40 | copy[i+2] -= 1; 41 | queue.push([copy, index, i, end]); 42 | } 43 | } 44 | } 45 | if (count >= 3) { 46 | copy = hist.slice(0); 47 | copy[i] -= 3; 48 | queue.push([copy, index, i, end]); 49 | } 50 | } 51 | return false; 52 | }; 53 | queue.push([init_hist, init_beg, init_beg, init_end]); 54 | while (queue.length > 0) { 55 | var cur_item = queue[0], 56 | hist = cur_item[0], 57 | index = cur_item[1], 58 | beg = cur_item[2], 59 | end = cur_item[3]; 60 | var worked = process(hist, index, beg, end); 61 | if (worked) { 62 | return true; 63 | } 64 | queue.shift(); 65 | } 66 | return false; 67 | }, 68 | checkRegularMahjongNoPair = function (hist) { 69 | return checkRegularMahjongNoPairHonor(hist, mahjong_util.vals.honor_beg, mahjong_util.vals.honor_end) && 70 | checkRegularMahjongNoPairColor(hist, vals.pin_beg, vals.pin_end) && 71 | checkRegularMahjongNoPairColor(hist, vals.sou_beg, vals.sou_end); 72 | }, 73 | checkSevenPairs = function (hist) { 74 | for (var i = mahjong_util.vals.id_min; i <= mahjong_util.vals.id_max; i++) { 75 | if ((hist[i] % 2) !== 0) { 76 | return false; 77 | } 78 | } 79 | return true; 80 | }, 81 | checkRegularMahjong = function (hist) { 82 | if (mahjong_util.sum(hist) !== 14) { 83 | throw ("Cannot check mahjong with " + mahjong_util.sum(hist) + " tiles in hand."); 84 | } 85 | if (checkSevenPairs(hist)) { 86 | return true; 87 | } 88 | /* 89 | * enumerate through all possible pairs 90 | */ 91 | for (var i = 0; i < vals.count; i++) { 92 | if (hist[i] >= 2) { 93 | hist[i] -= 2; 94 | var pass = checkRegularMahjongNoPair(hist); 95 | hist[i] += 2; 96 | 97 | if (pass) { 98 | return true; 99 | } 100 | } 101 | } 102 | return false; 103 | }, 104 | 105 | findRegularMahjongAcc = function (hist, beg, end) { 106 | var mjs = [], 107 | queue = [], 108 | valid = false; 109 | var process = function(hist, sets, index, beg, end) { 110 | var copy; 111 | if (mahjong_util.sum(hist.slice(beg, end+1)) === 0) { 112 | mjs.push(sets); 113 | return true; 114 | } 115 | for (var i = beg; i <= end; i++) { 116 | if (mahjong_util.sum(hist.slice(index, i)) > 0) { 117 | return false; 118 | } 119 | var count = hist[i]; 120 | if (count > 0) { 121 | if (i + 2 <= end) { 122 | if (hist[i+1] > 0 && 123 | hist[i+2] > 0) { 124 | copy = hist.slice(0); 125 | copy[i] -= 1; 126 | copy[i+1] -= 1; 127 | copy[i+2] -= 1; 128 | var new_sets = sets.slice(0); 129 | new_sets.push([i, i+1, i+2]); 130 | queue.push([copy, new_sets, i, beg, end]); 131 | } 132 | } 133 | } 134 | if (count >= 3) { 135 | copy = hist.slice(0); 136 | copy[i] -= 3; 137 | sets.push([i, i, i]); 138 | queue.push([copy, sets, i, beg, end]); 139 | } 140 | } 141 | return false; 142 | }; 143 | queue.push([hist, [], beg, beg, end]); 144 | while (queue.length > 0) { 145 | var cur_item = queue[0], 146 | sets = cur_item[1], 147 | index = cur_item[2]; 148 | hist = cur_item[0]; 149 | beg = cur_item[3]; 150 | end = cur_item[4]; 151 | if (process(hist, sets, index, beg, end)) { 152 | valid = true; 153 | } 154 | queue.shift(); 155 | } 156 | return [mjs, valid]; 157 | }, 158 | findHonors = function (hist) { 159 | hist = hist.slice(0); 160 | var sets = []; 161 | for (var i = vals.honor_beg; i <= vals.honor_end; i++) { 162 | if (hist[i] >= 3) { 163 | sets.push([i, i, i]); 164 | hist[i] -= 3; 165 | } 166 | } 167 | if (mahjong_util.sum(hist.slice(vals.honor_beg, vals.honor_end + 1)) === 0) { 168 | return [[sets], true]; 169 | } 170 | return [[sets], false]; 171 | }, 172 | findRegularMahjong = function (hist) { 173 | /* 174 | * try all pairs 175 | */ 176 | for (var i = vals.id_min; i <= vals.id_max; i++) { 177 | if (hist[i] < 2) { 178 | continue; 179 | } 180 | var pair = i; 181 | hist[i] -= 2; 182 | var honor_result = findHonors(hist); 183 | var pin_result = findRegularMahjongAcc(hist, vals.pin_beg, vals.pin_end); 184 | var sou_result = findRegularMahjongAcc(hist, vals.sou_beg, vals.sou_end); 185 | if (honor_result[1] && pin_result[1] && sou_result[1]) { 186 | var hand = []; 187 | if (honor_result[0].length) { 188 | for (i=0; i 0) { 224 | var new_hist = hist.slice(0); 225 | new_hist[i]--; 226 | var shanten_number = shanten.shantenGeneralized(new_hist); 227 | if (shanten_number < best) { 228 | best = shanten_number; 229 | discard = [i]; 230 | } else if (shanten_number === best) { 231 | discard.push(i); 232 | } 233 | } 234 | } 235 | } 236 | return {msg: return_str, 237 | discard: discard, 238 | shanten: best}; 239 | }, 240 | addStreetScore = function (score, hist, beg, end) { 241 | var i; 242 | for (i = beg; i <= end - 1; i++) { 243 | score[i] += hist[i + 1] * 100; 244 | } 245 | 246 | for (i = beg; i <= end - 2; i++) { 247 | score[i] += hist[i + 2] * 10; 248 | } 249 | 250 | for (i = beg; i <= end - 3; i++) { 251 | score[i] += hist[i + 3] * 5; 252 | } 253 | 254 | for (i = beg + 1; i <= end; i++) { 255 | score[i] += hist[i - 1] * 100; 256 | } 257 | 258 | for (i = beg + 2; i <= end; i++) { 259 | score[i] += hist[i - 2] * 10; 260 | } 261 | 262 | for (i = beg + 3; i <= end; i++) { 263 | score[i] += hist[i - 3] * 5; 264 | } 265 | 266 | return score; 267 | }, 268 | findBestDiscard = function (hist, worst_tiles) { 269 | /* 270 | * score by combination with other tiles 271 | */ 272 | var score = [ 273 | 0,1,2,3,4,3,2,1,0, // central tiles are more valuable 274 | 0,1,2,3,4,3,2,1,0, 275 | 5,-1,-1,5, // winds are more valuable 276 | 5,5,5, // honors 277 | -1, -1], 278 | i; 279 | for (i = vals.id_min; i <= vals.id_max; i++) { 280 | var add = 1000 * (hist[i] - 1); 281 | score[i] += add; 282 | } 283 | 284 | score = addStreetScore (score, hist, vals.pin_beg, vals.pin_end); 285 | score = addStreetScore (score, hist, vals.sou_beg, vals.sou_end); 286 | if (worst_tiles) { 287 | for (i=0; i 0) { 299 | var v = score[i]; 300 | if (v < bestV) { 301 | bestV = v; 302 | bestI = i; 303 | } 304 | } 305 | } 306 | return {discard: bestI, 307 | score: score}; 308 | }, 309 | generateWall = function() { 310 | var wall = []; 311 | for (var plr = 0; plr < 4; plr++) { 312 | for (var i = vals.id_min; i <= vals.id_max; i++) { 313 | wall.push(i); 314 | } 315 | } 316 | wall.sort(function() {return 0.5 - Math.random();}); 317 | return wall; 318 | }, 319 | generateHand = function() { 320 | var hand = []; 321 | for (var i = vals.id_min; i<=vals.id_max; i++) { 322 | hand.push(0); 323 | } 324 | return hand.slice(0); 325 | }, 326 | deal = function (num_players) { 327 | var wall = generateWall(); 328 | var hands = []; 329 | for (var plr = 0; plr < num_players; plr++) { 330 | hands[plr] = generateHand(); 331 | for (var i = 0; i <= 13; i++) { //TODO: switch to < 13 when actually dealing 332 | hands[plr][wall.pop()] += 1; 333 | } 334 | } 335 | return {hands: hands, 336 | wall: wall}; 337 | }, 338 | getWaits = function (hist) { 339 | var count = mahjong_util.sum(hist), 340 | i, 341 | waits = []; 342 | if (count !== 13) { 343 | throw new Error("invalid tile count (" + count + ")"); 344 | } 345 | for (i=vals.id_min; i<=vals.id_max; i++) { 346 | var hand = hist.slice(0); 347 | hand[i] += 1; 348 | if (checkRegularMahjong(hand)) { 349 | for (var j=0; j<(4 - hist[i]); j++) { 350 | waits.push(i); 351 | } 352 | } 353 | } 354 | return waits; 355 | }, 356 | findBestDiscardWait = function (hist) { 357 | var waits = 0, 358 | i, 359 | discard = []; 360 | for (i=vals.id_min; i<=vals.id_max; i++) { 361 | if (hist[i] > 0) { 362 | var hand = hist.slice(0); 363 | hand[i] -= 1; 364 | var num_waits = getWaits(hand); 365 | if (num_waits.length === 0) { 366 | continue; 367 | } 368 | if (num_waits.length === waits) { 369 | discard.push(i); 370 | } else if (num_waits.length > waits) { 371 | waits = num_waits.length; 372 | discard = []; 373 | discard.push(i); 374 | } 375 | } 376 | } 377 | return discard; 378 | }; 379 | 380 | module.exports = { 381 | checkRegularMahjong: checkRegularMahjong, 382 | findRegularMahjong: findRegularMahjong, 383 | findRegularMahjongAcc: findRegularMahjongAcc, 384 | main: main, 385 | findBestDiscard: findBestDiscard, 386 | deal: deal, 387 | generateHand: generateHand, 388 | getWaits: getWaits, 389 | findBestDiscardWait: findBestDiscardWait 390 | }; 391 | -------------------------------------------------------------------------------- /server/models.js: -------------------------------------------------------------------------------- 1 | /*global require module */ 2 | 3 | /* 4 | * Models for the mahjong game 5 | */ 6 | 7 | 8 | /* Imports */ 9 | 10 | var _ = require('underscore'), 11 | db = require('./db'), 12 | ObjectID = require('mongodb').ObjectID, 13 | mahjong = require('./mahjong'), 14 | moniker = require('moniker'), 15 | Q = require('q'); 16 | 17 | // TODO: disable in production 18 | Q.longStackSupport = true; 19 | 20 | 21 | /* Database Objects (these have *._id) */ 22 | 23 | function Player(name) { 24 | this.name = name; 25 | } 26 | 27 | function Game(wall) { 28 | this.wall = wall; 29 | this.seats = []; 30 | this.current_player_id = null; 31 | this.winner_id = null; 32 | } 33 | 34 | 35 | /* JavaScript Objects */ 36 | 37 | function Seat(player_id) { 38 | this.player_id = player_id; 39 | this.hand = []; 40 | this.discard = []; 41 | this.last_tile = null; 42 | } 43 | 44 | 45 | /* Exported Modules */ 46 | 47 | module.exports.createGame = function(player_ids) { 48 | var insertGame = Q.nbind(db.games.insert, db.games); 49 | var deal = mahjong.deal(player_ids.length), 50 | hands = deal.hands, 51 | i; 52 | var game = new Game(deal.wall); 53 | game.current_player_id = player_ids[0]; 54 | for (i=0; i= shanten) { 29 | return false; 30 | } 31 | 32 | // 3-sets 33 | for (i = vals.buf_beg; i <= vals.buf_end_no_honors; i++) { 34 | if (buffered[i] >= 3) { 35 | // Remove 3-set 36 | copy = buffered.slice(0); 37 | copy[i] -= 3; 38 | // Remove any orphaned single tiles 39 | csingles = removeSingles(copy, i - 2, i + 2); 40 | queue.push([depth + 0, copy, singles + csingles, pairs]); 41 | 42 | if ((copy[i - 2] === 0) && 43 | (copy[i - 1] === 0) && 44 | (copy[i + 0] === 0) && 45 | (copy[i + 1] === 0) && 46 | (copy[i + 2] === 0)) { 47 | // If there are no neighbor tiles after removing the 48 | // 3-set, stop analyzing this branch 49 | return false; 50 | } 51 | } 52 | 53 | if ((buffered[i + 0] >= 1) && 54 | (buffered[i + 1] >= 1) && 55 | (buffered[i + 2] >= 1)) { 56 | 57 | copy = buffered.slice(0); 58 | copy[i + 0]--; 59 | copy[i + 1]--; 60 | copy[i + 2]--; 61 | csingles = removeSingles(copy, i - 2, i + 4); 62 | queue.push([depth + 0, copy, singles + csingles, pairs]); 63 | 64 | if ((copy[i - 2] === 0) && 65 | (copy[i - 1] === 0) && 66 | (copy[i + 0] === 0) && 67 | (copy[i + 1] === 0) && 68 | (copy[i + 2] === 0) && 69 | (copy[i + 3] === 0) && 70 | (copy[i + 4] === 0)) { 71 | return false; 72 | } 73 | } 74 | } 75 | 76 | 77 | /* 78 | * 2-sets 79 | */ 80 | for (i = vals.buf_beg; i <= vals.buf_end_no_honors; i++) { 81 | if (buffered[i] >= 2) { 82 | copy = buffered.slice(0); 83 | copy[i] -= 2; 84 | csingles = removeSingles(copy, i - 2, i + 2); 85 | queue.push([depth + 1, copy, singles + csingles - 1, pairs]); 86 | // } else { 87 | // for (discard = vals.buf_beg; discard <= vals.buf_end_no_honors; discard++) { 88 | // if (discard === i) { 89 | // continue; 90 | // } 91 | 92 | // if (buffered[discard] >= 1) { 93 | // copy = buffered.slice(0); 94 | // copy[i] -= 2; 95 | // copy[discard]--; 96 | // csingles = removeSingles(copy, i - 2, i + 2); 97 | // csingles += removeSingles (copy, discard - 2, discard + 2); 98 | // queue.push([depth + 1, copy, singles + csingles, pairs]); 99 | // } 100 | // } 101 | // } 102 | } 103 | 104 | if (buffered[i] >= 2) { 105 | copy = buffered.slice(0); 106 | copy[i] -= 2; 107 | csingles = removeSingles(copy, i - 2, i + 2); 108 | queue.push([depth, copy, singles + csingles, pairs + 1]); 109 | } 110 | 111 | if ((buffered[i + 0] >= 1) && (buffered[i + 1] >= 1)) { 112 | if (singles > 0) { 113 | copy = buffered.slice(0); 114 | copy[i + 0] -= 1; 115 | copy[i + 1] -= 1; 116 | csingles = removeSingles (copy, i - 2, i + 3); 117 | queue.push([depth + 1, copy, singles + csingles - 1, pairs]); 118 | } else { 119 | for (discard = vals.buf_beg; discard <= vals.buf_end_no_honors; discard++) { 120 | if (discard === i + 0) { 121 | continue; 122 | } 123 | if (discard === i + 1) { 124 | continue; 125 | } 126 | 127 | if (buffered[discard] >= 1) { 128 | copy = buffered.slice(0); 129 | copy[i + 0] -= 1; 130 | copy[i + 1] -= 1; 131 | copy[discard]--; 132 | csingles = removeSingles(copy, i - 2, i + 3); 133 | csingles += removeSingles(copy, discard - 2, discard + 2); 134 | queue.push([depth + 1, copy, singles + csingles, pairs]); 135 | } 136 | } 137 | } 138 | } 139 | 140 | if ((buffered[i + 0] >= 1) && (buffered[i + 2] >= 1)) { 141 | if (singles > 0) { 142 | copy = buffered.slice(0); 143 | copy[i + 0] -= 1; 144 | copy[i + 2] -= 1; 145 | csingles = removeSingles(copy, i - 2, i + 4); 146 | queue.push([depth + 1, copy, singles + csingles - 1, pairs]); 147 | } else { 148 | for (discard = vals.buf_beg; discard <= vals.buf_end_no_honors; discard++) { 149 | if (discard === i + 0) { 150 | continue; 151 | } 152 | if (discard === i + 2) { 153 | continue; 154 | } 155 | 156 | if (buffered[discard] >= 1) { 157 | copy = buffered.slice(0); 158 | copy[i + 0] -= 1; 159 | copy[i + 2] -= 1; 160 | copy[discard]--; 161 | csingles = removeSingles(copy, i - 2, i + 4); 162 | csingles += removeSingles(copy, discard - 2, discard + 2); 163 | queue.push([depth + 1, copy, singles + csingles, pairs]); 164 | } 165 | } 166 | } 167 | } 168 | } 169 | var singles_left = m_util.sum(buffered) + singles; 170 | if (pairs == 1 && singles_left == 2) { 171 | var pos; 172 | for (pos = vals.buf_beg; pos <= vals.buf_end_no_honors; pos++) { 173 | if ((buffered[pos] == 1 && buffered[pos+1] == 1) || 174 | (buffered[pos] == 1 && buffered[pos+2] == 1)) { 175 | pairs--; 176 | singles = singles - 2; 177 | break; 178 | } 179 | } 180 | } 181 | while (pairs > 0 && singles_left > 0) { 182 | pairs--; 183 | singles_left--; 184 | depth++; 185 | } 186 | 187 | while (pairs > 3) { 188 | pairs -=3; 189 | depth += 2; 190 | } 191 | 192 | if (pairs > 0) { 193 | if (pairs != 2) { 194 | return false; 195 | } 196 | depth += 0; 197 | } 198 | 199 | depth += parseInt((singles_left - 1) * 2 / 3, 10); 200 | 201 | if (depth < shanten) { 202 | // Update the global shanten number 203 | shanten = depth; 204 | } 205 | }; 206 | queue.push([depth, buffered, singles, pairs]); 207 | while (queue.length > 0) { 208 | var cur_item = queue[0]; 209 | process(cur_item[0], cur_item[1], cur_item[2], cur_item[3]); 210 | queue.shift(); 211 | } 212 | return shanten; 213 | }, 214 | shantenGeneralized = function (hist) { 215 | var shanten = m_util.sum(hist) * 2 / 3, 216 | buffered = m_util.translateToBufferedNoHonors(hist), 217 | honors_result = calcHonors(hist.slice(0)), 218 | pairs = honors_result[0], 219 | singles = honors_result[1]; 220 | singles += removeSingles(buffered, vals.buf_beg, vals.buf_end_no_honors); 221 | return shantenSimulation(0, shanten, buffered, singles, pairs); 222 | }, 223 | calcHonors = function (hist) { 224 | var singles = 0, 225 | pairs = 0; 226 | 227 | for (var i = vals.honor_beg; i <= vals.honor_end; i++) { 228 | // always remove triplets 229 | if (hist[i] >= 3) { 230 | hist[i] -= 3; 231 | } 232 | 233 | // extract pairs 234 | if (hist[i] === 2) { 235 | hist[i] = 0; 236 | pairs += 1; 237 | } 238 | 239 | // remove singles 240 | else if (hist[i] === 1) { 241 | hist[i] = 0; 242 | singles += 1; 243 | } 244 | } 245 | return [pairs, singles]; 246 | }, 247 | removeSingles = function (buffered, beg, end) { 248 | // check if there are any isolated 249 | // single tiles from beg to end 250 | var count = 0, 251 | i; 252 | 253 | for (i = beg; i <= end; i++) { 254 | if (buffered[i] !== 1) { 255 | continue; 256 | } 257 | 258 | if (buffered[i - 1] > 0) { 259 | continue; 260 | } 261 | 262 | if (buffered[i - 2] > 0) { 263 | continue; 264 | } 265 | 266 | if (buffered[i + 1] > 0) { 267 | continue; 268 | } 269 | 270 | if (buffered[i + 2] > 0) { 271 | continue; 272 | } 273 | 274 | buffered[i]--; 275 | count++; 276 | } 277 | 278 | return count; 279 | }; 280 | 281 | module.exports = { 282 | shantenGeneralized: shantenGeneralized 283 | } 284 | -------------------------------------------------------------------------------- /shared/mahjong_util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Utilities for managing a game of mahjong. 3 | * Note that this specific variant, often played 4 | * in Japan is called Three-Player mahjong or 5 | * "Cock-Eyed" mahjong. 6 | */ 7 | 8 | // Establish the root object, `window` in the browser, or `exports` on the server. 9 | var root = this; 10 | // Create a safe reference to the mahjong_util object for use below. 11 | var mahjong_util = function(obj) { 12 | if (obj instanceof mahjong_util) return obj; 13 | if (!(this instanceof mahjong_util)) return new mahjong_util(obj); 14 | this.mahjong_utilwrapped = obj; 15 | }; 16 | // Export the mahjong_util object for **Node.js**, with 17 | // backwards-compatibility for the old `require()` API. If we're in 18 | // the browser, add `mahjong_util` as a global object via a string identifier, 19 | // for Closure Compiler "advanced" mode. 20 | if (typeof exports !== 'undefined') { 21 | if (typeof module !== 'undefined' && module.exports) { 22 | exports = module.exports = mahjong_util; 23 | } 24 | exports.mahjong_util = mahjong_util; 25 | } else { 26 | root.mahjong_util = mahjong_util; 27 | } 28 | 29 | // Import underscore if in **Node.js** 30 | // (import automatically available if in browser) 31 | if (typeof require !== 'undefined') { 32 | var _ = require('underscore'); 33 | } 34 | 35 | var debug = function() { 36 | false && console.log && console.log.apply(console, arguments); 37 | }, 38 | honors = [ 39 | ['E', 'East'], 40 | ['S', 'South'], 41 | ['W', 'West'], 42 | ['N', 'North'], 43 | ['B', 'White Dragon'], 44 | ['G', 'Green Dragon'], 45 | ['R', 'Red Dragon'], 46 | ['1', '1 Crack'], 47 | ['9', '9 Crack'] 48 | ], 49 | basic_honors = _.map(honors, function(honor) { 50 | return honor[0]; 51 | }), 52 | colors = [ 53 | ['Pin', 'Pin (dot)'], 54 | ['Sou', 'Sou (bamboo)'], 55 | ['Honor', 'Honor'] 56 | ], 57 | vals = { 58 | // id = value + (9 * color); 59 | // (buffered values have two extra spaces 60 | // on either side of the pins and sous 61 | // name id buffered 62 | // pin 00-08 02-10 63 | // sou 09-17 13-21 64 | // honors 18-26 24-32 65 | id_min: 0, 66 | color_beg: 0, 67 | pin_beg: 0, 68 | pin_end: 8, 69 | sou_beg: 9, 70 | sou_end: 17, 71 | color_end: 17, 72 | honor_beg: 18, 73 | honor_end: 26, 74 | id_max: 26, 75 | count: 26 + 1, 76 | buf_beg: 2, 77 | buf_end_no_honors: 21, 78 | buf_end: 32 79 | }, 80 | arrayOf = function(n, times) { 81 | // return an array of the value of 'n' repeated 'times' number of time 82 | return Array.apply(null, new Array(times)).map(Number.prototype.valueOf,n); 83 | }, 84 | getColor = function (tile, is_verbose) { 85 | // return color at specific position 86 | if (tile < 0) { 87 | return null; 88 | } 89 | tile = tile - (tile % 9); 90 | tile /= 9; 91 | var color_tuple = colors[tile]; 92 | if (!color_tuple) { 93 | return null; 94 | } 95 | return is_verbose ? color_tuple[1] : color_tuple[0]; 96 | }, 97 | getValue = function (tile) { 98 | // return value at specific position 99 | return tile % 9; 100 | }, 101 | getHonor = function (tile, is_verbose) { 102 | // return honor at specific position 103 | var value = getValue(tile); 104 | if (is_verbose) { 105 | return honors[value][1]; 106 | } else { 107 | return honors[value][0]; 108 | } 109 | }, 110 | isHonor = function (tile) { 111 | return tile >= vals.honor_beg; 112 | }, 113 | isPin = function (tile) { 114 | return tile >= vals.pin_beg && tile <= vals.pin_end; 115 | }, 116 | isSou = function (tile) { 117 | return tile >= vals.sou_beg && tile <= vals.sou_end; 118 | }, 119 | toString = function (tile, is_verbose) { 120 | if (isHonor(tile)) { 121 | return getHonor(tile, is_verbose); 122 | } else { 123 | return (getValue(tile) + 1) + ' ' + getColor(tile, is_verbose); 124 | } 125 | }, 126 | translateToBufferedNoHonors = function (hist) { 127 | // Convert a histogram of tiles into the buffered 128 | // representation which places extra space on the end 129 | // of the pins and sous 130 | hist = hist.slice(0, vals.honor_beg); 131 | hist.splice(vals.honor_beg, 0, 0, 0); 132 | hist.splice(vals.sou_beg, 0, 0, 0); 133 | hist.splice(vals.pin_beg, 0, 0, 0); 134 | return hist; 135 | }, 136 | translateFromBufferedNoHonors = function (buffered) { 137 | var hist = buffered.slice(0); 138 | hist.splice(0, 2); 139 | hist.splice(9, 2); 140 | hist.splice(18, 2); 141 | return hist; 142 | }, 143 | toHandString = function(hist) { 144 | // Convert from a histogram 145 | // [3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,2,0] 146 | // into a string representation of the hand 147 | // '111222333p NNN11' 148 | var i, 149 | pins = [], 150 | sous = [], 151 | s_honors = [], 152 | handStr = ''; 153 | for (i=0; i'); 66 | tile_compiled = swig.compile(''); 67 | 68 | function renderTiles(seat, is_hidden) { 69 | var hist = seat.hand.slice(0), 70 | side = (seat.side && seat.side.slice(0)) || [], 71 | last_tile = seat.last_tile, 72 | buffer = [], 73 | side_buffer = [], 74 | last_tile_str, 75 | i; 76 | _.each(side, function(tile_num) { 77 | side_buffer.push(shared.renderTile(tile_num, false)); 78 | hist[tile_num] -= 1; 79 | }); 80 | for (i=0; i'; 106 | return buffer_str + side_str; 107 | } 108 | 109 | function renderHand(game, seat, player_id) { 110 | var is_hidden = seat.player_id != player_id; 111 | if (shared.exists(game.winner_id) && game.winner_id == seat.player_id) { 112 | is_hidden = false; 113 | } 114 | return renderTiles(seat, is_hidden); 115 | } 116 | 117 | function renderDiscard(seat) { 118 | return _.reduce(seat.discard, function(memo, tile_num) { 119 | return memo + shared.renderTile(tile_num); 120 | }, ''); 121 | } 122 | 123 | function variableParser(str, line, parser, types, stack, options) { 124 | parser.on(types.VAR, function (token) { 125 | // get the root object (e.g. player.name -> player) 126 | var top_obj = token.match.split('.')[0]; 127 | // check to see root object exists in the local scope 128 | // otherwise, look in the global scope 129 | this.out.push('(typeof ' + top_obj + ' === "undefined" ? ' + 130 | '_ctx.' + token.match + ' : ' + token.match + 131 | ')'); 132 | return; 133 | }); 134 | return true; 135 | } 136 | 137 | swig.setFilter('tile', shared.renderTile); 138 | swig.setFilter('isComputer', shared.isComputer); 139 | swig.setExtension('renderHand', renderHand); 140 | swig.setExtension('renderDiscard', renderDiscard); 141 | swig.setTag('renderHand', 142 | variableParser, 143 | function(compiler, args, content, parents, options, blockName) { 144 | return '_output += _ext.renderHand(' + 145 | args[0] + ',' + args[1] + ',' + args[2] + ');'; 146 | }, 147 | false); 148 | swig.setTag('renderDiscard', 149 | variableParser, 150 | function(compiler, args, content, parents, options, blockName) { 151 | return '_output += _ext.renderDiscard(' + 152 | args[0] + ');'; 153 | }, 154 | false); 155 | }; 156 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /*global describe it */ 2 | 3 | var shanten = require('../server/shanten'); 4 | var mahjong = require('../server/mahjong'); 5 | var m_util = require("../shared/mahjong_util"); 6 | var assert = require("assert"); 7 | var util = require("util"); 8 | var _ = require('underscore'); 9 | 10 | describe('utility functions', function(){ 11 | describe('getColor', function(){ 12 | it('should return the correct color', function(){ 13 | assert.equal(m_util.getColor(0), 'Pin'); 14 | assert.equal(m_util.getColor(8), 'Pin'); 15 | assert.equal(m_util.getColor(9), 'Sou'); 16 | assert.equal(m_util.getColor(17), 'Sou'); 17 | assert.equal(m_util.getColor(18), 'Honor'); 18 | assert.equal(m_util.getColor(26), 'Honor'); 19 | assert.equal(m_util.getColor(27), null); 20 | assert.equal(m_util.getColor(-1), null); 21 | }); 22 | }); 23 | describe('toTileString', function(){ 24 | it('should return the correct tile string', function(){ 25 | var hands = [['123456789p 123s WW', 26 | [1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0]], 27 | ['123s 123456789p WW', 28 | [1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0]], 29 | ['111222333p NNN11', 30 | [3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,2,0]], 31 | ['1p 123456789s BGR9', 32 | [1,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,0,1]], 33 | ['123456789s 1p BGR9', 34 | [1,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,0,1]], 35 | ['NNSSWWEEEBGBGR', 36 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,2,2,2,2,1,0,0]] 37 | ]; 38 | _.each(hands, function(elem) { 39 | var hand_string = elem[0], 40 | hand_list_oracle = elem[1], 41 | hand_list = m_util.toTileString(hand_string) 42 | assert(hand_list_oracle.equals(hand_list), 43 | util.format("Hand: %s, Expected: %s, Actual: %s", hand_string, hand_list_oracle, hand_list)); 44 | }); 45 | }); 46 | it('should return the correct hand string', function(){ 47 | var hands = [['123456789p 123s WW', 48 | [1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0]], 49 | ['111222333p NNN11', 50 | [3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,2,0]], 51 | ['1p 123456789s BGR9', 52 | [1,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,0,1]], 53 | ['EEESSWWNNBBGGR', 54 | [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,2,2,2,2,1,0,0]] 55 | ]; 56 | _.each(hands, function(elem) { 57 | var hand_string_oracle = elem[0], 58 | hand_list = elem[1], 59 | hand_string = m_util.toHandString(hand_list); 60 | assert(hand_string_oracle == hand_string, 61 | util.format("Expected: %s, Actual: %s", hand_string_oracle, hand_string)); 62 | }); 63 | }); 64 | }); 65 | }); 66 | 67 | describe('shanten functions', function(){ 68 | describe('shantenCalculation', function(){ 69 | it('should return correct shanten number', function(){ 70 | var hands = [ 71 | ['25s 34444589p WWE', 2], 72 | ['12333345s EEE SS', 0], 73 | ['1113335557779s', 0], 74 | ['258p 258s ESWN19RG', 8], 75 | ['EEESSSWWWGGRR', 0], 76 | ['1s EEESSSBBGGRR', 1], 77 | ['1289s 111555999p', 1], 78 | ['12333s 89p EEESSS', 0], 79 | ['12333s WWEEESSS', 0], 80 | ['1122333s EEESSS', 0], 81 | ['1233334s EEESSS', 0], 82 | ['67p 2334446s ESWN', 3], 83 | ['1124455567799s', 2], 84 | ['1122236677888s', 1], 85 | ['1133445667788s', 0] 86 | ]; 87 | _.each(hands, function(elem) { 88 | var hand_string = elem[0], 89 | shanten_num_oracle = elem[1], 90 | shanten_num = shanten.shantenGeneralized(m_util.toTileString(hand_string)); 91 | assert(shanten_num_oracle == shanten_num, 92 | util.format("Hand: %s, Expected: %s, Actual: %s", hand_string, shanten_num_oracle, shanten_num)); 93 | }); 94 | }); 95 | }); 96 | }); 97 | 98 | describe('mahjong functions', function(){ 99 | describe('mahjongCalculation', function(){ 100 | it('should return if hand is a mahjong', function(){ 101 | var hands = [ 102 | ['12333345s EEESSS', true], 103 | ['11133355577799s', true], 104 | ['EEESSSWWWGGRRR', true], 105 | ['12333s 789p EEESSS', true], 106 | ['12333s WWWEEESSS', true], 107 | ['11223333s EEESSS', true], 108 | ['11223335s EEESSS', false], 109 | ['11334455667788s', true], 110 | ['115577s 224466p RR', true], 111 | ['115577s 224466p GR', false] 112 | ]; 113 | _.each(hands, function(elem) { 114 | var hand_string = elem[0], 115 | mahjong_oracle = elem[1], 116 | is_mahjong = mahjong.checkRegularMahjong(m_util.toTileString(hand_string)); 117 | assert(mahjong_oracle == is_mahjong, 118 | util.format("Hand: %s, Expected: %s, Actual: %s", hand_string, mahjong_oracle, is_mahjong)); 119 | }); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /views/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cock-eyed Mahjong{% block titleExtra %}{% endblock %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block metaExtra %} 11 | {% endblock %} 12 | 13 | 14 | {% block body %} 15 | {% endblock %} 16 | 17 | 18 | -------------------------------------------------------------------------------- /views/game.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block metaExtra %} 4 | 7 | 10 | 11 | {% endblock %} 12 | 13 | {% block body %} 14 |
    15 | {% if game_id %} 16 | {% include "./partials/board.html" %} 17 | {% else %} 18 |

    Okaerinasai

    19 |
    Shuffling...
    20 | {% endif %} 21 |
    22 |
    23 | 29 | 35 | 41 |
    42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /views/home.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block metaExtra %} 4 | 5 | {% endblock %} 6 | 7 | {% block body %} 8 |
    9 |

    Konnichiwa and Welcome to Cock-eyed Mahjong

    10 |

    {{ 24|tile|safe }} Create a New Game

    11 | 15 |
    16 |

    {{ 24|tile|safe }} Current Games

    17 | 22 |
    23 |

    {{ 24|tile|safe }} Learn the Rules

    24 |

    Cock-eyed Mahjong (or Three-Player Mahjong) is a fast-paced Japanese variation on the classic Japanese version of the game.
    For the full set of rules head over to
    japanese-mahjong.com.

    25 |
    26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /views/lobby.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block metaExtra %} 4 | 5 | {% endblock %} 6 | 7 | {% block body %} 8 |
    9 |

    Current Players

    10 |
      11 | {% for player in players %} 12 |
    • {{ player.name }}
    • 13 | {% endfor %} 14 |
    15 |
    To invite a friend, send them this URL:
    16 |
    17 | 18 |
    19 |
    and then click Start.
    20 |
    21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /views/partials/board.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ...play again 4 | 5 | {{ player.name }} | restart 6 | {% for seat in game.seats %} 7 | {% if seat.player_id != player._id %} 8 |
    9 |

    {% if seat.player_id|isComputer %}Computer {{ seat.player_id }}{% else %}{{ player_map[seat.player_id].name }}{% endif %}

    10 |
    {% renderHand game seat player._id %}
    11 | {% include "./discard_tiles.html" with {seat: seat} %} 12 |
    13 | {% endif %} 14 | {% endfor %} 15 | {% for seat in game.seats %} 16 | {% if seat.player_id == player._id %} 17 |
    18 |
    19 | 20 | 21 |
    22 | {% include "./discard_tiles.html" with {seat: seat} %} 23 |
    {% renderHand game seat player._id %}
    24 |
    25 | {% endif %} 26 | {% endfor %} 27 | -------------------------------------------------------------------------------- /views/partials/board_simulation.html: -------------------------------------------------------------------------------- 1 | {{ player.name }} | restart 2 |

    Hand Contains

    3 |
    {{ rendered_tiles|safe }}
    4 | {% if msg %} 5 |

    {{ msg }}

    6 | play again! 7 | {% else %} 8 |

    AMI suggests {{ shanten }} shanten

    9 |
    10 |
    11 | {{ recommended.discard_tile|tile|safe }} 12 |
    13 | {% for tile_num in discards %} 14 | {{ tile_num|tile|safe }} 15 | {% endfor %} 16 |
    17 |
    18 |
    19 | {% endif %} 20 | -------------------------------------------------------------------------------- /views/partials/discard_tiles.html: -------------------------------------------------------------------------------- 1 |
    {% renderDiscard seat %}
    2 | -------------------------------------------------------------------------------- /views/partials/tile.html: -------------------------------------------------------------------------------- 1 |
    2 | --------------------------------------------------------------------------------