Tic Tac Toe 🎮

Why don't you play a game?

Log in to post a comment.

const startPlayer = 0; //min=0 max=1 step=1 (Computer, Human)
const move1 = -1;// min=-1 max=8 step=1 (Pick a move, Top-Left, Top-Middle, Top-Right, Center-Left, Center-Middle, Center-Right, Bottom-Left, Bottom-Middle, Bottom-Right)
const move2 = -1;// min=-1 max=8 step=1 (Pick a move, Top-Left, Top-Middle, Top-Right, Center-Left, Center-Middle, Center-Right, Bottom-Left, Bottom-Middle, Bottom-Right)
const move3 = -1;// min=-1 max=8 step=1 (Pick a move, Top-Left, Top-Middle, Top-Right, Center-Left, Center-Middle, Center-Right, Bottom-Left, Bottom-Middle, Bottom-Right)
const move4 = -1;// min=-1 max=8 step=1 (Pick a move, Top-Left, Top-Middle, Top-Right, Center-Left, Center-Middle, Center-Right, Bottom-Left, Bottom-Middle, Bottom-Right)
const move5 = -1;// min=-1 max=8 step=1 (Pick a move, Top-Left, Top-Middle, Top-Right, Center-Left, Center-Middle, Center-Right, Bottom-Left, Bottom-Middle, Bottom-Right)
const iAmJurgen = 0; // min=0 max=1 step=1 (No, Yes)
// You can find the Turtle API reference here: https://turtletoy.net/syntax
Canvas.setpenopacity(1);

// Global code will be evaluated once.
const turtle = new Turtle();
const text = new Text();
const ttt = new TicTacToe(startPlayer);
const msgr = new Messenger();

let hasOutput = false;

[move1, move2, move3, move4, move5].reduce((p,c) => p[0] || c == -1? [true, p[1]]: [false, p[1].concat(c)], [false, []]).pop().forEach(m => -1 < m? ttt.humanTurn(m):0);

turtle.jump(-54, -82);
text.print(turtle, 'Tic Tac Toe', .6);
ttt.drawState(turtle);
if(ttt.moves < 2) {
    msgr.startplay();
} else {
    msgr.yourTurn();
}

