TicTacToe

by _ex_
Tic-Tac-Toe game tree created using the minimax algorithm. Click the left mouse button to go down the tree, press SHIFT and click to go up the tree.
♥0 | Line 350 | Modified 2011-02-09 14:28:19 | MIT License
play

ActionScript3 source code

/**
 * 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();
}