<div class="message">
This pen is deprecated. Please check out the <a href="https://codepen.io/lonekorean/project/editor/ZGpqVX/" target="_blank">new project version here</a>.
</div>
<div class="wrapper">
<div class="content">
<div class="sidebar">
<h1>Align 4</h1>
<div class="panel dif">
<h2>Difficulty</h2>
<div class="dif-options">
<div>
<input id="dif1" type="radio" name="dif-options" value="1">
<label for="dif1">1</label>
</div>
<div>
<input id="dif2" type="radio" name="dif-options" value="2">
<label for="dif2">2</label>
</div>
<div>
<input id="dif3" type="radio" name="dif-options" value="3" checked>
<label for="dif3">3</label>
</div>
<div>
<input id="dif4" type="radio" name="dif-options" value="4">
<label for="dif4">4</label>
</div>
<div>
<input id="dif5" type="radio" name="dif-options" value="5">
<label for="dif5">5</label>
</div>
</div>
<div class="start">
<button>Start Game</button>
</div>
</div>
<div class="panel info">
<h2></h2>
<div class="blurb"></div>
<div class="outlook"></div>
</div>
</div>
<div class="board">
<div class="lit-columns">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
<div class="lit-cells"></div>
<div class="chips"></div>
<div class="click-columns">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
</div>
@import url(https://fonts.googleapis.com/css?family=Doppio+One);
.message {
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
padding: 10px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
background-color: #fff;
font-family: "Doppio One", sans-serif;
font-size: 20px;
}
html {
display: table;
width: 100%;
height: 100%;
}
body {
display: table-row;
background: #000 radial-gradient(1000px 500px, #3f546b, #000);
}
.wrapper {
display: table-cell;
vertical-align: middle;
text-align: center;
}
.content {
display: inline-block;
width: 668px;
margin: 0 auto;
padding: 50px 20px 10px;
}
.sidebar {
float: left;
margin-right: 20px;
width: 220px;
text-align: left;
font-family: "Doppio One", sans-serif;
color: #ccc;
}
h1, h2 {
color: #fff;
margin: 0;
font-weight: normal;
}
h1 {
height: 68px;
line-height: 68px;
font-size: 40px;
text-align: right;
}
h2 {
font-size: 18px;
}
.panel {
padding: 12px;
margin-bottom: 20px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 8px;
}
.dif-options {
clear: both;
overflow: hidden;
margin: 20px -7px 0;
}
.dif-options div {
float: left;
width: 20%;
}
.dif-options input {
display: none;
}
.dif-options label {
display: block;
margin: 0 auto;
width: 24px;
height: 24px;
background-color: #666;
border: solid 2px #ccc;
border-radius: 8px;
color: #999;
text-align: center;
line-height: 24px;
cursor: pointer;
}
.dif-options input:checked + label {
color: #fff;
background-color: #593f6b;
border-color: #fff;
cursor: default;
}
.freeze .dif-options input:not(:checked) + label {
font-size: 0;
margin: 7px auto;
width: 10px;
height: 10px;
border-radius: 4px;
color: transparent;
line-height: 10px;
cursor: default;
transition: .2s;
}
.start {
margin-top: 20px;
}
.freeze .start {
display: none;
}
.start button {
display: block;
width: 100%;
padding: 2px 12px 4px;
font-family: "Doppio One", sans-serif;
font-size: 24px;
border: solid 2px #ccc;
border-radius: 8px;
background-color: #593f6b;
color: #fff;
cursor: pointer;
}
.start button:focus {
outline: none;
}
.info div {
margin-top: 10px;
}
.board {
position: relative;
float: left;
width: 428px;
height: 428px;
margin-top: 68px;
background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/77020/board.png);
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.4);
}
.lit-columns, .lit-cells, .chips, .click-columns {
position: absolute;
width: 428px;
height: 428px;
}
.lit-columns div {
float: left;
width: 60px;
height: 426px;
margin: 1px 0 1px 1px;
transition: background-color 0.1s;
}
.lit-columns .lit {
background-color: rgba(255, 255, 255, 0.1);
transition: background-color 0.1s;
}
.lit-cells div {
position: absolute;
width: 60px;
height: 60px;
background-color: rgba(255, 255, 255, 0.3);
}
.chips div {
position: absolute;
width: 60px;
height: 60px;
backface-visibility: hidden;
}
.chips .p1 {
background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/77020/p1-chip.png);
}
.chips .p2 {
background-image: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/77020/p2-chip.png);
}
.chips .cursor {
bottom: 428px;
transition: left 0.1s ease-out;
}
.chips .dropped {
transition: bottom ease-in;
animation: bounce 0.3s;
}
.click-columns div {
float: left;
width: 61px;
height: 428px;
}
.click-columns div:first-child {
padding-left: 1px;
}
.click-columns .hover {
cursor: pointer;
}
@keyframes bounce {
0% { animation-timing-function: ease-out; transform: translateY(0); }
30% { animation-timing-function: ease-in; transform: translateY(-40px); }
60% { animation-timing-function: ease-out; transform: translateY(0); }
80% { animation-timing-function: ease-in; transform: translateY(-10px); }
100% { animation-timing-function: ease-out; transform: translateY(0); }
}
// constants
var WEB_WORKER_URL = 'https://s.codepen.io/lonekorean/pen/KnaJf.js';
var BLURBS = {
'start': {
header: 'Get Ready',
blurb: 'Select your difficulty and start the game.'
},
'p1-turn': {
header: 'Your Turn',
blurb: 'Click on the board to drop your chip.'
},
'p2-turn': {
header: 'Computer\'s Turn',
blurb: 'The computer is trying to find the best way to make you lose.'
},
'p1-win': {
header: 'You Win',
blurb: 'You are a winner. Remember this moment. Carry it with you, forever.'
},
'p2-win': {
header: 'Computer Wins',
blurb: 'Try again when you\'re done wiping your tears of shame.'
},
'tie': {
header: 'Tie',
blurb: 'Everyone\'s a winner! Or loser. Depends on how you look at it.'
}
};
var OUTLOOKS = {
'win-imminent': 'Uh oh, computer is feeling saucy!',
'loss-imminent': 'Computer is unsure. Now\'s your chance!'
};
// global variables
var worker;
var currentGameState;
// document ready
$(function() {
$('.start button').on('click', startGame);
setBlurb('start');
setOutlook();
// create worker
worker = new Worker(WEB_WORKER_URL);
worker.addEventListener('message', function(e) {
switch(e.data.messageType) {
case 'reset-done':
startHumanTurn();
break;
case 'human-move-done':
endHumanTurn(e.data.coords, e.data.isWin, e.data.winningChips, e.data.isBoardFull);
break;
case 'progress':
updateComputerTurn(e.data.col);
break;
case 'computer-move-done':
endComputerTurn(e.data.coords, e.data.isWin, e.data.winningChips, e.data.isBoardFull,
e.data.isWinImminent, e.data.isLossImminent);
break;
}
}, false);
});
function setBlurb(key) {
$('.info h2').text(BLURBS[key].header);
$('.info .blurb').text(BLURBS[key].blurb);
}
function setOutlook(key) {
var $outlook = $('.info .outlook');
if(key) {
$outlook
.text(OUTLOOKS[key])
.show();
} else {
$outlook.hide();
}
}
function startGame() {
$('.dif').addClass('freeze');
$('.dif input').prop('disabled', true);
$('.lit-cells, .chips').empty();
worker.postMessage({
messageType: 'reset',
});
}
function startHumanTurn() {
setBlurb('p1-turn');
$('.click-columns div').addClass('hover');
// if mouse is already over a column, show cursor chip there
var col = $('.click-columns div:hover').index();
if(col !== -1) {
createCursorChip(1, col);
}
$('.click-columns')
.on('mouseenter', function() {
var col = $('.click-columns div:hover').index();
createCursorChip(1, col);
})
.on('mouseleave', function() {
destroyCursorChip();
});
$('.click-columns div')
.on('mouseenter', function() {
var col = $(this).index();
moveCursorChip(col);
})
.on('click', function() {
$('.click-columns, .click-columns div').off();
var col = $(this).index();
worker.postMessage({
messageType: 'human-move',
col: col
});
});
}
function endHumanTurn(coords, isWin, winningChips, isBoardFull) {
$('.click-columns div').removeClass('hover');
if(!coords) {
// column was full, human goes again
startHumanTurn();
} else {
dropCursorChip(coords.row, function() {
if(isWin) {
endGame('p1-win', winningChips);
} else if(isBoardFull) {
endGame('tie');
} else {
// pass turn to computer
startComputerTurn();
}
});
}
}
function startComputerTurn() {
setBlurb('p2-turn');
// computer's cursor chip starts far left and moves right as it thinks
createCursorChip(2, 0);
var maxDepth = parseInt($('input[name=dif-options]:checked').val(), 10) + 1;
worker.postMessage({
messageType: 'computer-move',
maxDepth: maxDepth
});
}
function updateComputerTurn(col) {
moveCursorChip(col);
}
function endComputerTurn(coords, isWin, winningChips, isBoardFull, isWinImminent, isLossImminent) {
moveCursorChip(coords.col, function() {
dropCursorChip(coords.row, function() {
if (isWin) {
endGame('p2-win', winningChips);
} else if (isBoardFull) {
endGame('tie');
} else {
if(isWinImminent) {
setOutlook('win-imminent');
} else if (isLossImminent) {
setOutlook('loss-imminent');
} else {
setOutlook();
}
// pass turn to human
startHumanTurn();
}
});
});
}
function endGame(blurbKey, winningChips) {
$('.dif').removeClass('freeze');
$('.dif input').prop('disabled', false);
setBlurb(blurbKey);
setOutlook();
if(winningChips) {
// not a tie, highlight the chips in the winning run
for(var i = 0; i < winningChips.length; i++) {
createLitCell(winningChips[i].col, winningChips[i].row);
}
}
}
function createLitCell(col, row) {
$('<div>')
.css({
'left': indexToPixels(col),
'bottom': indexToPixels(row)
})
.appendTo('.lit-cells');
}
function createCursorChip(player, col) {
var playerClass = 'p' + player;
$('<div>', { 'class': 'cursor ' + playerClass })
.css('left', indexToPixels(col))
.appendTo('.chips');
// also highlight column
$('.lit-columns div').eq(col).addClass('lit');
}
function destroyCursorChip() {
$('.chips .cursor').remove();
$('.lit-columns .lit').removeClass('lit');
}
function moveCursorChip(col, callback) {
$('.chips .cursor').css('left', indexToPixels(col));
$('.lit-columns .lit').removeClass('lit');
$('.lit-columns div').eq(col).addClass('lit');
// callback is only used when the computer is about to drop a chip
// give it a slight delay for visual interest
setTimeout(callback, 300);
}
function dropCursorChip(row, callback) {
// speed of animation depends on how far the chip has to drop
var ms = (7 - row) * 40;
var duration = (ms / 1000) + 's';
$('.chips .cursor')
.removeClass('cursor')
.css({
'bottom': indexToPixels(row),
'-webkit-transition-duration': duration, 'transition-duration': duration,
'-webkit-animation-delay': duration, 'animation-delay': duration
})
.addClass('dropped');
$('.lit-columns .lit').removeClass('lit');
setTimeout(callback, ms);
}
function indexToPixels(index) {
return (index * 61 + 1) + 'px';
}
This Pen doesn't use any external CSS resources.