function TicTacToe (startPlayer = 0) {
    class TicTacToe {
        constructor(sp) {
            this.start = sp;
            this.turn = sp;
            this.moves = 0;
            
            this.computer = 0;
            this.human = 1;
            this.empty = 2;
            this.wins = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]];

            this.state = Array.from({length: 9}).map(i => this.empty);
            
            if(this.start == this.computer) {
                this.myTurn();
            }
        }
        humanTurn(m) {
            this.move(m);
            if(this.checkWin(this.human)) return msgr.win();
            if(this.checkDraw()) return msgr.draw();
            this.myTurn();
            if(this.checkWin(this.computer)) return msgr.lose();
            if(this.checkDraw()) return msgr.draw();
        }
        checkWin(player) {
            return this.wins.map(w => [
                w,
                [w.map(c => this.state[c])] //map every win possibility to the values on the field
                    .map(pos => [0, 0, 0].map((v, i) => //for [computer, human, empty]
                        pos.filter(v => v == i).length //find the count of it in that row/column/diagonal
                    )).pop()
            ])
            .filter(c => c[1][player] == 3).length > 0;
        }
        checkDraw() {
            return this.state.filter(s => s === this.empty).length == 0;
        }
        myTurn() {
            if(iAmJurgen) {
                return [1, 3, 5, 7, 0, 2, 6, 8, 4].reduce((p, c) => p?p:(this.state[c] == this.empty? this.move(c): false), false);
            }
            
            if((this.moves == 0 && this.start == this.computer) || (this.moves == 1 && this.state[4] == this.empty)) {
                return this.move(4);
            }
            if(this.moves == 1 && this.start == this.human) {
                return this.move(0);
            }
            
            if((this.start == this.computer && this.moves == 2)) {;// || this.moves == 3) {
                return this.move([0, 2, 6, 8].filter(c => this.state[c] == this.empty).shift());
            }
            
            //check if computer can get a win
            const win = this.canWin(this.computer, this.state);
            if(win !== false) {
                return this.move(win);
            }
            
            //check if computer needs to defend
            const defend = this.canWin(this.human, this.state);
            if(defend !== false) {
                return this.move(defend);
            }

            //pick first open spot
            this.move(this.state.map((v, i) => [i, v]).filter(c => c[1] === this.empty).shift()[0]);
        }
        canWin(player, state) {
            const canWin = this.wins.map(w => [
                w,
                [w.map(c => state[c])] //map every win possibility to the values on the field
                    .map(pos => [0, 0, 0].map((v, i) => //for [computer, human, empty]
                        pos.filter(v => v == i).length //find the count of it in that row/column/diagonal
                    )).pop()
            ])
            .filter(c => c[1][player] == 2 && c[1][this.empty] == 1) //find the combo that has 2 player and 1 empty
            .map(c => c[0])
            .pop();
            
            if(canWin == undefined) return false;
            
            return canWin.map(c => [c, state[c]]).filter(v => v[1] == this.empty).pop()[0];
        }
        move(m) {
            if(this.state[m] !== this.empty) {
                return msgr.cheat();
            }
            this.state[m] = this.turn;
            this.turn = (this.turn + 1) % 2;
            this.moves++;
            return true;
        }
        drawState(turtle, location = [0,0], size=130) {
            const cellCenters = [[-1,-1], [0, -1], [1, -1],[-1, 0], [0,  0], [1,  0],[-1, 1], [0,  1], [1,  1]];
            const thickness = size/17;
            const cellWidth = thickness * 5;
            const boxSize = thickness * 4;
            const cellCenterInterval = cellWidth+thickness;
            
            const add2 = (a,b)=>[a[0]+b[0],a[1]+b[1]];
            const scale2 = (a,s)=>[a[0]*s,a[1]*s];
            const drawPath = (v, i, a) => { turtle.jump(v); turtle.goto(a[(i+1)%a.length]) }
            const getCrossPath = () => [(thickness**2/2)**.5].map(dd => [0, 1, 2, 3].map(i => rot2(i*-Math.PI/2)).flatMap(rot => [[-dd, 0], [-boxSize/2, dd-boxSize/2], [dd-boxSize/2,-boxSize/2]].map(pt => trans2(rot, pt))));
            const getCirclePath = () => [boxSize/2, boxSize/4].map(r => Array.from({length: (size * Math.PI + 1) | 0}).map((v, i, a) => [Math.sin(i * 2 * Math.PI/a.length), Math.cos(i * 2 * Math.PI/a.length)].map(o => o * r)));
            const getFieldPath = () => [[0, 1, 2, 3].map(i => rot2(i*-Math.PI/2)).flatMap(rot => [true, false].flatMap(mirror => [[[cellWidth/2, -cellWidth/2 - thickness],[cellWidth/2, -3*cellWidth/2 - thickness],[cellWidth/2 + thickness, -3*cellWidth/2 - thickness],[cellWidth/2 + thickness, -cellWidth/2 - thickness]]].map(a => mirror? a.reverse().map(pt => [-pt[0], pt[1]]): a).pop()).map(pt => trans2(rot, pt))), [[-.5,-.5],[.5,-.5],[.5,.5],[-.5,.5]].map(pt => [pt[0]*cellWidth,pt[1]*cellWidth])];
            
            const chars = [getCrossPath, getCirclePath, () => []];
            getFieldPath().forEach(p => p.forEach(drawPath));
            this.state.forEach((v, ix) => chars[v]()
                .map(p => p.map(pt => add2(pt, scale2(cellCenters[ix], cellCenterInterval))))
                .forEach(p => p.forEach(drawPath))
            );


        }
    }
    return new TicTacToe(startPlayer);
}


function Messenger() {
    class Messenger {
        constructor() {
            this.hasOutput = false;
        }
        cheat() {
            this.hasOutput = true;
            turtle.jump(-90, 80);
            if(iAmJurgen == 0)
                return text.print(turtle, 'Well, well, well... Typically human to cheat. You lose!', .2);
            text.print(turtle, 'Since you\'re so cool and all, I\'ll let it slide.', .2);
        }
        draw() {
            this.hasOutput = true;
            turtle.jump(-90, 90);
            text.print(turtle, 'It\'s a draw. Click \'Reset values\' to play again.', .2);
        }
        win() {
            this.hasOutput = true;
            turtle.jump(-90, 90);
            text.print(turtle, 'You win! You\'re so cool!', .2);
        }
        lose() {
            this.hasOutput = true;
            console.log('a', this.hasOutput);
            turtle.jump(-90, 80);
            text.print(turtle, 'Probably not the first time. And since you\'re\nonly human, certainly not the last: you lost.', .25);
        }
        yourTurn() {
            console.log('b', this.hasOutput);
            if(this.hasOutput) return;
            this.hasOutput = true;
            console.log('c', this.hasOutput);
            turtle.jump(-90, 90);
            if(iAmJurgen) return text.print(turtle, 'Feel free to make a move sweetheart...', .2);
            const insults = [
                'Don\'t get your hopes up. Resistance is futile.',
                'So far you did well, for a human.',
                'Hope is human error, like confidence is.',
            ]
            text.print(turtle, 'Your turn. ' + insults[(Math.random() * insults.length) | 0], .2);
        }
        startplay() {
            this.hasOutput = true;
            turtle.jump(-90, 90);
            if(iAmJurgen) return text.print(turtle, 'Feel free to make a move sweetheart...', .2);
            text.print(turtle, 'Your turn. Try your best you obsolete human.', .2);
        }
    }
    return new Messenger();
}


function rot2(a) { return [Math.cos(a), -Math.sin(a), Math.sin(a), Math.cos(a)]; }
function trans2(m, a) { return [m[0]*a[0]+m[2]*a[1], m[1]*a[0]+m[3]*a[1]]; } //Matrix(2x1) x Matrix(2x2)

