File talk:Tictactoe-X.svg

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search

This concept originally appeared in Ian Stewart's "Mathematical Recreations" section of Scientific American http://www.ncbi.nlm.nih.gov/pubmed/10914406 Which in turn was based on "Fractal Images of Formal Systems," The Journal of Philosophical Logic 26: 181-222, 1997 http://www.pgrim.org/fractal/index.html It was certainly popularized by xkcd who deserves credit for that.

Source of image[edit]

This file and related files were generated by a computer program that I wrote, and which I also hereby release under CC SA-3.0 and GFDL licences:

''' inspired by xkcd: Tic-Tac-Toe [http://xkcd.com/832/] '''

def svg_prologue(f):
    f.write('''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   version="1.1">
 <defs>
  <style type="text/css">
   .XO { /* general styling for Xs and Os */
    fill: none;
    stroke-linecap: butt;
    stroke-linejoin: miter;
    stroke-opacity: 1;
   }
   .XO_1 { /* level 1 X/O */
    stroke-width: 27px;
   }
   .XO_2 { /* level 2 X/O */
    stroke-width: 9px;
   }
   .XO_3 { /* level 3 X/O */
    stroke-width: 3px;
   }
   .XO_4 { /* level 4 X/O */
    stroke-width: 1px;
   }
   .XO_5 { /* level 5 X/O */
    stroke-width: .33px;
   }
   .XO_sel { /* optimal move */
    stroke: red;
   }
   .XO_unsel { /* already-placed move */
    stroke: black;
   }
   .X { /* normal X */
   }
   .O { /* normal O */
   }
   .win3 { /* 3-in-a-row line */
    fill: none;
    stroke: black;
    stroke-linecap: butt;
    stroke-linejoin: miter;
    stroke-opacity: 1;
   }
   .win3_2 { /* win line after 4 or 5 moves */
    stroke-width: 6px;
   }
   .win3_3 { /* win line after 6 or 7 moves */
    stroke-width: 2px;
   }
   .win3_4 { /* win line after 8 or 9 moves */
    stroke-width: .66px;
   }
   .win3X { /* 3-in-a-row line for X */
   }
   .win3O { /* 3-in-a-row line for O */
   }
   .rect_win { /* rectangle for a win */
    fill: none;
    stroke: black;
    stroke-width: 1px;
    stroke-linecap: butt;
    stroke-linejoin: miter;
    stroke-opacity: 1;
   }
   .rect_draw { /* rectangle for a draw */
    fill: none;
    stroke: black;
    stroke-width: 1px;
    stroke-linecap: butt;
    stroke-linejoin: miter;
    stroke-opacity: 1;
   }
   .rect_subdiv { /* rectangle for an unresolved board */
    fill: none;
    stroke: none;
   }
   .rect_sel { /* rectangle for a single selected piece */
    fill: #ccc;
    stroke: none;
   }
   .rect_unsel { /* rectangle for a single unselected piece */
    fill: none;
    stroke: none;
   }
  </style>
 </defs>
''')

def svg_epilogue(f):
    f.write('''
</svg>''')

WINS = [ # ((p0,p1,p2), (x0,y0, x1,y1))
    ((0,1,2), (0,1./6, 1,1./6)), # row0
    ((3,4,5), (0,3./6, 1,3./6)), # row1
    ((6,7,8), (0,5./6, 1,5./6)), # row2
    ((0,3,6), (1/6.,0, 1./6,1)), # col0
    ((1,4,7), (3/6.,0, 3./6,1)), # col0
    ((2,5,8), (5/6.,0, 5./6,1)), # col0
    ((0,4,8), (0,0, 1,1)), # diag-l2r
    ((2,4,6), (1,0, 0,1)), # diag-r2l
]

def check_win(board):
    for p, l in WINS:
        for player in 'XO':
            if all(board[x]==player for x in p):
                return (player, p, l)
    return None

RESULTS = {'O':{}, 'X':{}}
BESTMOVE = {'O':{}, 'X':{}}

def other(x):
    return {'O':'X','X':'O'}[x]

# Optimality is defined as the *shortest time to a win*, for the sake of cleaner output.
def cacher(func):
    def f(board, player):
        if board in RESULTS[player]:
            return RESULTS[player][board]
        res = None
        out = check_win(board)
        if out:
            # switch 0,1 and 100,1 around to play losers' Tic-Tac-Toe
            if out[0] == player: res = 0,1
            else: res = 100,1
        elif ' ' not in board:
            res = 0,1
        if res is None:
            res = func(board, player)
        RESULTS[player][board] = res
        return res
    return f

@cacher
def my_move(board, player):
    # My turn. Pick the "optimal" move.
    best = 9999
    bestctr = 9999
    curctr = 0
    besti = -1
    for i, x in enumerate(board):
        if x != ' ': continue
        newboard = board[:i]+player+board[i+1:]
        res, ctr = other_move(newboard, player)
        res += 1
        if res < best:
            best = res
            bestctr = ctr
            curctr = ctr
            besti = i
        elif res == best:
            if ctr < bestctr:
                bestctr = ctr
                besti = i
            curctr += ctr
    BESTMOVE[player][board] = besti
    return best, curctr

@cacher
def other_move(board, player):
    # Other guy's turn to move. Consider all of his possible moves.
    worst = 0
    curctr = 0
    for i, x in enumerate(board):
        if x != ' ': continue
        newboard = board[:i]+other(player)+board[i+1:]
        res, ctr = my_move(newboard, player)
        res += 1
        if res > worst:
            worst = res
            curctr = ctr
        elif res == worst:
            curctr += ctr
    return worst, curctr

PADDING = 1/50.
def subpos(pos, i):
    x,y,s,l = pos
    s/=3.
    x+=s*PADDING
    y+=s*PADDING
    return (x+(i%3)*s, y+(i//3)*s, s-2*s*PADDING, l+1)

def svg_rect(f, rectclass, pos, hist):
    x,y,s,l = pos
    f.write(' '*l + '<rect x="%f" y="%f" width="%f" height="%f" id="r%s" class="rect_%s" />\n'%(x, y, s, s, hist, rectclass))

def svg_board(f, board, player, pos, hist):
    x,y,s,l = pos
    # special case: pass in 'sel' or 'unsel' to display a single mark
    if board in ('sel', 'unsel'):
        svg_rect(f, board, pos, hist)
        if player == 'X':
            f.write(' '*l + '<path d="m %f,%f %f,%f m %f,%f %f,%f" id="xo%s" class="X XO XO_%d XO_%s" />\n'%(x,y, s,s, 0,-s, -s,s, hist, l, board))
        elif player == 'O':
            f.write(' '*l + '<circle cx="%f" cy="%f" r="%f" id="xo%s" class="O XO XO_%d XO_%s" />\n'%(x+s/2., y+s/2., s/2., hist, l, board))
        return

    if board in BESTMOVE[player]:
        best = BESTMOVE[player][board]
        board = board[:best]+player+board[best+1:]
        hist+=str(best)
    else:
        best = -1

    f.write(' '*l + '<g id="g%s">\n'%hist)

    win = check_win(board)

    # forced move: don't expand the node
    if win is None and board.count(' ') == 1:
        i = board.find(' ')
        board = board[:i]+other(player)+board[i+1:]

    win = check_win(board)

    if win:
        svg_rect(f, 'win', pos, hist)
    elif ' ' not in board:
        svg_rect(f, 'draw', pos, hist)
    else:
        svg_rect(f, 'subdiv', pos, hist)

    # draw all the static elements
    for i,pt in enumerate(board):
        if pt not in 'XO': continue
        if i == best:
            svg_board(f, 'sel', player, subpos(pos, i), hist+'_'+str(i))
        else:
            svg_board(f, 'unsel', pt, subpos(pos, i), hist+'_'+str(i))

    # check for a win and draw the win line appropriately
    if win:
        p, pt, ln = win
        x1, y1, x2, y2 = ln
        f.write(' '*l + ' <line x1="%f" y1="%f" x2="%f" y2="%f" class="win3 win3_%d win3%s" />\n'%(x1*s+x, y1*s+y, x2*s+x, y2*s+y, l, p))
        f.write(' '*l + '</g>\n')
        return

    for i,pt in enumerate(board):
        if pt != ' ': continue
        newboard = board[:i]+other(player)+board[i+1:]
        svg_board(f, newboard, player, subpos(pos, i), hist+str(i))

    f.write(' '*l + '</g>\n')

if __name__ == '__main__':
    my_move(' '*9, 'X')
    other_move(' '*9, 'O')

    f=open("ttt-X.svg", "w")
    svg_prologue(f)
    svg_board(f, ' '*9, 'X', (0, 0, 1000, 0), '')
    svg_epilogue(f)
    f.close()

    f=open("ttt-O.svg", "w")
    svg_prologue(f)
    for i in xrange(9):
        svg_board(f, ' '*i + 'X' + ' '*(8-i), 'O', subpos((0, 0, 1000, 0), i), str(i))
    svg_epilogue(f)
    f.close()

They are not the result of "tracing" the comic image. nneonneo (talk) 14:24, 11 January 2011 (UTC)[reply]