RosettaCodeData/Task/Tic-tac-toe/JavaScript/tic-tac-toe-1.js

437 lines
18 KiB
JavaScript

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>TicTacToe</title>
</head>
<body>
<canvas id="canvas" width="400" height="400"></canvas>
<script>
//All helper functions
isBetween = (num, a, b) => {
return num >= a && num <= b;
}
randInt = (low, high) => {
return Math.floor(Math.random() * (high - low + 1)) + low;
}
choice = arr => {
return arr[randInt(0, arr.length - 1)];
}
//Checks if every value in an array equals an item
equals = (arr, item) => {
return arr.filter(a => {
return a === item;
}).length === arr.length;
}
//Returns number of items in array that equal an item
equallen = (arr, item) => {
return arr.filter(a => {
return a === item;
}).length
}
//Checks if any value in the array equals an item
equalanyof = (arr, item) => {
return equallen(arr, item) > 0;
}
//Should be scalable, but it uses default elements for calculations and tracking
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
const width = canvas.width;
const blockSize = canvas.width / 3;
const lineSize = blockSize / 5;
//Draws background
ctx.fillStyle = "rgb(225, 225, 225)";
ctx.fillRect(0, 0, 400, 400);
//Title page
ctx.fillStyle = "rgb(0, 0, 0)";
ctx.font = width / (250 / 17) + "px Arial"; //34
ctx.textAlign = "center";
ctx.fillText("Tic Tac Toe", width / 2, width / (2 + 2 / 3)); //200, 150
//Button for starting
ctx.fillStyle = "rgb(200, 200, 200)";
ctx.fillRect(width / 3.2, width / 2, width / (2 + 2 / 3), width / 8); //125, 200, 150, 50
ctx.fillStyle = "rgb(0, 0, 0)";
ctx.font = width / (200 / 9) + "px Arial"; //18
ctx.fillText("Start", width / 2, width / (40 / 23)); //200, 230
//Uses an array so a forEach loop can scan it for the correct tile
let tileArray = []; //Contains all tiles
let available = []; //Contains only available tiles
class Tile {
constructor(x, y) {
this.x = x * blockSize;
this.y = y * blockSize;
this.state = "none";
tileArray.push(this);
available.push(this);
}
draw() {
ctx.strokeStyle = "rgb(175, 175, 175)";
ctx.lineWidth = blockSize / 10;
if (this.state === "X") {
ctx.beginPath();
ctx.moveTo(this.x + blockSize / 4, this.y + blockSize / 4);
ctx.lineTo(this.x + blockSize / (4 / 3), this.y + blockSize / (4 / 3));
ctx.moveTo(this.x + blockSize / 4, this.y + blockSize / (4 / 3));
ctx.lineTo(this.x + blockSize / (4 / 3), this.y + blockSize / 4);
ctx.stroke();
} else if (this.state === "O") {
ctx.beginPath();
ctx.arc(this.x + blockSize / 2, this.y + blockSize / 2, blockSize / 4, 0, 2 * Math.PI);
ctx.stroke();
}
//Removes this from the available array
const ind = available.indexOf(this);
available = available.slice(0, ind).concat(available.slice(ind + 1, available.length));
}
}
//Defines the game
let game = {
state: "start",
turn: "Player",
player: "X",
opp: "O"
}
//Generates tiles
for (let x = 0; x < 3; x++) {
for (let y = 0; y < 3; y++) {
new Tile(x, y);
}
}
//Gets the mouse position
getMousePos = evt => {
let rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
}
}
//Checks for win conditions
checkCondition = () => {
//Local variables are created to make access easier
let as = tileArray[0].state;
let bs = tileArray[1].state;
let cs = tileArray[2].state;
let ds = tileArray[3].state;
let es = tileArray[4].state;
let fs = tileArray[5].state;
let gs = tileArray[6].state;
let hs = tileArray[7].state;
let is = tileArray[8].state;
//Equals function checks if each value in the array has a state of X or O
if (equals([as, bs, cs], "X") || equals([ds, es, fs], "X") || equals([gs, hs, is], "X") ||
equals([as, ds, gs], "X") || equals([bs, es, hs], "X") || equals([cs, fs, is], "X") ||
equals([as, es, is], "X") || equals([cs, es, gs], "X")) {
alert("Player wins!");
game.state = "over";
} else if (equals([as, bs, cs], "O") || equals([ds, es, fs], "O") || equals([gs, hs, is], "O") ||
equals([as, ds, gs], "O") || equals([bs, es, hs], "O") || equals([cs, fs, is], "O") ||
equals([as, es, is], "O") || equals([cs, es, gs], "O")) {
alert("Opponent wins!");
game.state = "over";
//It is a tie if none of the above conditions are fulfilled and there are no available tiles
} else if (available.length === 0) {
alert("It's a tie!");
game.state = "over";
}
}
//Controls the opponent. Uses many nested switches/if-else for efficiency
oppTurn = () => {
if (game.state === "game") {
let tile = 0;
//Similar local variables as the win checker
let at = tileArray[0].state;
let bt = tileArray[1].state;
let ct = tileArray[2].state;
let dt = tileArray[3].state;
let et = tileArray[4].state;
let ft = tileArray[5].state;
let gt = tileArray[6].state;
let ht = tileArray[7].state;
let it = tileArray[8].state;
let all = [at, bt, ct, dt, et, ft, gt, ht, it];
/*The AI will automatically win if possible
I considered using a filter based system, but it was ugly and
inelegant, and also redundant
I used a nested if-else instead
Equallen checks how many values in the array equal the given value*/
if (equallen(all, "O") >= 2) {
if (equallen([at, bt, ct], "O") === 2 && equallen([at, bt, ct], "X") === 0) {
if (at === "none") {
tile = tileArray[0];
} else if (bt === "none") {
tile = tileArray[1];
} else if (ct === "none") {
tile = tileArray[2];
}
} else if (equallen([dt, et, ft], "O") === 2 && equallen([dt, et, ft], "X") === 0) {
if (dt === "none") {
tile = tileArray[3];
} else if (et === "none") {
tile = tileArray[4];
} else if (ft === "none") {
tile = tileArray[5];
}
} else if (equallen([gt, ht, it], "O") === 2 && equallen([gt, ht, it], "X") === 0) {
if (gt === "none") {
tile = tileArray[6];
} else if (ht === "none") {
tile = tileArray[7];
} else if (it === "none") {
tile = tileArray[8];
}
} else if (equallen([at, dt, gt], "O") === 2 && equallen([at, dt, gt], "X") === 0) {
if (at === "none") {
tile = tileArray[0];
} else if (dt === "none") {
tile = tileArray[3];
} else if (gt === "none") {
tile = tileArray[6];
}
} else if (equallen([bt, et, ht], "O") === 2 && equallen([bt, et, ht], "X") === 0) {
if (bt === "none") {
tile = tileArray[1];
} else if (et === "none") {
tile = tileArray[4];
} else if (ht === "none") {
tile = tileArray[7];
}
} else if (equallen([ct, ft, it], "O") === 2 && equallen([ct, ft, it], "X") === 0) {
if (ct === "none") {
tile = tileArray[2];
} else if (ft === "none") {
tile = tileArray[5];
} else if (it === "none") {
tile = tileArray[8];
}
} else if (equallen([at, et, it], "O") === 2 && equallen([at, et, it], "X") === 0) {
if (at === "none") {
tile = tileArray[0];
} else if (et === "none") {
tile = tileArray[4];
} else if (it === "none") {
tile = tileArray[8];
}
} else if (equallen([ct, et, gt], "O") === 2 && equallen([ct, et, gt], "X") === 0) {
if (ct === "none") {
tile = tileArray[2];
} else if (et === "none") {
tile = tileArray[4];
} else if (gt === "none") {
tile = tileArray[6];
}
}
}
//Stops player from winning if possible
if (equallen(all, "X") >= 2 && tile === 0) {
if (equallen([at, bt, ct], "X") === 2 && equallen([at, bt, ct], "O") === 0) {
if (at === "none") {
tile = tileArray[0];
} else if (bt === "none") {
tile = tileArray[1];
} else if (ct === "none") {
tile = tileArray[2];
}
} else if (equallen([dt, et, ft], "X") === 2 && equallen([dt, et, ft], "O") === 0) {
if (dt === "none") {
tile = tileArray[3];
} else if (et === "none") {
tile = tileArray[4];
} else if (ft === "none") {
tile = tileArray[5];
}
} else if (equallen([gt, ht, it], "X") === 2 && equallen([gt, ht, it], "O") === 0) {
if (gt === "none") {
tile = tileArray[6];
} else if (ht === "none") {
tile = tileArray[7];
} else if (it === "none") {
tile = tileArray[8];
}
} else if (equallen([at, dt, gt], "X") === 2 && equallen([at, dt, gt], "O") === 0) {
if (at === "none") {
tile = tileArray[0];
} else if (dt === "none") {
tile = tileArray[3];
} else if (gt === "none") {
tile = tileArray[6];
}
} else if (equallen([bt, et, ht], "X") === 2 && equallen([bt, et, ht], "O") === 0) {
if (bt === "none") {
tile = tileArray[1];
} else if (et === "none") {
tile = tileArray[4];
} else if (ht === "none") {
tile = tileArray[7];
}
} else if (equallen([ct, ft, it], "X") === 2 && equallen([ct, ft, it], "O") === 0) {
if (ct === "none") {
tile = tileArray[2];
} else if (ft === "none") {
tile = tileArray[5];
} else if (it === "none") {
tile = tileArray[8];
}
} else if (equallen([at, et, it], "X") === 2 && equallen([at, et, it], "O") === 0) {
if (at === "none") {
tile = tileArray[0];
} else if (et === "none") {
tile = tileArray[4];
} else if (it === "none") {
tile = tileArray[8];
}
} else if (equallen([ct, et, gt], "X") === 2 && equallen([ct, et, gt], "O") === 0) {
if (ct === "none") {
tile = tileArray[2];
} else if (et === "none") {
tile = tileArray[4];
} else if (gt === "none") {
tile = tileArray[6];
}
}
}
//Other options in case the above are not fulfilled
//Controls the course of play over the game
if (tile === 0) {
switch (9 - available.length) {
case 1:
//If the center is taken, it plays randomly in the corner
//Otherwise, it takes the center
if (et === "X") {
tile = tileArray[choice([0, 2, 6, 8])];
} else {
tile = tileArray[4];
}
break;
case 3:
if (et === "X" && (equalanyof([at, ct, gt, it], "O"))) {
/*To counter the strategy of
O - -
- X -
X - -
O - -
- X -
- - X
and related strategies*/
if (at === "X") {
if (it === "none") {
tile = tileArray[8];
} else {
tile = tileArray[choice([2, 6])];
}
} else if (ct === "X") {
if (gt === "none") {
tile = tileArray[6];
} else {
tile = tileArray[choice([0, 8])];
}
} else if (gt === "X") {
if (ct === "none") {
tile = tileArray[2];
} else {
tile = tileArray[choice([0, 8])];
}
} else if (it === "X") {
if (at === "none") {
tile = tileArray[0];
} else {
tile = tileArray[choice([2, 6])];
}
}
} else {
tile = choice(tileArray);
}
break;
}
}
//Generates a random number if it could cause an error
if (tile.state != "none") {
tile = choice(available);
}
//Draws the selection
tile.state = game.opp;
tile.draw();
checkCondition();
game.turn = "Player";
}
}
//Click handler
document.onclick = event => {
let pos = getMousePos(event);
switch (game.state) {
case "start":
//Checks if the button was clicked
if (isBetween(pos.x, width / 3.2, width / (16 / 11)) && isBetween(pos.y, width / 2, width / 1.6)) {
game.state = "game"
//Draws the setup for the game
ctx.fillStyle = "rgb(225, 225, 225)";
ctx.fillRect(0, 0, 400, 400);
//Draws the lines
ctx.fillStyle = "rgb(200, 200, 200)";
ctx.fillRect(blockSize - lineSize / 2, 0, lineSize, width);
ctx.fillRect(blockSize * 2 - lineSize / 2, 0, lineSize, width);
ctx.fillRect(0, blockSize - lineSize / 2, width, lineSize);
ctx.fillRect(0, blockSize * 2 - lineSize / 2, width, lineSize);
}
break;
case "game":
if (game.turn === "Player") {
//Goes through the tile array, checking if the click occurred there
tileArray.forEach(tile => {
if (isBetween(pos.x, tile.x, tile.x + blockSize) && isBetween(pos.y, tile.y, tile.y + blockSize)) {
if (available.indexOf(tile) != -1) {
tile.state = game.player;
tile.draw();
checkCondition();
game.turn = "Opponent";
oppTurn();
}
}
});
}
break;
}
}
</script>
</body>
</html>