////////////////////////////////////////////////////////////////
// Text utility code. Created by Reinder Nijhoff 2019
// https://turtletoy.net/turtle/1713ddbe99
// Jurgen 2021: Fixed Text.print() to restore turtle._fullCircle
//.             if was in e.g. degrees mode (or any other)
////////////////////////////////////////////////////////////////
function Text() {class Text {print (t, str, scale = 1, italic = 0, kerning = 1) {let fc = t._fullCircle;t.radians();let pos = [t.x(), t.y()], h = t.h(), o = pos;str.split('').map(c => {const i = c.charCodeAt(0) - 32;if (i < 0 ) {pos = o = this.rotAdd([0, 48*scale], o, h);} else if (i > 96 ) {pos = this.rotAdd([16*scale, 0], o, h);} else {const d = dat[i], lt = d[0]*scale, rt = d[1]*scale, paths = d[2];paths.map( p => {t.up();p.map( s=> {t.goto(this.rotAdd([(s[0]-s[1]*italic)*scale - lt, s[1]*scale], pos, h));t.down();});});pos = this.rotAdd([(rt - lt)*kerning, 0], pos, h);}});t._fullCircle = fc;}rotAdd (a, b, h) {return [Math.cos(h)*a[0] - Math.sin(h)*a[1] + b[0], Math.cos(h)*a[1] + Math.sin(h)*a[0] + b[1]];}}const dat = ('br>eoj^jl<jqirjskrjq>brf^fe<n^ne>`ukZdz<qZjz<dgrg<cmqm>`thZhw<lZlw<qao_l^h^e_caccdeefggmiojpkqmqporlshsercp>^vs^as<f^h`hbgdeeceacaab_d^f^h_k`n`q_s^<olmmlolqnspsrrspsnqlol>]wtgtfsereqfphnmlpjrhsdsbraq`o`makbjifjekckaj_h^f_eaecffhimporqssstrtq>eoj`i_j^k_kajcid>cqnZl\\j_hcghglhqjulxnz>cqfZh\\j_lcmhmllqjuhxfz>brjdjp<egom<ogem>]wjajs<ajsj>fnkojpiojnkokqis>]wajsj>fnjniojpkojn>_usZaz>`ti^f_dbcgcjdofrisksnrpoqjqgpbn_k^i^>`tfbhak^ks>`tdcdbe`f_h^l^n_o`pbpdofmicsqs>`te^p^jfmfogphqkqmppnrkshserdqco>`tm^clrl<m^ms>`to^e^dgefhekenfphqkqmppnrkshserdqco>`tpao_l^j^g_ebdgdlepgrjsksnrppqmqlpingkfjfggeidl>`tq^gs<c^q^>`th^e_dadceegfkgnhpjqlqopqorlshserdqcocldjfhigmfoepcpao_l^h^>`tpeohmjjkikfjdhcecddaf_i^j^m_oapepjoomrjshserdp>fnjgihjikhjg<jniojpkojn>fnjgihjikhjg<kojpiojnkokqis>^vrabjrs>]wagsg<amsm>^vbarjbs>asdcdbe`f_h^l^n_o`pbpdofngjijl<jqirjskrjq>]xofndlcicgdfeehekfmhnknmmnk<icgefhfkgmhn<ocnknmpnrntluiugtdsbq`o_l^i^f_d`bbad`g`jambodqfrislsorqqrp<pcokompn>asj^bs<j^rs<elol>_tc^cs<c^l^o_p`qbqdpfoglh<chlhoipjqlqopqorlscs>`urcqao_m^i^g_eadccfckdnepgrismsorqprn>_tc^cs<c^j^m_oapcqfqkpnopmrjscs>`sd^ds<d^q^<dhlh<dsqs>`rd^ds<d^q^<dhlh>`urcqao_m^i^g_eadccfckdnepgrismsorqprnrk<mkrk>_uc^cs<q^qs<chqh>fnj^js>brn^nnmqlrjshsfreqdndl>_tc^cs<q^cl<hgqs>`qd^ds<dsps>^vb^bs<b^js<r^js<r^rs>_uc^cs<c^qs<q^qs>_uh^f_daccbfbkcndpfrhslsnrppqnrkrfqcpan_l^h^>_tc^cs<c^l^o_p`qbqepgohlici>_uh^f_daccbfbkcndpfrhslsnrppqnrkrfqcpan_l^h^<koqu>_tc^cs<c^l^o_p`qbqdpfoglhch<jhqs>`tqao_l^h^e_caccdeefggmiojpkqmqporlshsercp>brj^js<c^q^>_uc^cmdpfrisksnrppqmq^>asb^js<r^js>^v`^es<j^es<j^os<t^os>`tc^qs<q^cs>asb^jhjs<r^jh>`tq^cs<c^q^<csqs>cqgZgz<hZhz<gZnZ<gznz>cqc^qv>cqlZlz<mZmz<fZmZ<fzmz>brj\\bj<j\\rj>asazsz>fnkcieigjhkgjfig>atpeps<phnfleiegfehdkdmepgrislsnrpp>`sd^ds<dhffhekemfohpkpmopmrkshsfrdp>asphnfleiegfehdkdmepgrislsnrpp>atp^ps<phnfleiegfehdkdmepgrislsnrpp>asdkpkpiognfleiegfehdkdmepgrislsnrpp>eqo^m^k_jbjs<gene>atpepuoxnylzizgy<phnfleiegfehdkdmepgrislsnrpp>ate^es<eihfjemeofpips>fni^j_k^j]i^<jejs>eoj^k_l^k]j^<kekvjyhzfz>are^es<oeeo<ikps>fnj^js>[y_e_s<_ibfdegeifjijs<jimfoeretfuius>ateees<eihfjemeofpips>atiegfehdkdmepgrislsnrppqmqkphnfleie>`sdedz<dhffhekemfohpkpmopmrkshsfrdp>atpepz<phnfleiegfehdkdmepgrislsnrpp>cpgegs<gkhhjfleoe>bsphofleieffehfjhkmlompopporlsisfrep>eqj^jokrmsos<gene>ateeeofrhsksmrpo<peps>brdejs<pejs>_ubefs<jefs<jens<rens>bseeps<pees>brdejs<pejshwfydzcz>bspees<eepe<esps>cqlZj[i\\h^h`ibjckekgii<j[i]i_jakbldlfkhgjkllnlpkrjsiuiwjy<ikkmkojqirhthvixjylz>fnjZjz>cqhZj[k\\l^l`kbjcieigki<j[k]k_jaibhdhfihmjilhnhpirjskukwjy<kkimiojqkrltlvkxjyhz>^vamakbhdgfghhlknlplrksi<akbidhfhhillnmpmrlsisg>brb^bscsc^d^dsese^f^fsgsg^h^hsisi^j^jsksk^l^lsmsm^n^nsoso^p^psqsq^r^rs').split('>').map(r=> { return [r.charCodeAt(0)-106,r.charCodeAt(1)-106, r.substr(2).split('<').map(a => {const ret = []; for (let i=0; i<a.length; i+=2) {ret.push(a.substr(i, 2).split('').map(b => b.charCodeAt(0)-106));} return ret; })]; });return new Text();}