At first, I created an unbeatable Tic-Tac-Toe game in Python. However, I wanted a better interface, so I used a PHP backend that runs the script and simple JavaScript that sends user commands to the script.
index.html: (excuse stupid class names)
<html>
<head>
<script src='jquery.js'></script>
<link rel="shortcut icon" type="image/png" href="/favicon.png"/>
<link href='https://fonts.googleapis.com/css?family=Kaushan+Script' rel='stylesheet' type='text/css'>
<link href='styling.css' rel='stylesheet' type='text/css'>
<script src='code/ttt-realhard.js'></script>
<title> Tic-Tac-Toe Unbeatable </title>
<style>
table {
text-align: center;
vertical-align: middle;
font: bold 36px sans-serif;
}
p,
h1,
td,
th {
font-family: 'Lato', sans-serif;
}
table,
td,
th {
border: none;
border-collapse: collapse;
}
td, th {
text-align: center;
font-weight: 300;
height: 150px;
width: 150px;
font-size: 1em;
}
#board tr td:hover {
background: #e4e4e4;
cursor: pointer;
}
.ab_c {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
td#c0,
td#c1,
td#c2,
td#c3,
td#c4,
td#c5 {
border-bottom: 3px solid grey;
}
td#c0,
td#c1,
td#c3,
td#c4,
td#c6,
td#c7 {
border-right: 3px solid grey;
}
.lolyoucantbeatme {
color: grey;
font-weight: 400;
font-size: 4em;
}
.lolyoureallywontbutyoucantry {
color: grey;
font-weight: 100;
font-size: 2em;
}
</style>
</head>
<body>
<!-- v1.0 unbeatable-->
<!-- by joseph -->
<div id='youwontbeatme'>
<h1 class='lolyoucantbeatme ab_c' style='top:2%'> Tic Tac Toe </h1>
<p class='lolyoureallywontbutyoucantry ab_c' style='top:10%'> Try to beat me! </p>
</div>
<img src='spinner2.gif' id='spinner' width='150' height='150' style='display:none;' class='ab_c'>
<table id='board' class='ab_c' style='top:55%'>
<tr>
<td id='c0' onclick='ttt.ticclick(this)'></td><td id='c1' onclick='ttt.ticclick(this)'></td><td id='c2' onclick='ttt.ticclick(this)'></td>
</tr>
<tr>
<td id='c3' onclick='ttt.ticclick(this)'></td><td id='c4' onclick='ttt.ticclick(this)'></td><td id='c5' onclick='ttt.ticclick(this)'></td>
</tr>
<tr>
<td id='c6' onclick='ttt.ticclick(this)'></td><td id='c7' onclick='ttt.ticclick(this)'></td><td id='c8' onclick='ttt.ticclick(this)'></td>
</tr>
</table>
<div id='lawl'class="ab_c" style="left: 90%;white-space: nowrap;display: none;"><p id='status'></p></div>
</body>
</html>
ttt-realhard.js:
function getRandomInt (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
ttt = {
"ticclick": function(cell) {
if ($(cell).html() == '' && !ttt.system.laptopresponse) { $(cell).html('X'); ttt.system.lock();ttt.system.freecode();ttt.system.help(); }
// TODO: Wait for computer move.
},
"realz": [
'#c0','#c1','#c2',
'#c3','#c4','#c5',
'#c6','#c7','#c8'
],
}
ttt['system'] = {
"freecode": function() {
for (var i = 0; i < ttt.realz.length; i++) {
var ab = $(ttt.realz[i]);
if (ab.html() == "X") {
ab.css('background-color', '#b3b3ff');
} else if (ab.html() == 'O') {
ab.css('background-color', '#ffcccc');
}
}
},
"coolstatus": function(logic, step) {
if (!logic) return; // stupid hack, too lazy to figure out why sometimes logic is not passed.
var step = step || 0;
if (step > logic.length) {$('#lawl').hide();return;}
$('#lawl').show().html(logic[step]);
step++;
setTimeout(function() { ttt.system.coolstatus(logic, step); }, 50)
},
"boardencode": function(board) {
/* Encodes a board to a format that is easily transferred. */
var buffer = "";
for (var i = 0; i < board.length; i++) {
buffer += (board[i] == '' ? '-' : board[i]); // ternary operators are cool
}
return buffer;
},
"laptopresponse": false,
"lock": function() {
console.log('locked');
$('#spinner').show();
$('#youwontbeatme').hide();
ttt.system.laptopresponse = true;
},
"unlock": function() {
console.log('unlocked');
$('#spinner').hide();
$('#youwontbeatme').show();
ttt.system.laptopresponse = false;
},
"isWaitingForLaptop": function() {
return ttt.system.laptopresponse;
},
"parseServerResponse": function(dat) {
// Parse board.
ab = JSON.parse(dat);
ttt.system.coolstatus(ab.Board.Logic);
for (var i = 0; i < ab.Board.DBoard.length; i++) {
$("#c" + i.toString()).html(null == ab.Board.DBoard[i] ? "" : ab.Board.DBoard[i]);
}
// Now, check for a winner.
ttt.system.freecode();
if (ab.Board.Complete) {
// Someone won...
// Check for a tie, perhaps?
if (!ab.Board.Winner) {
// Tie!
console.log('Tie.');
$('#spinner').css('width', '200px').css('height','200px');
$('#youwontbeatme').show().html("<h1 class='lolyoucantbeatme ab_c' style='top:2%'> TIE! </h1>");
$('.lolyoucantbeatme').css('color', 'orange');
setTimeout(function(){window.location.replace("index.html")},5e3);
//$('#lawl').show().html('<a href="easy/"> Play the easy version instead? </a>');
return 0;
} else if (ab.Board.Winner == 'O') {
console.log('Computer win.');
$('#youwontbeatme').show();
$('#youwontbeatme').html("<h1 class='lolyoucantbeatme ab_c' style='top:2%'> I won. </h1>");
$('.lolyoucantbeatme').css('color', 'red');
$('#c'+ab.Board.WinningCombo[0].toString()).css('font-weight','bold').css('color','red');
$('#c'+ab.Board.WinningCombo[1].toString()).css('font-weight','bold').css('color','red');
$('#c'+ab.Board.WinningCombo[2].toString()).css('font-weight','bold').css('color','red');
setTimeout(function(){window.location.replace("index.html")},5e3);
//$('#lawl').show().html('<a href="easy/"> Play the easy version instead? </a>');
return 0;
}
}
// No one won. Unlock board.
console.log('unlocking board cuz no one won');
ttt.system.unlock();
},
"help": function() {
console.log('Sending to server: ' + ttt.system.boardencode(ttt.system.collect()))
// Help!
pre='inhumane';
jQuery.get("code/server.php?mode="+pre+"&data=" + ttt.system.boardencode(ttt.system.collect()), ttt.system.parseServerResponse);
},
"collect": function() {
var buffer = [];
for (var i = 0; i < ttt.realz.length; i++) {
buffer[i] = $(ttt.realz[i]).html();
}
return buffer;
}
}
var goFirst = Math.floor((Math.random() * 2) + 1);
if (goFirst == 1) {
$(document).ready(ttt.system.help);
}
server.php:
<?php
isset($_GET['mode']) || die('ERR_NOMODE');
isset($_GET['data']) || die('ERR_NODATA');
$MODE = $_GET['mode'];
$BOARDDATA = $_GET['data'];
$INVOKE '/Users/joe/Desktop/ScienceFair/ttt/unbeatable_raw.py %s';
$RESULT = exec(sprintf($INVOKE, $BOARDDATA));
// echo('Invoked '. sprintf($INVOKE, $BOARDDATA));
echo($RESULT);
?>
The guts (Python script):
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import random, os, sys, json
class Tic(object):
lastwincombo = []
logic = []
winning_combos = (
[0, 1, 2], [3, 4, 5], [6, 7, 8],
[0, 3, 6], [1, 4, 7], [2, 5, 8],
[0, 4, 8], [2, 4, 6])
winners = ('X-win', 'Draw', 'O-win')
def __init__(self, squares=[]):
if len(squares) == 0:
self.squares = [None for i in range(9)]
else:
self.squares = squares
def available_moves(self):
return [k for k, v in enumerate(self.squares) if v is None]
def available_combos(self, player):
return self.available_moves() + self.get_squares(player)
def complete(self):
if None not in [v for v in self.squares]:
return True
if self.winner() != None:
return True
return False
def X_won(self):
return self.winner() == 'X'
def O_won(self):
return self.winner() == 'O'
def tied(self):
return self.complete() == True and self.winner() is None
def winner(self, lastcheck = False):
for player in ('X', 'O'):
positions = self.get_squares(player)
for iterator, combo in enumerate(self.winning_combos):
win = True
for pos in combo:
if pos not in positions:
win = False
if win:
self.lastwincombo = combo
return player
return None
def get_squares(self, player):
return [k for k, v in enumerate(self.squares) if v == player]
def make_move(self, position, player):
self.squares[position] = player
def alphabeta(self, node, player, alpha, beta):
if node.complete():
if node.X_won():
return -1
elif node.tied():
return 0
elif node.O_won():
return 1
for move in node.available_moves():
node.make_move(move, player)
val = self.alphabeta(node, get_enemy(player), alpha, beta)
node.make_move(move, None)
if player == 'O':
if val > alpha:
alpha = val
if alpha >= beta:
return beta
else:
if val < beta:
beta = val
if beta <= alpha:
return alpha
if player == 'O':
return alpha
else:
return beta
def determine(board, player):
a = -2
choices = []
if len(board.available_moves()) == 9:
return 4
for move in board.available_moves():
board.make_move(move, player)
val = board.alphabeta(board, get_enemy(player), -2, 2)
board.make_move(move, None)
board.logic.append("move:{} and it causes:{}; ".format(move,board.winners[val + 1]))
if val > a:
a = val
choices = [move]
elif val == a:
choices.append(move)
return random.choice(choices)
def get_enemy(player):
if player == 'X':
return 'O'
return 'X'
a = sys.argv[1]
a=str.upper(a)
a=list(a)
for i,v in enumerate(a):
if v == '-':
a[i] = None
if __name__ == "__main__":
board = Tic(a)
dat = {"Board": {"Complete": True, "NextMove": 0, "Winner": None, "WinningCombo": [], "Logic": None, "DBoard": []}}
#board.show()
player = 'X'
if board.complete():
dat['Board']['Winner'] = board.winner()
dat['Board']['WinningCombo'] = board.lastwincombo;
dat['Board']['DBoard'] = board.squares
print(json.dumps(dat))
sys.exit()
player = get_enemy(player)
computer_move = determine(board, player)
board.make_move(computer_move, 'O')
if board.complete():
dat['Board']['Winner'] = board.winner()
dat['Board']['WinningCombo'] = board.lastwincombo;
dat['Board']['DBoard'] = board.squares
print(json.dumps(dat))
sys.exit()
dat['Board']['Logic'] = board.logic
dat['Board']['DBoard'] = board.squares
dat['Board']['Complete'] = False
dat['Board']['NextMove'] = computer_move
print(json.dumps(dat))