Skip to content

Instantly share code, notes, and snippets.

@bigomega
Last active February 7, 2016 16:44
Show Gist options
  • Save bigomega/52cfa6835a675d1edaa8 to your computer and use it in GitHub Desktop.
Save bigomega/52cfa6835a675d1edaa8 to your computer and use it in GitHub Desktop.
Tic Tac Toe
/node_modules
npm-debug.log

There's ONE dependency for this, readline-sync for synchronous readline from stdin (you know because callback hell is a real place)

assuming you got npm

Install

npm install

Play

npm start
Files
	constants.js
	tic-tac-toe.js
	| |--> game.js
	|       |--> point.js
	|--> bot.js

'use strict';
const constants = require('./constants');
const botConfig = {
me: {
normal: 1,
win: Infinity,
},
enemy: {
normal: 1,
win: 1000000
},
};
module.exports = class Bot {
constructor(game){
this.game = game;
}
getMove(player){
let potentialPoint;
const possibleSlots = this._possibleSlots;
// Can improve efficiency by caching the getscores value
potentialPoint = possibleSlots.reduce((p1, p2) =>
this._getScores(p2, player).total > this._getScores(p1, player).total ? p2 : p1
);
console.log(`\nPlayer-${player} (BOT) placed at (${potentialPoint.x}, ${potentialPoint.y})\n`)
return [potentialPoint.x, potentialPoint.y];
}
_getScores(point, player){
let enemyScores = [],
myScore = 0;
Object.keys(this.game.playerPositions).forEach(k => {// For each player
point.player = k;
let potential = point.potential;
const who = k == player ? 'me' : 'enemy'; //Need type casting
// For all direction lines (4 not 8)
const localScore = constants.lineDirections.reduce((totalScore, ldir) => {
const length = potential[ldir].length;
let dirScore;
// Todo:
// Should consider 1 less than win but open end for enemy
if(length >= this.game.winCount) // about to win/lose
dirScore = botConfig[who].win;
else
dirScore = length * botConfig[who].normal;
return totalScore + dirScore;
}, 0);
if(who === 'me')
myScore = localScore;
else
enemyScores.push(localScore);
});
// remember to reset the point
point.player = 0;
return {
total: myScore + enemyScores.reduce((t, s) => t + s),
myScore,
enemyScores,
};
}
get _possibleSlots(){
let possibleSlots = [],
point;
possibleSlots.has = (point) => possibleSlots.reduce((truth, pt) => truth || point.posEquals(pt), false)
if(this.game.gravity){
this.game.board.forEach(column => { // for each columns in board
if(!column[0].player){
let y = 0;
// loop till value is found or limit exceeded
for(; y < this.game.height && !column[y].player; y++) { };
possibleSlots.push(column[y - 1]);
}
});
} else {
Object.keys(this.game.playerPositions).forEach(player => // for each player
this.game.playerPositions[player].forEach(point => // for each point he placed
constants.directions.forEach(dir => // for each diretion around it
point[dir] !== null && !point[dir].player && !possibleSlots.has(point[dir]) && possibleSlots.push(point[dir])
)
)
);
}
return possibleSlots;
}
}
exports.directions = ['n', 'e', 'w', 's', 'ne', 'nw', 'se', 'sw'];
exports.lineDirections = ['vertical', 'horizontal', 'diagonal_ne', 'diagonal_nw'];
exports.playerKeys = [
{text: '_', color: 'grey'},
{text: 'X', color: 'green'},
{text: 'O', color: 'blue'},
];
exports.colorCodes = {
grey: 30,
red: 31,
green: 32,
orange: 33,
blue: 34,
pink: 35,
cyan: 36,
white: 37,
};
'use strict';
const Point = require('./point');
const constants = require('./constants');
const colorLog = (playerKey, colorOverride) => {
// to me, colors matter :D
const cc = constants.colorCodes[colorOverride || playerKey.color] || 0;
return `\x1b[${cc}m${playerKey.text}\x1b[0m`;
};
module.exports = class Game {
constructor(length, height, winCount, gravity, playerCount, botPlayers){
// Default parameter is not yet supported by node (v5.5.0)
this.length = length || 3;
this.height = height || 3;
this.winCount = winCount || 3;
this.playerCount = playerCount || 2;
this.gravity = Boolean(gravity);
this.botPlayers = botPlayers || [2];
this.reset();
return this;
}
reset(){
this.win = 0;
this._board = [];
for (let i = 0; i < this.length; i++)
this._board[i] = Array(this.height + 1).join(1).split('').map((v, j) => new Point(i, j, this));
this.playerPositions = {};
for (let i = 1; i <= this.playerCount; i++)
this.playerPositions[i] = [];
this._paint();
}
get board(){ return this._board; }
boardPoint(x, y){ return this._board[x][y]; }
place(x, y, player){
if(this.win) throw `Game ended. Player-${this.win} won.`;
// if(!x || !y || !player) return console.log('Place what where?'), false;
if(x < 0 || x > this.length - 1 || // out of box
y < 0 || y > this.height - 1 ||
player < 1 || player > this.playerCount || // non-player
this._board[x][y].player // filled position
){
console.log(`\n------- Invalid move: player-${player} at (${x}, ${y}) -------\n`);
return false;
}
// loop till value is found or limit exceeded
if(this.gravity){
for(; y < this.height && !this._board[x][y].player; y++) { };
y = y - 1;
}
this._board[x][y].player = player;
this.playerPositions[player].push(this._board[x][y]);
const maxCoins = this._board[x][y].potential.max;
if(maxCoins.length >= this.winCount) return this._end(player, maxCoins), false;
const allPlaced = Object.keys(this.playerPositions)
.reduce((mem, k) => this.playerPositions[mem].concat(this.playerPositions[k]));
if(allPlaced.length === this.height * this.length)
return this._end('0'), false;
this._paint();
return this;
}
_end(player, strikeCoins){
strikeCoins && strikeCoins.forEach(point => {point.strike = true;})
this._paint();
this.win = player;
if(+player)
console.log(`\nGame ended. Player-${this.win} has won.\n`);
else
console.log('\nGame has ended in a draw.\n');
}
_paint(){
console.log('\x1B[2J\x1B[0f'); // Clean screen
// x index
console.log([' '].concat(Array(this.length + 1).join(1).split('').map((v, i) => i)).join(' '));
for(let i = 0; i < this.height ; i++){
console.log(
i + ' ', // y index
this._board.map(arr => {
const point = arr[i];
return colorLog(constants.playerKeys[point.player], point.strike && 'red')
}).join(' ')
);
}
// A line
console.log('\n', new Array(this.length + 3).join('--'), '\n');
}
}
{
"name": "tic-tac-toe",
"version": "0.1.0",
"description": "",
"main": "tic-tac-toe.js",
"dependencies": {
"readline-sync": "^1.3.0"
},
"devDependencies": {},
"scripts": {
"start": "node tic-tac-toe.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "[email protected]:52cfa6835a675d1edaa8.git"
},
"author": "bigOmega",
"license": "MIT"
}
'use strict';
const constants = require('./constants');
module.exports = class Point {
constructor(x, y, game){
this.x = x || 0;
this.y = y || 0;
this.player = 0;
this.game = game;
// initialising direction the checks and getters
constants.directions.forEach(dirString => {
Object.defineProperty(this, 'check_' + dirString, {
get: function(){
const dir = this['_' + dirString];
if(this.player && dir.allowed){
const neighbour = this.game.boardPoint(dir.x, dir.y);
if(neighbour.player && neighbour.player == this.player)
return [neighbour].concat(neighbour['check_' + dirString]);
else
return [];
} else {
return [];
}
}
});
Object.defineProperty(this, dirString, {
get: function(){
const dir = this['_' + dirString];
return dir.allowed ? this.game.boardPoint(dir.x, dir.y) : null;
}
});
});
}
get logger(){
return `pt: (${this.x}, ${this.y}) - ${this.player}`;
}
get potential(){
const vertical = Array.prototype.concat(this.check_n, this, this.check_s),
horizontal = Array.prototype.concat(this.check_e, this, this.check_w),
diagonal_nw = Array.prototype.concat(this.check_nw, this, this.check_se),
diagonal_ne = Array.prototype.concat(this.check_ne, this, this.check_sw),
max = [vertical, horizontal, diagonal_nw, diagonal_ne]
.reduce((mem, dir) => mem.length > dir.length ? mem : dir);
return {
vertical,
horizontal,
diagonal_ne,
diagonal_nw,
max,
};
}
posEquals(point){
return point.x == this.x && point.y == this.y;
}
get _n(){ const x = this.x, y = this.y; return { allowed: y - 1 >= 0, x: x, y: y - 1}; } // north
get _e(){ const x = this.x, y = this.y; return { allowed: x - 1 >= 0, x: x - 1, y: y}; } // east
get _w(){ const x = this.x, y = this.y; return { allowed: x + 1 < this.game.length, x: x + 1, y: y}; } // west
get _s(){ const x = this.x, y = this.y; return { allowed: y + 1 < this.game.height, x: x, y: y + 1}; } // south
get _ne(){ // north-east
const x = this.x, y = this.y;
const north = this._n, east = this._e;
return {allowed: north.allowed && east.allowed, x: east.x, y: north.y};
}
get _nw(){ // north-west
const x = this.x, y = this.y;
const north = this._n, west = this._w;
return { allowed: north.allowed && west.allowed, x: west.x, y: north.y};
}
get _se(){ // south-east
const x = this.x, y = this.y;
const south = this._s, east = this._e;
return { allowed: south.allowed && east.allowed, x: east.x, y: south.y};
}
get _sw(){ // south-west
const x = this.x, y = this.y;
const south = this._s, west = this._w;
return { allowed: south.allowed && west.allowed, x: west.x, y: south.y};
}
}
'use strict';
var readlineSync = require('readline-sync');
const constants = require('./constants');
const Game = require('./game');
const Bot = require('./bot');
let game, bot, mover = 1;
(function initialise () {
let length, height, winCount, gravity, playerCount, botPlayers = [];
const mode = +readlineSync.question('[1] Typical 3x3\n[2] Interesting 7x7\n[3] Gravity 10x10\n[4] Custom\n\nSelect mode: ') - 1;
// 3x3 is default
if(mode == 1){
length = height = 7;
winCount = 5;
} else if(mode == 2){
length = height = 10;
winCount = 5;
gravity = true;
} else if(mode == 3){
length = +readlineSync.question('Board length: (3) ', {defaultInput: 3});
height = +readlineSync.question('Board height: (3) ', {defaultInput: 3});
winCount = +readlineSync.question('Piece count for win: (3) ', {defaultInput: 3});
gravity = readlineSync.question('Gravity? [y/n]: (n) ', {defaultInput: 'n'}) === 'y';
}
// playerCount = +readlineSync.question('Players in game: (2) ', {defaultInput: 2});
botPlayers = readlineSync.question('Play against bot? [y/n]: (y) ') === 'n' ? [] : [2];
game = new Game(length, height, winCount, gravity, 2, botPlayers);
// game = new Game
bot = new Bot(game);
})();
while(!game.win){
let input;
if(game.botPlayers.indexOf(mover) > -1)
input = bot.getMove(mover);
else
input = readlineSync.question(`Player-${mover} (x,y): `).split(',').map(v => +v);
let ret = input[0] + 1 && input[1] + 1 && game.place(input[0], input[1], mover);
if(ret){
mover += 1;
if(mover > game.playerCount) mover = 1;
}
if(game.win && readlineSync.question('Retry? [y/n]: (y) ', {defaultInput: 'y'}) === 'y'){
mover = 1;
game.reset();
}
}
console.log('Thanks for playing\n\nby @bigOmega\nCheckout the code here: https://gist.github.com/bigomega/52cfa6835a675d1edaa8');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment