/**
* Copyright _ex_ ( http://wonderfl.net/user/_ex_ )
* MIT License ( http://www.opensource.org/licenses/mit-license.php )
* Downloaded from: http://wonderfl.net/c/2qkP
*/
package {
import flash.display.MovieClip;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
[SWF(backgroundColor = "#000000", frameRate = "30", width = "500", height = "500")]
public class main extends MovieClip {
public static var application:Object;
public function main():void {
if (stage) { init(); }
else { addEventListener(Event.ADDED_TO_STAGE, init); }
}
private function init(e:Event = null):void {
removeEventListener(Event.ADDED_TO_STAGE, init);
stage.quality = "LOW";
if (application == null) {
application = new TicTacToe(this);
}
stage.addEventListener(MouseEvent.MOUSE_DOWN, application.onMouseDown);
stage.addEventListener(KeyboardEvent.KEY_DOWN, application.onKeyDown);
stage.addEventListener(KeyboardEvent.KEY_UP, application.onKeyUp);
}
}
}
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.MovieClip;
import flash.events.Event;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.geom.Matrix;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.text.TextField;
import flash.text.TextFormat;
import flash.text.TextFieldAutoSize;
import flash.ui.Keyboard;
//--------------------------------------------------------------------------
// Board indexes:
// 0 | 1 | 2
// ---------
// 3 | 4 | 5
// ---------
// 6 | 7 | 8
class TicTacToe {
private static const N:int = 9; // Number of cells on board.
private static const L:int = 3; // Width and height of board.
private static const VOID:int = ' '.charCodeAt(0);
private static const DRAW:int = '='.charCodeAt(0);
private static const X:int = 'X'.charCodeAt(0);
private static const O:int = 'O'.charCodeAt(0);
private static const COLOR_X:uint = 0xFF2200AA;
private static const COLOR_O:uint = 0xFFAA0022;
private static const COLOR_DRAW:uint = 0xFFAAAA11;
private static const OC:int = 3; // Offset for drawing cells
private static const OT:int = 8; // Offset for drawing text
private var mGenerated:Array;
private var mRemaining:Array;
private var mCells:Array;
private var mLines:Array;
private var mTotalLines:int;
private var mRoot:Node;
private var mActiveNode:Node;
private var mTotal:int = 0;
private var mWinsX:int = 0;
private var mWinsO:int = 0;
private var mDraws:int = 0;
private var mCanvas:Bitmap;
private var mBitmapData:BitmapData;
private var mBitmapDataX:BitmapData;
private var mBitmapDataO:BitmapData;
private var mBitmapDataXS:BitmapData;
private var mBitmapDataOS:BitmapData;
private var mSizeBoard:int;
private var mSizeCell:int;
private var mShiftPressed:Boolean;
private var mControlPressed:Boolean;
public function TicTacToe(canvas:MovieClip) {
// Set board size.
mSizeBoard = 486;
mSizeCell = mSizeBoard / L;
mShiftPressed = false;
mControlPressed = false;
// Create bitmaps for drawing.
createBitmaps(canvas);
// Initialize arrays.
var k:int;
for (mGenerated = new Array(), k = 0; k++ < N; mGenerated.push( -1));
for (mRemaining = new Array(), k = 0; k < N; mRemaining.push(k++));
for (mCells = new Array(), k = 0; k++ < N; mCells.push(VOID));
// Hard-coded possible lines for L = 3.
if (L != 3) { throw new Error("mLines hardcoded for L = 3"); }
mLines = [0, 1, 2, 3, 4, 5, 6, 7, 8, // horizontal lines
0, 3, 6, 1, 4, 7, 2, 5, 8, // vertical lines
2, 4, 6, 0, 4, 8]; // diagonals
mTotalLines = mLines.length / L;
// Build game tree.
mRoot = new Node;
mRoot.turn = O;
mRoot.minmax = 0;
mActiveNode = null;
generateTreeMinMax(mRoot, 0);
trace("X: " + mRoot.winsX);
trace("O: " + mRoot.winsO);
trace("DRAW: " + mRoot.draws);
trace("TOTAL: " + mRoot.total);
drawNodeFrequencies(0, 0, mSizeBoard, mRoot, false);
}
private function createBitmaps(canvas:MovieClip):void {
// Canvas bitmap.
mBitmapData = new BitmapData(mSizeBoard, mSizeBoard);
mCanvas = new Bitmap(mBitmapData);
mCanvas.x = (canvas.stage.stageWidth - mSizeBoard)/2;
mCanvas.y = (canvas.stage.stageHeight - mSizeBoard)/2;
canvas.addChild(mCanvas);
// Bitmaps for X and O.
var format:TextFormat = new TextFormat();
format.size = 120;
var text:TextField = new TextField();
text.autoSize = TextFieldAutoSize.LEFT;
text.text = "X";
format.color = 0xFFFFFF00;
text.setTextFormat(format);
mBitmapDataX = new BitmapData(mSizeCell, mSizeCell, true, 0);
mBitmapDataX.fillRect(new Rectangle(OC, OC, mSizeCell - 2 * OC, mSizeCell - 2 * OC), COLOR_X);
mBitmapDataX.draw(text, new Matrix(1, 0, 0, 1, 37, 20));
format.color = 0xFF000000;
text.setTextFormat(format);
mBitmapDataXS = new BitmapData(mSizeCell, mSizeCell, true, 0);
mBitmapDataXS.draw(text, new Matrix(1, 0, 0, 1, 37, 20));
text.text = "O";
format.color = 0xFFFFFF00;
text.setTextFormat(format);
mBitmapDataO = new BitmapData(mSizeCell, mSizeCell, true, 0);
mBitmapDataO.fillRect(new Rectangle(OC, OC, mSizeCell - 2 * OC, mSizeCell - 2 * OC), COLOR_O);
mBitmapDataO.draw(text, new Matrix(1, 0, 0, 1, 35, 16));
format.color = 0xFF000000;
text.setTextFormat(format);
mBitmapDataOS = new BitmapData(mSizeCell, mSizeCell, true, 0);
mBitmapDataOS.draw(text, new Matrix(1, 0, 0, 1, 35, 16));
}
private function drawNodeFrequencies(x:int, y:int, size:int, node:Node, bestMove:Boolean):void {
// Sort node results.
var colors:Array = [ { val:node.winsO, c:COLOR_O, name:'O' },
{ val:node.winsX, c:COLOR_X, name:'X' },
{ val:node.draws, c:COLOR_DRAW, name:'=' } ];
colors.sortOn("val", Array.DESCENDING + Array.NUMERIC);
// Areas are proportionally drawn: a >= b >= c
var total:int = colors[0].val + colors[1].val + colors[2].val;
var c:int = size * (colors[2].val / total);
var b:int = size * (colors[1].val / total);
var a:int = size - b - c;
if (c > 0) {
mBitmapData.fillRect(new Rectangle(x, y, size, c), colors[2].c);
}
if (b > 0) {
mBitmapData.fillRect(new Rectangle(x, y + c, size, b), colors[1].c);
}
mBitmapData.fillRect(new Rectangle(x, y + b + c, size, a), colors[0].c);
// Draw statistics text.
var format:TextFormat = new TextFormat();
format.font = "_typewriter";
format.color = 0xFFFFFFFF;
format.size = 12;
var text:TextField = new TextField();
text.autoSize = TextFieldAutoSize.LEFT;
if (bestMove) {
mBitmapData.copyPixels((node.turn == X)? mBitmapDataXS : mBitmapDataOS,
new Rectangle(0, 0, mSizeCell, mSizeCell),
new Point(x, y), null, new Point(), true);
}
text.text = colors[2].name + ": (" + int(100 * colors[2].val / total) + "%) " + colors[2].val + "\n"
+ colors[1].name + ": (" + int(100 * colors[1].val / total) + "%) " + colors[1].val + "\n"
+ colors[0].name + ": (" + int(100 * colors[0].val / total) + "%) " + colors[0].val + "\n"
+ "T: " + node.total + "\n"
+ "m: " + node.minmax;
text.setTextFormat(format);
var bitmapText:BitmapData = new BitmapData(text.width, text.height, true, 0);
bitmapText.draw(text);
mBitmapData.copyPixels(bitmapText, new Rectangle(0, 0, text.width, text.height),
new Point(x + OT, y + size - text.height - OT / 2), null, new Point(), true);
}
private function drawNodes(node:Node):void {
// Get the best moves.
var bestScore:int = ((node.turn == X)? 3 : -3);
var bestMoves:Object = new Object();
for (var k:int = 0; k < N; ++k) {
var child:Node = node.nodes[k];
if (child != null) {
if ((node.turn == X) && (child.minmax < bestScore)) {
bestScore = child.minmax;
bestMoves = new Object();
}
if ((node.turn == O) && (child.minmax > bestScore)) {
bestScore = child.minmax;
bestMoves = new Object();
}
if (child.minmax == bestScore) {
bestMoves[k] = child;
}
}
}
// Draw statistics for free cells and gather best moves.
for (var x:int = 0; x < L; ++x) {
for (var y:int = 0; y < L; ++y) {
k = x + L * y;
child = node.nodes[k];
if (child != null) {
drawNodeFrequencies(mSizeCell * x + OC, mSizeCell * y + OC, mSizeCell - 2 * OC,
child, (bestMoves[k] != null));
}
}
}
// Draw current game state.
while (node != mRoot) {
mBitmapData.copyPixels((node.turn == X)? mBitmapDataX : mBitmapDataO,
new Rectangle(0, 0, mSizeCell, mSizeCell),
new Point(mSizeCell * (node.move % L), mSizeCell * int(node.move / L)));
node = node.parent;
}
// Draw current game state.
while (node != mRoot) {
mBitmapData.copyPixels((node.turn == X)? mBitmapDataX : mBitmapDataO,
new Rectangle(0, 0, mSizeCell, mSizeCell),
new Point(mSizeCell * (node.move % L), mSizeCell * int(node.move / L)));
node = node.parent;
}
}
//--------------------------------------------------------------------------
// Search for possible 2-in-line menaces.
private function findTwoInLineMenace(player:int):Boolean {
for (var k:int = 0; k < mTotalLines; ++k) {
var counter:int = 0;
var empty:Boolean = false;
for (var i:int = 0; i < L; ++i) {
if (mCells[mLines[L * k + i]] == VOID) {
empty = true;
}
if (mCells[mLines[L * k + i]] == player) {
++counter;
}
}
if (empty && (counter == L - 1)) {
return true;
}
}
return false;
}
private function generateTreeMinMax(node:Node, depth:int):int {
// We need to start analyzing permutations after they reach
// length 5. (minimum moves required to win a board)
if (depth >= 5) {
// Check if game ended.
var winner:int = getWinner(depth, node.turn);
if (winner != VOID) {
// Propagate the result to the root.
var p:Node = node;
while (p != null) {
++p.total;
switch (winner) {
case X: ++p.winsX; break;
case O: ++p.winsO; break;
case DRAW: ++p.draws; break;
}
p = p.parent;
}
node.minmax = (winner == X)? 3 : ((winner == O)? -3 : 0);
// If the game ended we don't need to continue
// generating the following permutations.
return node.minmax;
}
}
if (depth >= 3 && findTwoInLineMenace(node.turn)) {
node.minmax = ((node.turn == X)? 2 : -2);
}
else {
node.minmax = (node.turn == X)? 1 : -1;
}
// Generate next permutations.
for (var k:int = 0; k < N; ++k) {
if (mRemaining[k] != -1) {
// Do a valid move.
mGenerated[depth] = k; // mRemaining[k] == k
mRemaining[k] = -1;
mCells[k] = (depth % 2 == 0)? X : O;
var child:Node = new Node;
child.parent = node;
child.move = k;
child.turn = mCells[k];
node.nodes[k] = child;
var score:int = generateTreeMinMax(child, depth + 1);
// Undo move.
mRemaining[k] = k;
mCells[k] = VOID;
if ((node.turn == X) && (score < node.minmax)) {
node.minmax = score;
}
if ((node.turn == O) && (score > node.minmax)) {
node.minmax = score;
}
}
}
return node.minmax;
}
private function getWinner(turns:int, player:int):int {
// Check a full line in all the possible lines.
for (var k:int = 0; k < mTotalLines; ++k) {
var counter:int = 0;
for (var i:int = 0; i < L; ++i) {
if (mCells[mLines[L * k + i]] == player) {
++counter;
}
}
if (counter == L) {
return player;
}
}
// Board is full.
if (turns >= N) {
return DRAW;
}
return VOID;
}
//--------------------------------------------------------------------------
public function onEnterFrame(evt:Event):void { }
public function onMouseDown(event:MouseEvent):void {
var mx:int = (event.stageX - mCanvas.x) / mSizeCell;
var my:int = (event.stageY - mCanvas.y) / mSizeCell;
// If Shift is pressed undo move.
if ((mShiftPressed || mControlPressed) && (mActiveNode != null)) {
mActiveNode = mActiveNode.parent;
}
if (mActiveNode == null) {
if (mShiftPressed || mControlPressed) {
drawNodeFrequencies(0, 0, mSizeBoard, mRoot, false);
}
else {
mBitmapData.fillRect(new Rectangle(0, 0, mSizeBoard, mSizeBoard), 0);
mActiveNode = mRoot;
drawNodes(mRoot);
}
}
else {
if (mShiftPressed || mControlPressed) {
drawNodes(mActiveNode);
}
else if ((mx >= 0 && mx < L && my >= 0 && my < L)
&& (mActiveNode.nodes[mx + L * my] != null)) {
mBitmapData.fillRect(new Rectangle(0, 0, mSizeBoard, mSizeBoard), 0);
mActiveNode = mActiveNode.nodes[mx + L * my];
drawNodes(mActiveNode);
}
}
}
public function onKeyDown(event:KeyboardEvent):void {
switch(event.keyCode) {
case Keyboard.SHIFT:
mShiftPressed = true;
break;
case Keyboard.CONTROL:
mControlPressed = true;
break;
}
}
public function onKeyUp(event:KeyboardEvent):void {
switch(event.keyCode) {
case Keyboard.SHIFT:
mShiftPressed = false;
break;
case Keyboard.CONTROL:
mControlPressed = false;
break;
}
}
}
class Node {
public var winsX:int = 0;
public var winsO:int = 0;
public var draws:int = 0;
public var total:int = 0;
public var move:int = 0;
public var turn:int = 0;
public var minmax:int = 0;
public var parent:Node = null;
public var nodes:Array = new Array();
}