Simple Tic-Tac-Toe game developed with heavy use of design-by-contract
A Pen by Jack Firth on CodePen.
<link href='http://fonts.googleapis.com/css?family=Fredoka+One' rel='stylesheet' type='text/css'> | |
<div data-ng-app="TicTacToeApp"> | |
<div class="tic-tac-toe" data-ng-controller="TicTacToeController"> | |
<div class="turn-header" data-ng-show = "!isGameOver"> | |
{{activePlayer}} to move | |
</div> | |
<div class="turn-header" data-ng-show = "isGameOver && isTie"> | |
tie game | |
</div> | |
<div class="turn-header" data-ng-show = "isGameOver && !isTie"> | |
{{winner}} won the game | |
</div> | |
<div class="board"> | |
<div class="row group" data-ng-repeat="row in board"> | |
<div class="cell" data-ng-repeat="cell in row.cells" data-ng-click="clickCell(cell)" data-ng-class="{'cell-player-won': cell.winner && isX(winner), 'cell-player-lost': cell.winner && isO(winner)}"> | |
<span class="cell-value"> | |
{{cell.value}} | |
</span> | |
</div> | |
</div> | |
</div> | |
<div class="button-footer"> | |
<button class="large" data-ng-disabled="!isGameOver" data-ng-click="resetGame()"> | |
replay | |
</button> | |
</div> | |
<div class="button-footer"> | |
<button data-ng-disabled="AI.active" data-ng-click="setActiveAI(AI)" data-ng-repeat="AI in AIs" > | |
{{AI.name}} | |
</button> | |
</div> | |
</div> | |
</div> |
'use strict'; | |
(function() { | |
var contractsEnabled = false; | |
/* String helpers */ | |
function toStringWithQuotes(v) { | |
return typeof v === "string" | |
? '"' + v.toString() + '"' | |
: typeof v === "undefined" | |
? "undefined" | |
: v.toString(); | |
} | |
function capitalizeFirstLetter(string) { | |
return string.charAt(0).toUpperCase() + string.slice(1); | |
} | |
/* Function wrapping and metadata manipulation */ | |
Function.prototype.getName = function getName() { | |
return typeof this.wrappedName === "undefined" ? this.name : this.wrappedName; | |
} | |
Function.prototype.getLength = function getLength() { | |
return typeof this.wrappedName === "undefined" ? this.length : this.wrappedLength; | |
} | |
Function.prototype.wraps = function wraps(f) { | |
this.wrappedLength = f.getLength(); | |
this.wrappedName = f.getName(); | |
return this; | |
} | |
Function.prototype.renamed = function renamed(name) { | |
this.wrappedName = name; | |
return this; | |
} | |
/* Helper predicates - no contracts because they're used in building the contract functions */ | |
var isOfType = (function isOfType(typeString) { | |
return (function (v) { | |
return typeof v === typeString; | |
}).renamed("is" + capitalizeFirstLetter(typeString)); | |
}); | |
var isBoolean = isOfType("boolean"); | |
var isObject = isOfType("object"); | |
var isFunction = isOfType("function"); | |
var isUndefined = isOfType("undefined"); | |
var isString = isOfType("string"); | |
var isNumber = isOfType("number"); | |
var isAny = (function isAny(v) { | |
return true; | |
}); | |
var isInteger = (function isInteger(v) { | |
return typeof v === "number" | |
&& v % 1 === 0; | |
}); | |
/* Arity checker */ | |
Function.prototype.arity = function arity() { | |
var f = this; | |
return function() { | |
if (arguments.length != f.getLength()) { | |
throw new Error("arity mismatch in " + f.getName() + ": expected " + f.getLength() + " arguments, got " + arguments.length); | |
} else { | |
return f.apply(this, arguments); | |
} | |
}.wraps(f); | |
} | |
/* Function return value contract */ | |
Function.prototype.post = (function post(p) { | |
var f = this; | |
return (function() { | |
var ret = f.apply(this, arguments); | |
if (p(ret)) { | |
return ret; | |
} else { | |
throw new Error(f.getName() + " broke it's contract: promised " + p.getName() + " but returned " + toStringWithQuotes(ret)); | |
} | |
}).wraps(f); | |
}).arity(); | |
/* Function arguments contract */ | |
Function.prototype.pre = function pre() { | |
var f = this; | |
var ps = arguments; | |
return (function () { | |
for (var i=0; i < ps.length; i+=1) { | |
if (!ps[i](arguments[i])) { | |
throw new Error("contract error in " + f.getName() + ": expected " + ps[i].getName() + " at position " + i + " but received " + toStringWithQuotes(arguments[i])); | |
} | |
} | |
return f.apply(this, Array.prototype.slice.call(arguments, 0)); | |
}).wraps(f); | |
}; | |
Function.prototype.contract = function contract() { | |
var f = this; | |
if (f.getLength() !== arguments.length - 1) { | |
throw new Error("Contract construction error in " + f.getName() + " - number of argument predicates provided does not match number of function arguments - provided " + (arguments.length - 1) + " argument predicates"); | |
} | |
if (!contractsEnabled) | |
return f; | |
var prePreds = Array.prototype.slice.call(arguments, 0, arguments.length - 1); | |
var postPred = arguments[arguments.length - 1]; | |
if (arguments.length > 1) { | |
return f | |
.arity() | |
.pre.apply(f, prePreds) | |
.post(postPred); | |
} | |
if (arguments.length == 1) { | |
return f | |
.arity() | |
.post(postPred); | |
} | |
else { | |
throw new Error("Contract construction error in " + f.getName() + " - no predicates provided"); | |
} | |
}; | |
/* Custom predicate helpers */ | |
var isObjectOf = (function isObjectOf(name, propPredicates) { | |
return (function (v) { | |
if (!isObject(v)) { | |
return false; | |
} | |
for (var prop in propPredicates) { | |
if (propPredicates.hasOwnProperty(prop)) { | |
if (isUndefined(v[prop])) { | |
return false; | |
} | |
} | |
} | |
return true; | |
}).contract(isAny, isBoolean).renamed("is" + capitalizeFirstLetter(name)); | |
}).contract(isString, isObject, isFunction); | |
var isValue = (function isValue(value) { | |
return (function (v) { | |
return v === value; | |
}).contract(isAny, isBoolean); | |
}).contract(isAny, isFunction); | |
var isNot = (function isNot(p) { | |
return (function (v) { | |
return !p(v); | |
}).contract(isAny, isBoolean); | |
}).contract(isFunction, isFunction); | |
var isOneOf = (function isOneOf() { | |
var ps = arguments; | |
return (function (v) { | |
for (var i = 0; i < ps.length; i += 1) { | |
if (ps[i](v)) { | |
return true; | |
} | |
} | |
return false; | |
}).contract(isAny, isBoolean); | |
}); | |
var isEach = (function isEach() { | |
var ps = arguments; | |
return (function (v) { | |
for (var i = 0; i < ps.length; i += 1) { | |
if (!ps[i](v)) { | |
return false; | |
} | |
} | |
return true; | |
}).contract(isAny, isBoolean); | |
}); | |
var hasPropertyOf = (function hasPropertyOf(name, p) { | |
return (function (v) { | |
return !isUndefined(v[name]) && p(v[name]); | |
}).contract(isAny, isBoolean); | |
}).contract(isString, isFunction, isFunction); | |
var isNumberInRange = (function isNumberInRange(low, high) { | |
return (function (v) { | |
return isNumber(v) && low <= v && v <= high; | |
}).contract(isAny, isBoolean).renamed("isNumberInRange" + low + "to" + high); | |
}).contract(isNumber, isNumber, isFunction); | |
var isDefined = isNot(isUndefined).renamed("isDefined"); | |
/* FP helpers */ | |
var buildArray = (function buildArray(length, f) { | |
var a = new Array(); | |
for (var i=0; i<length; i++) { | |
a[i] = f(i); | |
} | |
return a; | |
}).contract(isInteger, isFunction, Array.isArray); | |
var zip = (function zip(f, as, bs) { | |
var arr = new Array(); | |
for (var i = 0; i < as.length; i += 1) { | |
arr[i] = f(as[i], bs[i]); | |
} | |
return arr; | |
}).contract(isFunction, Array.isArray, Array.isArray, Array.isArray); | |
var foldOr = (function foldOr(f, vs) { | |
for (var i = 0; i < vs.length; i+=1) { | |
if (f(vs[i])) { | |
return true; | |
} | |
} | |
return false; | |
}).contract(isFunction, Array.isArray, isBoolean); | |
var foldAnd = (function foldAnd(f, vs) { | |
for (var i = 0; i < vs.length; i+=1) { | |
if (!f(vs[i])) { | |
return false; | |
} | |
} | |
return true; | |
}).contract(isFunction, Array.isArray, isBoolean); | |
/* Board data definitions */ | |
var cellValues = {empty: "", x: "X", o: "O"} | |
var isEmptyCellValue = isValue(cellValues.empty).renamed("isEmptyCellValue"); | |
var isX = isValue(cellValues.x).renamed("isX"); | |
var isO = isValue(cellValues.o).renamed("isO"); | |
var isPlayerValue = isOneOf(isX, isO).renamed("isPlayerValue"); | |
var isCellValue = isOneOf(isEmptyCellValue, isPlayerValue).renamed("isCellValue"); | |
var isCell = isObjectOf("cell", { | |
value: isCellValue, | |
row: isInteger, | |
column: isInteger, | |
winning: isBoolean | |
}); | |
var isEmptyCell = isEach(isCell, hasPropertyOf("value", isEmptyCellValue)).renamed("isEmptyCell"); | |
var isCellOrUndefined = isOneOf(isUndefined, isCell).renamed("isCellOrUndefined"); | |
var isRow = isObjectOf("row", { cells: Array.isArray }); | |
var isColumnIndex = isEach(isInteger, isNumberInRange(0, 2)).renamed("isColumnIndex"); | |
var isRowIndex = isEach(isInteger, isNumberInRange(0, 2)).renamed("isRowIndex"); | |
var isBoard = Array.isArray.contract(isAny, isBoolean).renamed("isBoard"); | |
var isMoveAndValue = isObjectOf("moveAndValue", { | |
cell: isEmptyCell, | |
value: isInteger | |
}); | |
/* Querying boards */ | |
var getCell = (function getCell(board, row, column) { | |
return board[row].cells[column]; | |
}).contract(isBoard, isRowIndex, isColumnIndex, isCell); | |
var getCellValue = (function getCellValue(board, row, column) { | |
return getCell(board, row, column).value; | |
}).contract(isBoard, isRowIndex, isColumnIndex, isCellValue); | |
var isPlayerAt = (function isPlayerAt(board, player, row, column) { | |
return isValue(player)(getCellValue(board, row, column)); | |
}).contract(isBoard, isPlayerValue, isRowIndex, isColumnIndex, isBoolean); | |
var isPlayerAtEach = (function isPlayerAtEach(board, player, rows, columns) { | |
return foldAnd( | |
function(indices) { return isPlayerAt(board, player, indices.row, indices.column); }, | |
zip(function(r, c) { return { row: r, column: c }; }, rows, columns)); | |
}).contract(isBoard, isPlayerValue, Array.isArray, Array.isArray, isBoolean); | |
/* Board terminality predicates */ | |
var rowFull = (function rowFull(row) { | |
return !foldOr(isEmptyCell, row.cells); | |
}).contract(isRow, isBoolean); | |
var isNotFullBoard = (function isNotFullBoard(board) { | |
return isBoard(board) && !foldAnd(rowFull, board); | |
}).contract(isAny, isBoolean); | |
var isRowWonBy = (function isRowWonBy(board, player, row) { | |
return isPlayerAtEach(board, player, [row, row, row], [0, 1, 2]); | |
}).contract(isBoard, isPlayerValue, isRowIndex, isBoolean); | |
var isColumnWonBy = (function isColumnWonBy(board, player, column) { | |
return isPlayerAtEach(board, player, [0, 1, 2], [column, column, column]); | |
}).contract(isBoard, isPlayerValue, isColumnIndex, isBoolean); | |
var anyRowsWonBy = (function anyRowsWonBy(board, player) { | |
return isRowWonBy(board, player, 0) | |
|| isRowWonBy(board, player, 1) | |
|| isRowWonBy(board, player, 2); | |
}).contract(isBoard, isPlayerValue, isBoolean); | |
var anyColumnsWonBy = (function anyColumnsWonBy(board, player) { | |
return isColumnWonBy(board, player, 0) | |
|| isColumnWonBy(board, player, 1) | |
|| isColumnWonBy(board, player, 2); | |
}).contract(isBoard, isPlayerValue, isBoolean); | |
var isTopLeftDiagonalWonBy = (function isTopLeftDiagonalWonBy(board, player) { | |
return isPlayerAtEach(board, player, [0, 1, 2], [0, 1, 2]); | |
}).contract(isBoard, isPlayerValue, isBoolean); | |
var isTopRightDiagonalWonBy = (function isTopLeftDiagonalWonBy(board, player) { | |
return isPlayerAtEach(board, player, [0, 1, 2], [2, 1, 0]); | |
}).contract(isBoard, isPlayerValue, isBoolean); | |
var isBoardWonBy = (function isBoardWonBy(board, player) { | |
return anyRowsWonBy(board, player) | |
|| anyColumnsWonBy(board, player) | |
|| isTopLeftDiagonalWonBy(board, player) | |
|| isTopRightDiagonalWonBy(board, player); | |
}).contract(isBoard, isPlayerValue, isBoolean); | |
var isTerminalBoard = (function isTerminalBoard(board) { | |
return isBoard(board) | |
&& (isBoardWonBy(board, cellValues.x) | |
|| isBoardWonBy(board, cellValues.o) | |
|| !isNotFullBoard(board)); | |
}).contract(isAny, isBoolean); | |
var isNotTerminalBoard = isNot(isTerminalBoard).renamed("isNotTerminalBoard"); | |
var otherPlayer = (function otherPlayer(v) { | |
return v === cellValues.x ? cellValues.o : cellValues.x; | |
}).contract(isPlayerValue, isPlayerValue); | |
/* Construction of data structures */ | |
var makeEmptyCell = (function makeEmptyCell(row) { | |
return (function makeEmptyRowCell(column) { | |
return { | |
value: cellValues.empty, | |
row: row, | |
column: column, | |
winning: false | |
}; | |
}).contract(isInteger, isEmptyCell); | |
}).contract(isInteger, isFunction); | |
var makeEmptyRow = (function makeEmptyRow(row) { | |
return { cells: buildArray(3, makeEmptyCell(row)) }; | |
}).contract(isInteger, isRow); | |
var makeEmptyBoard = (function makeEmptyBoard() { | |
return buildArray(3, makeEmptyRow); | |
}).contract(Array.isArray); | |
var copyBoard = (function copiedBoard(board) { | |
var copied = makeEmptyBoard(); | |
for (var row = 0; row < 3; row += 1) { | |
for (var column = 0; column < 3; column += 1) { | |
copied[row].cells[column].value = board[row].cells[column].value; | |
} | |
} | |
return copied; | |
}).contract(isBoard, isBoard); | |
var getBoardWithMove = (function getBoardWithMove(board, row, column, player) { | |
var copiedBoard = copyBoard(board); | |
copiedBoard[row].cells[column].value = player; | |
return copiedBoard; | |
}).contract(isNotFullBoard, isRowIndex, isColumnIndex, isPlayerValue, isBoard); | |
/* Random move AI */ | |
var randomInArray = (function randomInArray(array) { | |
return array[Math.floor(Math.random() * array.length)]; | |
}).contract(Array.isArray, isAny); | |
var randomRowCell = (function randomRowCell(row) { | |
return randomInArray(row.cells); | |
}).contract(isRow, isCell); | |
var randomRow = (function randomRow(board) { | |
return randomInArray(board); | |
}).contract(isBoard, isRow); | |
var randomMove = (function randomMove(board, movingPlayer) { | |
var cell = randomRowCell(randomRow(board)); | |
return isEmptyCell(cell) ? cell : randomMove(board, movingPlayer); | |
}).contract(isNotTerminalBoard, isPlayerValue, isEmptyCell); | |
/* Optimal move AI */ | |
var minMove = (function minMove(board, movingPlayer, lastCell) { | |
var bestMove = maxMove(board, movingPlayer, lastCell); | |
return { | |
cell: bestMove.cell, | |
value: bestMove.value * -1 | |
}; | |
}).contract(isBoard, isPlayerValue, isCell, isMoveAndValue); | |
var maxMove = (function maxMove(board, movingPlayer, lastCell) { | |
if (isBoardWonBy(board, movingPlayer)) { | |
return { cell: lastCell, value: 1 }; | |
} if (isBoardWonBy(board, otherPlayer(movingPlayer))) { | |
return { cell: lastCell, value: -1 }; | |
} if (!isNotFullBoard(board)) { | |
return { cell: lastCell, value: 0 }; | |
} | |
var bestMove = false; | |
for (var row = 0; row < 3; row += 1) { | |
for (var column = 0; column < 3; column += 1) { | |
var currentCell = board[row].cells[column]; | |
if (!isEmptyCell(currentCell)) { | |
continue; | |
} | |
var boardWithMove = getBoardWithMove(board, row, column, movingPlayer); | |
var boardMinMove = minMove(boardWithMove, otherPlayer(movingPlayer), currentCell); | |
if (!bestMove || boardMinMove.value > bestMove.value) { | |
bestMove = { cell: currentCell, value: boardMinMove.value }; | |
} | |
if (bestMove.value === 1) { | |
return bestMove; | |
} | |
} | |
} | |
return bestMove; | |
}).contract(isBoard, isPlayerValue, isCellOrUndefined, isMoveAndValue); | |
var bestMove = (function bestMove(board, movingPlayer) { | |
if (isEmptyCellValue(getCellValue(board, 1, 1))) { | |
return getCell(board, 1, 1); | |
} else { | |
return maxMove(board, movingPlayer, window.undefined).cell; | |
} | |
}).contract(isNotTerminalBoard, isPlayerValue, isEmptyCell); | |
/* Optimal move AI with mistakes */ | |
var bestMoveWithMistakes = (function bestMoveWithMistakes(board, movingPlayer) { | |
if (Math.random() < .2) { | |
return randomMove(board, movingPlayer); | |
} else { | |
return bestMove(board, movingPlayer); | |
} | |
}).contract(isNotTerminalBoard, isPlayerValue, isEmptyCell); | |
/* App construction */ | |
angular | |
.module('TicTacToeApp', []) | |
.controller( | |
'TicTacToeController', | |
['$scope', '$timeout', | |
function($scope, $timeout) { | |
$scope.board = makeEmptyBoard(); | |
$scope.activePlayer = cellValues.x; | |
$scope.isGameOver = false; | |
$scope.winner = undefined; | |
$scope.isTie = undefined; | |
$scope.isX = isX; | |
$scope.isO = isO; | |
$scope.nextTurn = function nextTurn() { | |
$scope.activePlayer = otherPlayer($scope.activePlayer); | |
}; | |
$scope.AIfunc = randomMove; | |
$scope.AIs = [ | |
{ | |
active: true, | |
name: "easy", | |
func: randomMove | |
}, | |
{ | |
active: false, | |
name: "normal", | |
func: bestMoveWithMistakes | |
}, | |
{ | |
active: false, | |
name: "hard", | |
func: bestMove | |
} | |
]; | |
$scope.setActiveAI = function setActiveAI(AI) { | |
for (var i = 0; i < $scope.AIs.length; i+=1) { | |
$scope.AIs[i].active = false; | |
} | |
AI.active = true; | |
$scope.AIfunc = AI.func; | |
$scope.resetGame(); | |
}; | |
$scope.clickCell = function clickCell(cell) { | |
if (!$scope.isGameOver && isEmptyCell(cell) && isX($scope.activePlayer)) { | |
cell.value = cellValues.x; | |
if ($scope.gameOver()) { | |
$scope.endGame(); | |
} else { | |
$scope.nextTurn(); | |
$timeout($scope.takeAITurn, 500); | |
} | |
} | |
}; | |
$scope.takeAITurn = function() { | |
$scope.AIfunc($scope.board, $scope.activePlayer).value = $scope.activePlayer; | |
if ($scope.gameOver()) { | |
$scope.endGame(); | |
} else { | |
$scope.nextTurn(); | |
} | |
}; | |
$scope.gameOver = function gameOver() { | |
return isTerminalBoard($scope.board); | |
}; | |
$scope.endGame = function endGame() { | |
$scope.isGameOver = true; | |
$scope.isTie = false; | |
if (isBoardWonBy($scope.board, cellValues.x)) { | |
$scope.winner = cellValues.x; | |
} else if (isBoardWonBy($scope.board, cellValues.o)) { | |
$scope.winner = cellValues.o; | |
} else if (!isNotFullBoard($scope.board)) { | |
$scope.isTie = true; | |
} else { | |
throw new Error("endGame method was called while board was not in a terminal state"); | |
} | |
if (!$scope.isTie) { | |
for (var i = 0; i < 3; i += 1) { | |
if (isRowWonBy($scope.board, $scope.winner, i)) { | |
for (var j = 0; j < 3; j += 1) { | |
getCell($scope.board, i, j).winner = true; | |
} | |
} | |
if (isColumnWonBy($scope.board, $scope.winner, i)) { | |
for (var j = 0; j < 3; j += 1) { | |
getCell($scope.board, j, i).winner = true; | |
} | |
} | |
} | |
if (isTopLeftDiagonalWonBy($scope.board, $scope.winner)) { | |
getCell($scope.board, 0, 0).winner = true; | |
getCell($scope.board, 1, 1).winner = true; | |
getCell($scope.board, 2, 2).winner = true; | |
} | |
if (isTopRightDiagonalWonBy($scope.board, $scope.winner)) { | |
getCell($scope.board, 0, 2).winner = true; | |
getCell($scope.board, 1, 1).winner = true; | |
getCell($scope.board, 2, 0).winner = true; | |
} | |
} | |
}; | |
$scope.resetGame = (function resetGame() { | |
$scope.board = makeEmptyBoard(); | |
$scope.isGameOver = false; | |
$scope.winner = undefined; | |
$scope.tie = undefined; | |
$scope.activePlayer = cellValues.x; | |
}); | |
}]); | |
})(); |
/* box-sizing */ | |
html { | |
box-sizing: border-box; | |
font-family: 'Fredoka One', cursive; | |
background: #1A1A1A; | |
height: 100%; | |
} | |
*, *:before, *:after { | |
box-sizing: inherit; | |
} | |
/* clearfix */ | |
.group:after { | |
content: ""; | |
display: table; | |
clear: both; | |
} | |
.tic-tac-toe { | |
width: 400px; | |
margin: 50px auto; | |
} | |
.board { | |
height: 400px; | |
} | |
.turn-header { | |
text-align: center; | |
font-size: 300%; | |
color: darkgrey; | |
padding-bottom: 20px; | |
} | |
.row { | |
height: 33.3333% | |
} | |
.row:not(:last-of-type) { | |
border-bottom: solid 2px darkgrey; | |
} | |
.cell { | |
float: left; | |
width: 33.3333%; | |
height: 100%; | |
color: darkgrey; | |
position: relative; | |
font-size: 600%; | |
} | |
.cell:not(:last-of-type) { | |
border-right: solid 2px darkgrey; | |
} | |
.cell-value { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
} | |
.cell-player-won { | |
animation-name: winner-glow; | |
animation-duration: .5s; | |
animation-iteration-count: infinite; | |
} | |
@keyframes winner-glow { | |
0% { | |
color: darkgrey; | |
} | |
50% { | |
color: darkgreen; | |
} | |
100% { | |
color: darkgrey; | |
} | |
} | |
.cell-player-lost { | |
animation-name: loser-glow; | |
animation-duration: .5s; | |
animation-iteration-count: infinite; | |
} | |
@keyframes loser-glow { | |
0% { | |
color: darkgrey; | |
} | |
50% { | |
color: darkred; | |
} | |
100% { | |
color: darkgrey; | |
} | |
} | |
.button-footer { | |
margin: 20px auto; | |
text-align: center; | |
} | |
button { | |
border: solid 2px #666; | |
border-radius: 5px; | |
font-family: inherit; | |
color: darkgrey; | |
padding: 5px 10px; | |
font-size: 150%; | |
background: #333; | |
} | |
button.large { | |
font-size: 200%; | |
} | |
button:disabled { | |
background: #222; | |
color: #444; | |
border-color: #444; | |
} | |
button:not(:last-of-type) { | |
border-right: 0px; | |
border-top-right-radius: 0px; | |
border-bottom-right-radius: 0px; | |
margin-right: 0px; | |
} | |
button:not(:first-of-type) { | |
border-top-left-radius: 0px; | |
border-bottom-left-radius: 0px; | |
margin-left: 0px; | |
} |
Simple Tic-Tac-Toe game developed with heavy use of design-by-contract
A Pen by Jack Firth on CodePen.