HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URL's added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using it's URL and the proper URL extention.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by Skypack, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ES6 import
usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<div class="container">
<div id="score">
<div class="timer">
<button id="play-pause">
<i id="play">Play</i>
<i id="pause">Pause</i>
</button>
<label>Timer:</label>
<span>00:00</span>
</div>
<div class="move-count" data-moves="0">
<label>Moves:</label>
<span>0</span>
</div>
<div class="score" data-score="0">
<label>Score:</label>
<span>0</span>
</div>
</div>
<div id="table">
<div class="upper-row">
<div id="stock" class="stock pile" data-pile="stock">
<i class="reload-icon" data-action="reload">
<span></span>
</i>
<ul></ul>
</div>
<div id="waste" class="waste pile" data-pile="waste">
<ul></ul>
</div>
<ul id="fnd" class="fnd">
<li id="spades" class="spades pile" data-pile="spades" data-empty="true"><ul></ul></li>
<li id="hearts" class="hearts pile" data-pile="hearts" data-empty="true"><ul></ul></li>
<li id="diamonds" class="diamonds pile" data-pile="diamonds" data-empty="true"><ul></ul></li>
<li id="clubs" class="clubs pile" data-pile="clubs" data-empty="true"><ul></ul></li>
</ul>
</div>
<div class="lower-row">
<ul id="tab" class="tab">
<li class="pile" data-pile="1"><ul></ul></li>
<li class="pile" data-pile="2"><ul></ul></li>
<li class="pile" data-pile="3"><ul></ul></li>
<li class="pile" data-pile="4"><ul></ul></li>
<li class="pile" data-pile="5"><ul></ul></li>
<li class="pile" data-pile="6"><ul></ul></li>
<li class="pile" data-pile="7"><ul></ul></li>
</ul>
</div>
</div>
</div><!-- /.container -->
<button id="auto-win">Auto Win</button>
<canvas id="confetti"></canvas>
<ul class="template">
<li data-rank="2">
<div class="two {{suit}}">
<div class="corner top">
<span class="rank">2</span>
<span class="suit"></span>
</div>
<span class="suit top_center"></span>
<span class="suit bottom_center"></span>
<div class="corner bottom">
<span class="rank">2</span>
<span class="suit"></span>
</div>
</div>
</li>
<li data-rank="3">
<div class="three {{suit}}">
<div class="corner top">
<span class="rank">3</span>
<span class="suit"></span>
</div>
<span class="suit top_center"></span>
<span class="suit middle_center"></span>
<span class="suit bottom_center"></span>
<div class="corner bottom">
<span class="rank">3</span>
<span class="suit"></span>
</div>
</div>
</li>
<li data-rank="4">
<div class="four {{suit}}">
<div class="corner top">
<span class="rank">4</span>
<span class="suit"></span>
</div>
<span class="suit top_left"></span>
<span class="suit top_right"></span>
<span class="suit bottom_left"></span>
<span class="suit bottom_right"></span>
<div class="corner bottom">
<span class="rank">4</span>
<span class="suit"></span>
</div>
</div>
</li>
<li data-rank="5">
<div class="five {{suit}}">
<div class="corner top">
<span class="rank">5</span>
<span class="suit"></span>
</div>
<span class="suit top_left"></span>
<span class="suit top_right"></span>
<span class="suit middle_center"></span>
<span class="suit bottom_left"></span>
<span class="suit bottom_right"></span>
<div class="corner bottom">
<span class="rank">5</span>
<span class="suit"></span>
</div>
</div>
</li>
<li data-rank="6">
<div class="six {{suit}}">
<div class="corner top">
<span class="rank">6</span>
<span class="suit"></span>
</div>
<span class="suit top_left"></span>
<span class="suit top_right"></span>
<span class="suit middle_left"></span>
<span class="suit middle_right"></span>
<span class="suit bottom_left"></span>
<span class="suit bottom_right"></span>
<div class="corner bottom">
<span class="rank">6</span>
<span class="suit"></span>
</div>
</div>
</li>
<li data-rank="7">
<div class="seven {{suit}}">
<div class="corner top">
<span class="rank">7</span>
<span class="suit"></span>
</div>
<span class="suit top_left"></span>
<span class="suit top_right"></span>
<span class="suit middle_left"></span>
<span class="suit middle_top"></span>
<span class="suit middle_right"></span>
<span class="suit bottom_left"></span>
<span class="suit bottom_right"></span>
<div class="corner bottom">
<span class="rank">7</span>
<span class="suit"></span>
</div>
</div>
</li>
<li data-rank="8">
<div class="eight {{suit}}">
<div class="corner top">
<span class="rank">8</span>
<span class="suit"></span>
</div>
<span class="suit top_left"></span>
<span class="suit top_right"></span>
<span class="suit middle_left"></span>
<span class="suit middle_top_center"></span>
<span class="suit middle_right"></span>
<span class="suit middle_bottom_center"></span>
<span class="suit bottom_left"></span>
<span class="suit bottom_right"></span>
<div class="corner bottom">
<span class="rank">8</span>
<span class="suit"></span>
</div>
</div>
</li>
<li data-rank="9">
<div class="nine {{suit}}">
<div class="corner top">
<span class="rank">9</span>
<span class="suit"></span>
</div>
<span class="suit top_left"></span>
<span class="suit top_right"></span>
<span class="suit middle_top_left"></span>
<span class="suit middle_center"></span>
<span class="suit middle_top_right"></span>
<span class="suit bottom_left"></span>
<span class="suit bottom_right"></span>
<span class="suit middle_bottom_left"></span>
<span class="suit middle_bottom_right"></span>
<div class="corner bottom">
<span class="rank">9</span>
<span class="suit"></span>
</div>
</div>
</li>
<li data-rank="10">
<div class="ten {{suit}}">
<div class="corner top">
<span class="rank">10</span>
<span class="suit"></span>
</div>
<span class="suit top_left"></span>
<span class="suit top_right"></span>
<span class="suit middle_top_left"></span>
<span class="suit middle_top_center"></span>
<span class="suit middle_top_right"></span>
<span class="suit bottom_left"></span>
<span class="suit bottom_right"></span>
<span class="suit middle_bottom_center"></span>
<span class="suit middle_bottom_left"></span>
<span class="suit middle_bottom_right"></span>
<div class="corner bottom">
<span class="rank">10</span>
<span class="suit"></span>
</div>
</div>
</li>
<li data-rank="J">
<div class="jack {{suit}}">
<div class="corner top">
<span class="rank">J</span>
<span class="suit"></span>
</div>
<span class="face middle_center"></span>
<div class="corner bottom">
<span class="rank">J</span>
<span class="suit"></span>
</div>
</div>
</li>
<li data-rank="Q">
<div class="queen {{suit}}">
<div class="corner top">
<span class="rank">Q</span>
<span class="suit"></span>
</div>
<span class="face middle_center"></span>
<div class="corner bottom">
<span class="rank">Q</span>
<span class="suit"></span>
</div>
</div>
</li>
<li data-rank="K">
<div class="king {{suit}}">
<div class="corner top">
<span class="rank">K</span>
<span class="suit"></span>
</div>
<span class="face middle_center"></span>
<div class="corner bottom">
<span class="rank">K</span>
<span class="suit"></span>
</div>
</div>
</li>
<li data-rank="A">
<div class="ace {{suit}}">
<div class="corner top">
<span class="rank">A</span>
<span class="suit"></span>
</div>
<span class="suit middle_center"></span>
<div class="corner bottom">
<span class="rank">A</span>
<span class="suit"></span>
</div>
</div>
</li>
</ul><!-- /.templates -->
/* Thanks to
@donpark on GitHub Scalable CSS Playing Cards
https://donpark.github.io/scalable-css-playing-cards/ */
@font-face {
font-family: 'Suit-Regular';
src: url("https://bfa.github.io/solitaire-js/fonts/suit-regular.eot");
src: url("https://bfa.github.io/solitaire-js/fonts/suit-regular.eot?#iefix") format('embedded-opentype'), url("https://bfa.github.io/solitaire-js/fonts/suit-regular.woff") format('woff'), url("https://bfa.github.io/solitaire-js/fonts/suit-regular.ttf") format('truetype'), url("https://bfa.github.io/solitaire-js/fonts/suit-regular.svg#suit-regular") format('svg');
font-weight: normal;
font-style: normal;
}
html, body {
width: 100%;
height: 100%;
}
body {
position: relative;
color: #f3f5f7;
background: #0e8b44 url('https://bfa.github.io/solitaire-js/img/green_felt.jpg');
background-size: cover;
background-position: center;
}
.navbar {
background: rgba(8,8,8,.75);
}
.navbar a {
color: #fff;
}
.navbar li[data-active="false"] {
opacity: 0.5;
pointer-events: none;
}
.navbar li[data-active="true"] {
opacity: 1;
pointer-events: auto;
}
.template {
display: none;
}
#score {
position: relative;
background: rgba(0,0,0,.15);
margin-top: 1.5em;
margin-bottom: 0.5em;
padding: 1em;
line-height: 1;
}
#score > * {
display: inline-block;
min-width: 100px;
}
#score > *:last-child {
float: right;
text-align: right;
}
#score label {
margin-bottom: 0;
}
#score .timer button {
display: none;
position: absolute;
-webkit-appearance: none;
background: transparent;
outline: none;
border: 0;
padding: 0;
top: 0;
left: 0;
width: 3em;
height: 100%;
}
#score .timer button i {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
font-size: 0;
}
#score .timer button i::before {
position: absolute;
top: 50%;
left: 50%;
width: 0.75em;
height: 1em;
margin-left: -0.375em;
margin-top: -0.5em;
font-size: 1.5rem;
content: none;
}
#score .timer button i#play::before {
border-top: 8px solid transparent;
border-left: 12px solid #fff;
border-bottom: 8px solid transparent;
}
#score .timer button i#pause::before {
border-right: 4px solid #fff;
border-left: 4px solid #fff;
}
[data-gameplay="active"] #score .timer button,
[data-gameplay="paused"] #score .timer button {
display: inline-block;
}
[data-gameplay="active"] #score .timer button i#pause::before,
[data-gameplay="paused"] #score .timer button i#play::before {
content: '';
}
[data-gameplay="active"] #score,
[data-gameplay="paused"] #score {
padding-left: 3em;
}
#auto-win {
display: none;
-webkit-appearance: none;
outline: none;
border: 0;
position: absolute;
z-index: 1;
bottom: 0;
width: 100%;
background: rgba(0,0,0,.8);
padding: 1em;
line-height: 1;
}
#table {
opacity: 0;
width: 100%;
padding: 15px 0;
}
#table > div {
position: relative;
margin-bottom: 10px;
}
#table > div:last-child {
margin-bottom: 0;
}
#table > div::after {
content: '';
display: table-cell;
clear: both;
}
#table ul {
display: inline-block;
padding: 0;
}
#table > div > ul {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.pile {
display: block;
position: relative;
float: left;
width: 13%;
margin-right: 1.5%;
margin-bottom: 10px;
padding: 5px;
}
.pile:last-child {
margin-right: 0;
}
.pile::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 2px dotted rgba(0,0,0,.25);
border-radius: 5px;
}
.card {
display: block;
position: absolute;
top: 0;
left: 0;
background-color: #ddd;
background-image: url('https://bfa.github.io/solitaire-js/img/card_back_bg.png');
background-size: contain;
background-repeat: no-repeat;
width: 100%;
height: 100%;
font-family: 'Suit-Regular', sans-serif;
font-size: 0.6vw;
border-radius: 5px;
z-index: -1;
}
.card * {
pointer-events: none;
}
@media screen and (min-width: 768px) {
.card {
font-size: 0.325em;
}
.pile::after, .card {
border-radius: 10px;
}
}
@media screen and (min-width: 992px) {
.card {
font-size: 0.425em;
}
}
@media screen and (min-width: 1200px) {
.card {
font-size: 0.525em;
}
}
.card > div {
display: none;
}
.card.up {
background-image: url('https://bfa.github.io/solitaire-js/img/card_face_bg.png');
background-repeat: repeat;
color: #111;
}
.card.up > div {
display: block;
}
.card::before {
content: '';
display: block;
position: absolute;
width: 100%;
height: 100%;
border-radius: 5px;
}
.card[data-selected="true"]::before {
box-shadow: 0 0 6px 3px #FCDB1A;
}
@media screen and (min-width: 768px) {
.card::before {
border-radius: 10px;
}
}
.card .suit {
font-size: 5.8em;
font-weight: normal;
width: 0.6896551724137931em;
height: 0.786206896551724em;
line-height: 0.786206896551724em;
position: absolute;
text-align: center;
}
@media screen and (max-width: 767px) {
.card > div > .suit {
display: none;
}
}
.card .heart,
.card .diamond {
color: #cc0000;
}
.card .spade .suit::before { content: '♠' }
.card .heart .suit::before { content: '♥' }
.card .diamond .suit::before { content: '♦' }
.card .club .suit::before { content: '♣' }
.card .corner {
line-height: 1;
position: absolute;
text-align: center;
}
.card .corner span {
display: block;
font-size: 9em;
font-weight: bold;
width: 1em;
text-align: center;
}
@media screen and (min-width: 768px) {
.card .corner span {
font-size: 3em;
}
}
.card .corner .suit {
margin-top: 0;
margin-left: 0;
}
.card .corner.top {
left: 0.64em;
top: 0.96em;
}
.card .corner.bottom {
bottom: 0.96em;
right: 0.64em;
-ms-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
.card .ace span.suit.middle_center {
font-size: 10.24em;
left: 50%;
top: 50%;
margin-top: -0.5em;
margin-left: -0.35em;
}
.card .face::before {
display: none;
content: '';
position: absolute;
top: 15.25%;
left: 19%;
width: 62%;
height: 70.5%;
background-repeat: no-repeat;
background-size: contain;
}
@media screen and (min-width: 768px) {
.card .face::before {
display: block;
}
}
.card .spade.king .face::before {
background-image: url('https://bfa.github.io/solitaire-js/img/face-king-spade.png');
}
.card .spade.queen .face::before {
background-image: url('https://bfa.github.io/solitaire-js/img/face-queen-spade.png');
}
.card .spade.jack .face::before {
background-image: url('https://bfa.github.io/solitaire-js/img/face-jack-spade.png');
}
.card .heart.king .face::before {
background-image: url('https://bfa.github.io/solitaire-js/img/face-king-heart.png');
}
.card .heart.queen .face::before {
background-image: url('https://bfa.github.io/solitaire-js/img/face-queen-heart.png');
}
.card .heart.jack .face::before {
background-image: url('https://bfa.github.io/solitaire-js/img/face-jack-heart.png');
}
.card .diamond.king .face::before {
background-image: url('https://bfa.github.io/solitaire-js/img/face-king-diamond.png');
}
.card .diamond.queen .face::before {
background-image: url('https://bfa.github.io/solitaire-js/img/face-queen-diamond.png');
}
.card .diamond.jack .face::before {
background-image: url('https://bfa.github.io/solitaire-js/img/face-jack-diamond.png');
}
.card .club.king .face::before {
background-image: url('https://bfa.github.io/solitaire-js/img/face-king-club.png');
}
.card .club.queen .face::before {
background-image: url('https://bfa.github.io/solitaire-js/img/face-queen-club.png');
}
.card .club.jack .face::before {
background-image: url('https://bfa.github.io/solitaire-js/img/face-jack-club.png');
}
.card .suit.top_center {
left: 50%;
top: 0;
margin-left: -0.35em;
margin-top: 0.65em;
}
.card .suit.top_left {
left: 0;
top: 0;
margin-left: 0.65em;
margin-top: 0.65em;
}
.card .suit.top_right {
right: 0;
top: 0;
margin-right: 0.65em;
margin-top: 0.65em;
}
.card .suit.middle_center {
left: 50%;
top: 50%;
margin-left: -0.35em;
margin-top: -0.5em;
}
.card .suit.middle_top {
left: 50%;
top: 0;
margin-left: -0.35em;
margin-top: 1.25em;
}
.card .suit.middle_bottom {
bottom: 0;
left: 50%;
margin-bottom: 0.65em;
margin-left: -0.35em;
-ms-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
.card .suit.middle_left {
left: 0;
top: 50%;
margin-left: 0.65em;
margin-top: -0.5em;
}
.card .suit.middle_right {
right: 0;
top: 50%;
margin-right: 0.65em;
margin-top: -0.5em;
}
.card .suit.middle_top_center {
left: 50%;
top: 50%;
margin-left: -0.35em;
margin-top: -1.35em;
}
.card .suit.middle_top_left {
left: 0;
top: 50%;
margin-left: 0.65em;
margin-top: -1em;
}
.card .suit.middle_top_right {
right: 0;
top: 50%;
margin-right: 0.65em;
margin-top: -1em;
}
.card .suit.middle_bottom_left {
bottom: 50%;
left: 0;
margin-left: 0.65em;
margin-bottom: -1em;
-ms-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
.card .suit.middle_bottom_right {
bottom: 50%;
right: 0;
margin-bottom: -1em;
margin-right: 0.65em;
-ms-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
.card .suit.middle_bottom_center {
bottom: 50%;
left: 50%;
margin-bottom: -1.35em;
margin-left: -0.35em;
-ms-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
.card .suit.bottom_center {
bottom: 0;
left: 50%;
margin-bottom: 0.65em;
margin-left: -0.35em;
-ms-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
.card .suit.bottom_left {
bottom: 0;
left: 0;
margin-bottom: 0.65em;
margin-left: 0.65em;
-ms-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
.card .suit.bottom_right {
bottom: 0;
right: 0;
margin-bottom: 0.65em;
margin-right: 0.65em;
-ms-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-webkit-transform: rotate(180deg);
transform: rotate(180deg);
}
.card:nth-child(1),
.card:nth-child(2),
.card:nth-child(3),
.card:nth-child(4),
.card:nth-child(5) {
box-shadow: 0 0 5px rgba(0,0,0,.25), 0 2px 1px rgba(0,0,0,.5);
z-index: 1;
}
.card:nth-child(1) { top: 0; z-index: 5; }
.card:nth-child(2) { top: 2px; z-index: 4; }
.card:nth-child(3) { top: 4px; z-index: 3; }
.card:nth-child(4) { top: 6px; z-index: 2; }
.card:nth-child(5) { top: 8px; z-index: 1; }
/* stock */
.stock {
z-index: 1;
}
.stock .reload-icon {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
font-size: 3vw;
font-weight: bold;
line-height: 1;
opacity: 0.25;
z-index: 1;
}
@media screen and (max-width: 767px) {
.stock .reload-icon {
font-size: 5vw;
}
}
.stock .reload-icon span {
display: block;
position: absolute;
top: 50%;
left: 0;
width: 100%;
text-align: center;
padding: 0.25em;
margin-top: -0.75em;
pointer-events: none;
}
.stock .reload-icon span::before,
.stock .reload-icon span::after {
content: '';
display: inline-block;
border-style: solid;
}
.stock .reload-icon span::before {
width: 1.25em;
height: 1.25em;
border-color: transparent black black black;
border-radius: 50%;
border-width: .125em;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
.stock .reload-icon span::after {
position: absolute;
top: 0;
left: 50%;
width: 0;
height: 0;
border-color: transparent transparent transparent black;
border-width: .3125em 0 .3125em .5em;
}
/* waste */
.waste {
z-index: 1;
}
/* foundation */
.fnd .pile {
left: 43.5%;
}
.fnd .pile::before {
content '';
position: absolute;
top: 50%;
left: 0;
width: 100%;
color: #000;
font-family: 'Suit-Regular', sans-serif;
font-size: 10vw;
margin-top: -0.6em;
line-height: 1;
text-align: center;
opacity: 0.25;
}
@media screen and (min-width: 768px) {
.fnd .pile::before {
font-size: 6em;
}
}
@media screen and (min-width: 992px) {
.fnd .pile::before {
font-size: 7em;
}
}
@media screen and (min-width: 1200px) {
.fnd .pile::before {
font-size: 8em;
}
}
.fnd .pile.spades::before { content: '♠' }
.fnd .pile.hearts::before { content: '♥' }
.fnd .pile.diamonds::before { content: '♦' }
.fnd .pile.clubs::before { content: '♣' }
/* tableau */
.tab .card {
box-shadow: 0 0 5px rgba(0,0,0,.5);
z-index: 1;
margin-bottom: 10em;
}
/* face up */
.tab .card:nth-child(2) { top: 6em; left: 0; }
.tab .card:nth-child(3) { top: 12em; left: 0; }
.tab .card:nth-child(4) { top: 18em; left: 0; }
.tab .card:nth-child(5) { top: 24em; left: 0; }
.tab .card:nth-child(6) { top: 30em; left: 0; }
.tab .card:nth-child(7) { top: 36em; left: 0; }
.tab .card:nth-child(8) { top: 42em; left: 0; }
.tab .card:nth-child(9) { top: 48em; left: 0; }
.tab .card:nth-child(10) { top: 54em; left: 0; }
.tab .card:nth-child(11) { top: 60em; left: 0; }
.tab .card:nth-child(12) { top: 66em; left: 0; }
.tab .card:nth-child(13) { top: 72em; left: 0; }
.tab .card:nth-child(14) { top: 78em; left: 0; }
.tab .card:nth-child(15) { top: 84em; left: 0; }
.tab .card:nth-child(16) { top: 90em; left: 0; }
.tab .card:nth-child(17) { top: 96em; left: 0; }
.tab .card:nth-child(18) { top: 102em; left: 0; }
.tab .card:nth-child(19) { top: 108em; left: 0; }
.tab .card:nth-child(20) { top: 114em; left: 0; }
.tab .card:nth-child(21) { top: 120em; left: 0; }
/* face down */
.tab .pile[data-unplayed='1'] .card:nth-child(2),
.tab .pile[data-unplayed='2'] .card:nth-child(2),
.tab .pile[data-unplayed='3'] .card:nth-child(2),
.tab .pile[data-unplayed='4'] .card:nth-child(2),
.tab .pile[data-unplayed='5'] .card:nth-child(2),
.tab .pile[data-unplayed='6'] .card:nth-child(2) { top: 3em; }
.tab .pile[data-unplayed='2'] .card:nth-child(3),
.tab .pile[data-unplayed='3'] .card:nth-child(3),
.tab .pile[data-unplayed='4'] .card:nth-child(3),
.tab .pile[data-unplayed='5'] .card:nth-child(3),
.tab .pile[data-unplayed='6'] .card:nth-child(3) { top: 6em; }
.tab .pile[data-unplayed='3'] .card:nth-child(4),
.tab .pile[data-unplayed='4'] .card:nth-child(4),
.tab .pile[data-unplayed='5'] .card:nth-child(4),
.tab .pile[data-unplayed='6'] .card:nth-child(4) { top: 9em; }
.tab .pile[data-unplayed='4'] .card:nth-child(5),
.tab .pile[data-unplayed='5'] .card:nth-child(5),
.tab .pile[data-unplayed='6'] .card:nth-child(5) { top: 12em; }
.tab .pile[data-unplayed='5'] .card:nth-child(6),
.tab .pile[data-unplayed='6'] .card:nth-child(6) { top: 15em; }
.tab .pile[data-unplayed='6'] .card:nth-child(7) { top: 18em; }
/* piles with odd # of face down cards */
.tab .pile[data-unplayed='1'] .card:nth-child(3) { top: 9em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(4),
.tab .pile[data-unplayed='3'] .card:nth-child(5) { top: 15em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(5),
.tab .pile[data-unplayed='3'] .card:nth-child(6),
.tab .pile[data-unplayed='5'] .card:nth-child(7) { top: 21em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(6),
.tab .pile[data-unplayed='3'] .card:nth-child(7),
.tab .pile[data-unplayed='5'] .card:nth-child(8) { top: 27em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(7),
.tab .pile[data-unplayed='3'] .card:nth-child(8),
.tab .pile[data-unplayed='5'] .card:nth-child(9) { top: 33em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(8),
.tab .pile[data-unplayed='3'] .card:nth-child(9),
.tab .pile[data-unplayed='5'] .card:nth-child(10) { top: 39em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(9),
.tab .pile[data-unplayed='3'] .card:nth-child(10),
.tab .pile[data-unplayed='5'] .card:nth-child(11) { top: 45em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(10),
.tab .pile[data-unplayed='3'] .card:nth-child(11),
.tab .pile[data-unplayed='5'] .card:nth-child(12) { top: 51em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(11),
.tab .pile[data-unplayed='3'] .card:nth-child(12),
.tab .pile[data-unplayed='5'] .card:nth-child(13) { top: 57em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(12),
.tab .pile[data-unplayed='3'] .card:nth-child(13),
.tab .pile[data-unplayed='5'] .card:nth-child(14) { top: 63em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(13),
.tab .pile[data-unplayed='3'] .card:nth-child(14),
.tab .pile[data-unplayed='5'] .card:nth-child(15) { top: 69em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(14),
.tab .pile[data-unplayed='3'] .card:nth-child(15),
.tab .pile[data-unplayed='5'] .card:nth-child(16) { top: 75em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(15),
.tab .pile[data-unplayed='3'] .card:nth-child(16),
.tab .pile[data-unplayed='5'] .card:nth-child(17) { top: 81em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(16),
.tab .pile[data-unplayed='3'] .card:nth-child(17),
.tab .pile[data-unplayed='5'] .card:nth-child(18) { top: 87em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(17),
.tab .pile[data-unplayed='3'] .card:nth-child(18),
.tab .pile[data-unplayed='5'] .card:nth-child(19) { top: 93em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(18),
.tab .pile[data-unplayed='3'] .card:nth-child(19),
.tab .pile[data-unplayed='5'] .card:nth-child(20) { top: 99em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(19),
.tab .pile[data-unplayed='3'] .card:nth-child(20),
.tab .pile[data-unplayed='5'] .card:nth-child(21) { top: 105em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(20),
.tab .pile[data-unplayed='3'] .card:nth-child(21) { top: 111em; left: 0; }
.tab .pile[data-unplayed='1'] .card:nth-child(21) { top: 117em; left: 0; }
/* piles with even # of face down cards */
.tab .pile[data-unplayed='2'] .card:nth-child(4) { top: 12em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(5),
.tab .pile[data-unplayed='4'] .card:nth-child(6) { top: 18em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(6),
.tab .pile[data-unplayed='4'] .card:nth-child(7),
.tab .pile[data-unplayed='6'] .card:nth-child(8) { top: 24em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(7),
.tab .pile[data-unplayed='4'] .card:nth-child(8),
.tab .pile[data-unplayed='6'] .card:nth-child(9) { top: 30em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(8),
.tab .pile[data-unplayed='4'] .card:nth-child(9),
.tab .pile[data-unplayed='6'] .card:nth-child(10) { top: 36em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(9),
.tab .pile[data-unplayed='4'] .card:nth-child(10),
.tab .pile[data-unplayed='6'] .card:nth-child(11) { top: 42em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(10),
.tab .pile[data-unplayed='4'] .card:nth-child(11),
.tab .pile[data-unplayed='6'] .card:nth-child(12) { top: 48em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(11),
.tab .pile[data-unplayed='4'] .card:nth-child(12),
.tab .pile[data-unplayed='6'] .card:nth-child(13) { top: 54em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(12),
.tab .pile[data-unplayed='4'] .card:nth-child(13),
.tab .pile[data-unplayed='6'] .card:nth-child(14) { top: 60em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(13),
.tab .pile[data-unplayed='4'] .card:nth-child(14),
.tab .pile[data-unplayed='6'] .card:nth-child(15) { top: 66em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(14),
.tab .pile[data-unplayed='4'] .card:nth-child(15),
.tab .pile[data-unplayed='6'] .card:nth-child(16) { top: 72em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(15),
.tab .pile[data-unplayed='4'] .card:nth-child(16),
.tab .pile[data-unplayed='6'] .card:nth-child(17) { top: 78em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(16),
.tab .pile[data-unplayed='4'] .card:nth-child(17),
.tab .pile[data-unplayed='6'] .card:nth-child(18) { top: 84em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(17),
.tab .pile[data-unplayed='4'] .card:nth-child(18),
.tab .pile[data-unplayed='6'] .card:nth-child(19) { top: 90em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(18),
.tab .pile[data-unplayed='4'] .card:nth-child(19),
.tab .pile[data-unplayed='6'] .card:nth-child(20) { top: 96em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(19),
.tab .pile[data-unplayed='4'] .card:nth-child(20),
.tab .pile[data-unplayed='6'] .card:nth-child(21) { top: 102em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(20),
.tab .pile[data-unplayed='4'] .card:nth-child(21) { top: 108em; left: 0; }
.tab .pile[data-unplayed='2'] .card:nth-child(21) { top: 114em; left: 0; }
/* Confetti */
#confetti {
position: absolute;
top: 0;
left: 0;
z-index: 10000;
pointer-events: none;
opacity: 0;
}
/* Disable Grammarly */
grammarly-card {
display: none;
}
/* ### TODO ###
- Refactor code :) Always
Optional Features:
- HTML Drag & Drop API
- Limit How Many Times Stock Can Be Reloaded (3x)
- 3 Card Draw
- High score
- Options panel for user
- Sound Fx
*/
// 0. DECLARE VARS
// document
var d = document;
// build deck
var deck = [];
// build suits
var suits = [];
suits['spades'] = [
// spades
['A','spade'],
['2','spade'],
['3','spade'],
['4','spade'],
['5','spade'],
['6','spade'],
['7','spade'],
['8','spade'],
['9','spade'],
['10','spade'],
['J','spade'],
['Q','spade'],
['K','spade']
];
suits['hearts'] = [
// hearts
['A','heart'],
['2','heart'],
['3','heart'],
['4','heart'],
['5','heart'],
['6','heart'],
['7','heart'],
['8','heart'],
['9','heart'],
['10','heart'],
['J','heart'],
['Q','heart'],
['K','heart']
];
suits['diamonds'] = [
// diamonds
['A','diamond'],
['2','diamond'],
['3','diamond'],
['4','diamond'],
['5','diamond'],
['6','diamond'],
['7','diamond'],
['8','diamond'],
['9','diamond'],
['10','diamond'],
['J','diamond'],
['Q','diamond'],
['K','diamond']
];
suits['clubs'] = [
// clubs
['A','club'],
['2','club'],
['3','club'],
['4','club'],
['5','club'],
['6','club'],
['7','club'],
['8','club'],
['9','club'],
['10','club'],
['J','club'],
['Q','club'],
['K','club']
];
// build stock pile
var s = [];
// build waste pile
var w = [];
// build foundations
var spades = [];
var hearts = [];
var diamonds = [];
var clubs = [];
// build tableau
var t = [];
t[1] = t[2] = t[3] = t[4] = t[5] = t[6] = t[7] = [];
// build table
var table = [];
table['stock'] = s;
table['waste'] = w;
table['spades'] = spades;
table['hearts'] = hearts;
table['diamonds'] = diamonds;
table['clubs'] = clubs;
table['tab'] = t;
// initial face up cards
var playedCards =
'#waste .card,' +
'#fnd .card,' +
'#tab .card:last-child';
// cache selectors
var $timer = d.querySelector('#score .timer');
var $timerSpan = d.querySelector('#score .timer span');
var $moveCount = d.querySelector('#score .move-count');
var $moveCountSpan = d.querySelector('#score .move-count span');
var $score = d.querySelector('#score .score');
var $scoreSpan = d.querySelector('#score .score span');
var $playPause = d.querySelector('#play-pause');
var $table = d.querySelector('#table');
var $upper = d.querySelector('#table .upper-row');
var $lower = d.querySelector('#table .lower-row');
var $stock = d.querySelector('#stock');
var $waste = d.querySelector('#waste');
var $fnd = d.querySelector('#fnd');
var $tab = d.querySelector('#tab');
var $autoWin = d.querySelector('#auto-win');
// other global vars
var clock = 0;
var time = 0;
var moves = 0;
var score = 0;
var bonus = 0;
var lastEventTime = 0;
var unplayedTabCards = [];
// 1. CREATE DECK
deck = create(deck, suits);
// 2. SHUFFLE DECK
deck = shuffle(deck);
// 3. DEAL DECK
table = deal(deck, table);
// 4. RENDER TABLE
render(table, playedCards);
// 5. START GAMEPLAY
play(table);
// ### EVENT HANDLERS ###
window.onresize = function(event) {
sizeCards();
};
// ### FUNCTIONS ###
// create deck
function create(deck, suits) {
console.log('Creating Deck...');
// loop through each suit
for (var suit in suits) {
suit = suits[suit];
// loop through each card in suit
for (var card in suit) {
card = suit[card];
deck.push(card); // push card to deck
}
}
return deck;
}
// shuffle deck
function shuffle(deck) {
console.log('Shuffling Deck...');
// declare vars
var i = deck.length, temp, rand;
// while there remain elements to shuffle
while (0 !== i) {
// pick a remaining element
rand = Math.floor(Math.random() * i);
i--;
// and swap it with the current element
temp = deck[i];
deck[i] = deck[rand];
deck[rand] = temp;
}
return deck;
}
// deal deck
function deal(deck, table) {
console.log('Dealing Deck...');
// move all cards to stock
table['stock'] = deck;
// build tableau
var tabs = table['tab'];
// loop through 7 tableau rows
for (var row = 1; row <= 7; row++) {
// loop through 7 piles in row
for (var pile = row; pile <= 7; pile++) {
// build blank pile on first row
if (row === 1) tabs[pile] = [];
// deal card to pile
move(table['stock'], tabs[pile], false);
}
}
return table;
}
// move card
function move(source, dest, pop, selectedCards = 1) {
if (pop !== true) {
var card = source.shift(); // take card from bottom
dest.push(card); // push card to destination pile
} else {
while (selectedCards) {
// take card from the top of selection
var card = source[source.length - selectedCards];
// remove it from the selected pile
source.splice(source.length - selectedCards, 1);
// put it in the destination pile
dest.push(card);
// decrement
selectedCards--;
}
}
return;
}
// render table
function render(table, playedCards) {
console.log('Rendering Table...');
// check for played cards
playedCards = checkForPlayedCards(playedCards);
// check for empty piles
emptyPiles = checkForEmptyPiles(table);
// update stock pile
update(table['stock'], '#stock ul', playedCards, true);
// update waste pile
update(table['waste'], '#waste ul', playedCards);
// update spades pile
update(table['spades'], '#spades ul', playedCards);
// update hearts pile
update(table['hearts'], '#hearts ul', playedCards);
// update diamonds pile
update(table['diamonds'], '#diamonds ul', playedCards);
// update clubs pile
update(table['clubs'], '#clubs ul', playedCards);
// update tableau
var tabs = table['tab'];
// loop through tableau piles
for (var i = 1; i <= 7; i++) {
// update tableau pile
update(tabs[i], '#tab li:nth-child('+i+') ul', playedCards, true);
}
// get unplayed tab cards
unplayedTabCards = getUnplayedTabCards();
// size cards
sizeCards();
// show table
$table.style.opacity = '100';
console.log('Table Rendered:', table);
return;
}
// update piles
function update(pile, selector, playedCards, append) {
var e = d.querySelector(selector);
var children = e.children; // get children
var grandParent = e.parentElement.parentElement; // get grand parent
// reset pile
e.innerHTML = '';
// loop through cards in pile
for (var card in pile) {
card = pile[card];
// get html template for card
var html = getTemplate(card);
// create card in pile
createCard(card, selector, html, append);
}
// turn cards face up
flipCards(playedCards, 'up');
// count played cards
var played = countPlayedCards(children);
e.parentElement.dataset.played = played;
// count all played cards for #tab and #fnd piles
if ( grandParent.id === 'tab' || grandParent.id === 'fnd' ) {
var playedAll = parseInt(grandParent.dataset.played);
if ( isNaN(playedAll) ) playedAll = 0;
grandParent.dataset.played = playedAll + played;
}
// count unplayed cards
var unplayed = countUnplayedCards(children);
e.parentElement.dataset.unplayed = unplayed;
// count all unplayed cards for #tab and #fnd piles
if ( grandParent.id === 'tab' || grandParent.id === 'fnd' ) {
var unplayedAll = parseInt(grandParent.dataset.unplayed);
if ( isNaN(unplayedAll) ) unplayedAll = 0;
grandParent.dataset.unplayed = unplayedAll + unplayed;
}
return pile;
}
// get html template for card
function getTemplate(card) {
var r = card[0]; // get rank
var s = card[1]; // get suit
// get html template
var html = d.querySelector('.template li[data-rank="'+r+'"]').innerHTML;
// search and replace suit variable
html = html.replace('{{suit}}', s);
return html;
}
// create card in pile
function createCard(card, selector, html, append) {
var r = card[0]; // get rank
var s = card[1]; // get suit
// get pile based on selector
if ( selector.includes('#stock') ) var p = 'stock';
if ( selector.includes('#waste') ) var p = 'waste';
if ( selector.includes('#spades') ) var p = 'spades';
if ( selector.includes('#hearts') ) var p = 'hearts';
if ( selector.includes('#diamonds') ) var p = 'diamonds';
if ( selector.includes('#clubs') ) var p = 'clubs';
if ( selector.includes('#tab') ) var p = 'tab';
var e = d.createElement('li'); // create li element
e.className = 'card'; // add .card class to element
e.dataset.rank = r; // set rank atribute
e.dataset.suit = s; // set suit attribute
e.dataset.pile = p; // set pile attribute;
e.dataset.selected = 'false'; // set selected attribute
e.innerHTML = html; // insert html to element
// query for pile
var pile = d.querySelector(selector);
// append to pile
if (append) pile.appendChild(e);
// or prepend to pile
else pile.insertBefore(e, pile.firstChild);
return;
}
// check for played cards
function checkForPlayedCards(playedCards) {
// query
var els = d.querySelectorAll('.card[data-played="true"]');
for (var e in els) { // loop through elements
e = els[e];
if (e.nodeType) {
var r = e.dataset.rank;
var s = e.dataset.suit;
playedCards += ', .card[data-rank="'+r+'"][data-suit="'+s+'"]' ;
}
}
return playedCards;
}
// check for empty piles
function checkForEmptyPiles(table) {
// reset empty data on all piles
var els = d.querySelectorAll('.pile'); // query elements
for (var e in els) { // loop through elements
e = els[e];
if (e.nodeType) {
delete e.dataset.empty;
}
}
// declare var with fake pile so we always have one
var emptyPiles = '#fake.pile';
// check spades pile
if ( table['spades'].length === 0 ) {
emptyPiles += ', #fnd #spades.pile';
}
// check hearts pile
if ( table['hearts'].length === 0 ) {
emptyPiles += ', #fnd #hearts.pile';
}
// check diamonds pile
if ( table['diamonds'].length === 0 ) {
emptyPiles += ', #fnd #diamonds.pile';
}
// check clubs pile
if ( table['clubs'].length === 0 ) {
emptyPiles += ', #fnd #clubs.pile';
}
// check tableau piles
var tabs = table['tab'];
// loop through tableau piles
for (var i = 1; i <= 7; i++) {
// check tabeau pile
if ( tabs[i].length === 0 ) {
emptyPiles += ', #tab li:nth-child('+i+').pile';
}
}
// mark piles as empty
els = d.querySelectorAll(emptyPiles); // query elements
for (var e in els) { // loop through elements
e = els[e];
if (e.nodeType) {
e.dataset.empty = 'true'; // mark as empty
}
}
return emptyPiles;
}
// count played cards
function countPlayedCards(cards) {
var played = 0;
// loop through cards
for (var card in cards) {
card = cards[card];
if (card.nodeType) {
// check if card has been played
if (card.dataset.played === 'true') played++;
}
}
return played;
}
// count unplayed cards
function countUnplayedCards(cards) {
var unplayed = 0;
// loop through cards
for (var card in cards) {
card = cards[card];
if (card.nodeType) {
// check if card has been played
if (card.dataset.played !== 'true') unplayed++;
}
}
return unplayed;
}
// flip cards
function flipCards(selectors, direction) {
var els = d.querySelectorAll(selectors); // query all elements
for (var e in els) { // loop through elements
e = els[e];
if (e.nodeType) {
switch(direction) {
case 'up' :
if (e.dataset.played !== 'true') {
// if flipping over tableau card
if (e.dataset.pile === 'tab') {
// loop through unplayed cards
for (var card in unplayedTabCards) {
card = unplayedTabCards[card];
// if rank and suit matches
if ( e.dataset.rank === card[0] &&
e.dataset.suit === card[1] )
// score 5 points
updateScore(5);
}
}
e.className += ' up'; // add class
e.dataset.played = 'true'; // mark as played
}
break;
case 'down' :
e.className = 'card'; // reset class
delete e.dataset.played; // reset played data attribute
default : break;
}
}
}
return;
}
// get face down cards in tableau pile
function getUnplayedTabCards() {
// reset array
unplayedTabCards = [];
// get all face down card elements
var els = d.querySelectorAll('#tab .card:not([data-played="true"])');
for (var e in els) { // loop through elements
e = els[e];
if (e.nodeType) {
unplayedTabCards.push( [ e.dataset.rank, e.dataset.suit ] );
}
}
return unplayedTabCards;
}
// size cards
function sizeCards(selector = '.pile', ratio = 1.4) {
var s = selector;
var r = ratio;
var e = d.querySelector(s); // query element
var h = e.offsetWidth * r; // get height of element
// set row heights
$upper.style.height = h + 10 + 'px';
$lower.style.height = h + 120 + 'px';
// set height of elements
var els = d.querySelectorAll(s); // query all elements
for (var e in els) { // loop through elements
e = els[e];
if (e.nodeType) e.style.height = h + 'px'; // set height in css
}
}
// gameplay
function play(table) {
// check for winning table
if ( checkForWin(table) ) return;
// check for autowin
checkForAutoWin(table);
// bind click events
bindClick(
'#stock .card:first-child,' +
'#waste .card:first-child,' +
'#fnd .card:first-child,' +
'#tab .card[data-played="true"]'
);
// bind dbl click events
bindClick(
'#waste .card:first-child,' +
'#tab .card:last-child',
'double'
);
console.log('Make Your Move...');
console.log('......');
}
// bind click events
function bindClick(selectors, double) {
var elements = d.querySelectorAll(selectors); // query all elements
// loop through elements
for (var e in elements) {
e = elements[e];
// add event listener
if (e.nodeType) {
if (!double) e.addEventListener('click', select);
else e.addEventListener('dblclick', select);
}
}
return;
}
// unbind click events
function unbindClick(selectors, double) {
var elements = d.querySelectorAll(selectors); // query all elements
// loop through elements
for (var e in elements) {
e = elements[e];
// remove event listener
if (e.nodeType) {
if (!double) e.removeEventListener('click', select);
else e.removeEventListener('dblclick', select);
}
}
return;
}
// on click handler: select
var clicks = 0; // set counter for counting clicks
var clickDelay = 200; // set delay for double click
var clickTimer = null; // set timer for timeout function
function select(event) {
// prevent default
event.preventDefault();
// start timer
if ( $timer.dataset.action !== 'start' ) {
timer('start');
}
// if timestamp matches then return false
var time = event.timeStamp; // get timestamp
if ( time === lastEventTime ) {
console.log('Status: Timestamp Matches, False Click');
return false;
}
else {
lastEventTime = time; // cache timestamp
}
// get variables
var e = event.target; // get element
var isSelected = e.dataset.selected; // get selected attribute
var rank = e.dataset.rank; // get rank attribute
var suit = e.dataset.suit; // get suit attribute
var pile = e.dataset.pile; // get pile attribute
var action = e.dataset.action; // get action attribute
// create card array
if (rank && suit) var card = [rank,suit];
// count clicks
clicks++;
// single click
if (clicks === 1 && event.type === 'click') {
clickTimer = setTimeout(function() {
console.log('Single Click Detected', event);
// reset click counter
clicks = 0;
// if same card is clicked
if (e.dataset.selected === 'true') {
console.log('Status: Same Card Clicked');
// deselect card
delete e.dataset.selected;
delete $table.dataset.move;
delete $table.dataset.selected;
delete $table.dataset.source;
console.log('Card Deselected', card, e);
}
// if move is in progress
else if ($table.dataset.move) {
console.log('Status: A Move Is In Progess');
// get selected
var selected = $table.dataset.selected.split(',');
// update table dataset with destination pile
$table.dataset.dest = e.closest('.pile').dataset.pile;
// get destination card or pile
if ( card ) var dest = card;
else var dest = $table.dataset.dest;
// validate move
if ( validateMove(selected, dest) ) {
// make move
makeMove();
reset(table);
render(table, playedCards);
play(table);
} else {
console.log('Move is Invalid. Try again...');
reset(table);
render(table, playedCards);
play(table);
console.log('Card Deselected', card, e);
}
}
// if stock is clicked
else if (pile === 'stock') {
console.log('Status: Stock Pile Clicked');
// if stock isn't empty
if (table['stock'].length) {
// move card from stock to waste
move(table['stock'], table['waste']);
reset(table);
render(table, playedCards);
// if empty, then bind click to stock pile element
if (table['stock'].length === 0) bindClick('#stock .reload-icon');
// count move
countMove(moves++);
// return to play
play(table);
}
}
// if stock reload icon is clicked
else if (action === 'reload') {
console.log('Reloading Stock Pile');
// remove event listener
unbindClick('#stock .reload-icon');
// reload stock pile
if (table['waste'].length) {
table['stock'] = table['waste']; // move waste to stock
table['waste'] = [] // empty waste
}
// render table
render(table, playedCards);
// turn all stock cards face down
flipCards('#stock .card', 'down');
// update score by -100 pts
updateScore(-100);
// return to play
play(table);
}
// if no move is in progress
else {
// select card
e.dataset.selected = 'true';
$table.dataset.move = 'true';
$table.dataset.selected = card;
$table.dataset.source = e.closest('.pile').dataset.pile;
// if ace is selected
if (rank === 'A') {
console.log('Ace Is Selected');
bindClick('#fnd #'+suit+'s.pile[data-empty="true"]');
}
if (rank === 'K') {
console.log('King Is Selected');
bindClick('#tab .pile[data-empty="true"]');
}
}
}, clickDelay);
}
// double click
else if (event.type === 'dblclick') {
console.log('Double Click Detected', event);
clearTimeout(clickTimer); // prevent single click
clicks = 0; // reset click counter
// select card
e.dataset.selected = 'true';
$table.dataset.move = 'true';
$table.dataset.selected = card;
$table.dataset.source = e.closest('.pile').dataset.pile;
// get destination pile
if ( card) var dest = card[1]+'s';
// update table dataset with destination
$table.dataset.dest = dest;
// validate move
if ( validateMove(card, dest) ) {
// make move
makeMove();
reset(table);
render(table, playedCards);
play(table);
} else {
console.log('Move is Invalid. Try again...');
reset(table);
render(table, playedCards);
play(table);
console.log('Card Deselected', card, e);
}
}
}
// validate move
function validateMove(selected, dest) {
console.log ('Validating Move...', selected, dest);
// if selected card exists
if (selected) {
var sRank = parseRankAsInt(selected[0]);
var sSuit = selected[1];
}
// if destination is another card
if (dest.constructor === Array) {
console.log('Desitination appears to be a card');
var dRank = parseRankAsInt(dest[0]);
var dSuit = dest[1];
var dPile = $table.dataset.dest;
// if destination pile is foundation
if (['spades','hearts','diamonds','clubs'].indexOf(dPile) >= 0) {
// if rank isn't in sequence then return false
if (dRank - sRank !== -1) {
console.log('Rank sequence invalid');
console.log(dRank - sRank)
return false;
}
// if suit isn't in sequence then return false
if ( sSuit !== dSuit ) {
console.log('Suit sequence invalid');
return false;
}
}
// if destination pile is tableau
else {
// if rank isn't in sequence then return false
if (dRank - sRank !== 1) {
console.log('Rank sequence invalid');
return false;
}
// if suit isn't in sequence then return false
if ( ( (sSuit === 'spade' || sSuit === 'club') &&
(dSuit === 'spade' || dSuit === 'club') ) ||
( (sSuit === 'heart' || sSuit === 'diamond') &&
(dSuit === 'heart' || dSuit === 'diamond') ) ) {
console.log('Suit sequence invalid');
return false;
}
}
// else return true
console.log('Valid move');
return true;
}
// if destination is foundation pile
if (['spades','hearts','diamonds','clubs'].indexOf(dest) >= 0) {
console.log('Destination appears to be empty foundation');
// get last card in destination pile
var lastCard = d.querySelector('#'+dest+' .card:first-child');
if (lastCard) {
var dRank = parseRankAsInt(lastCard.dataset.rank);
var dSuit = lastCard.dataset.suit;
}
// if suit doesn't match pile then return false
if ( sSuit + 's' !== dest ) {
console.log('Suit sequence invalid');
return false;
}
// if rank is ace then return true
else if ( sRank === 1 ) {
console.log('Valid Move');
return true;
}
// if rank isn't in sequence then return false
else if ( sRank - dRank !== 1 ) {
console.log('Rank sequence invalid');
return false;
}
// else return true
else {
console.log('Valid move');
return true;
}
}
// if destination is empty tableau pile
if ( dest >= 1 && dest <= 7 ) {
console.log('Destination appears tp be empty tableau');
return true;
}
}
// make move
function makeMove() {
console.log('Making Move...');
// get source and dest
var source = $table.dataset.source;
var dest = $table.dataset.dest;
console.log('From '+source+' pile to '+dest+' pile');
// if pulling card from waste pile
if ( source === 'waste') {
// if moving card to foundation pile
if ( isNaN(dest) ) {
console.log('Moving To Foundation Pile');
move(table[source], table[dest], true);
updateScore(10); // score 10 pts
}
// if moving card to tableau pile
else {
console.log('Moving To Tableau Pile');
move(table[source], table['tab'][dest], true);
updateScore(5); // score 5 pts
}
}
// if pulling card from foundation pile
else if (['spades','hearts','diamonds','clubs'].indexOf(source) >= 0) {
// only allow moves to tableau piles
if ( isNaN(dest) ) {
console.log('That move is not allowed');
return false;
}
// if moving card to tableau pile
else {
console.log('Moving To Tableau Pile');
move(table[source], table['tab'][dest], true);
updateScore(-15); // score -15 pts
}
}
// if pulling card from tableau pile
else {
// if moving card to foundation pile
if ( isNaN(dest) ) {
console.log('Moving To Foundation Pile');
move(table['tab'][source], table[dest], true);
updateScore(10); // score 10 pts
}
// if moving card to tableau pile
else {
console.log('Moving To Tableau Pile');
// get selected card
var selected = d.querySelector('.card[data-selected="true"');
// get cards under selected card
var selectedCards = [selected];
while ( selected = selected['nextSibling'] ) {
if (selected.nodeType) selectedCards.push(selected);
}
// move card(s)
move(
table['tab'][source],
table['tab'][dest],
true,
selectedCards.length
);
}
}
// unbind click events
unbindClick(
'#stock .card:first-child,' +
'#waste .card:first-child,' +
'#fnd .card:first-child,' +
'#fnd #spades.pile[data-empty="true"],' +
'#fnd #hearts.pile[data-empty="true"],' +
'#fnd #diamonds.pile[data-empty="true"],' +
'#fnd #clubs.pile[data-empty="true"],' +
'#tab .card[data-played="true"],' +
'#tab .pile[data-empty="true"]'
);
// unbind double click events
unbindClick(
'#waste .card:first-child' +
'#tab .card:last-child',
'double'
)
// count move
countMove(moves++);
// reset table
console.log('Ending Move...');
return;
}
// parse rank as integer
function parseRankAsInt(rank) {
// assign numerical ranks to letter cards
switch (rank) {
case 'A' : rank = '1'; break;
case 'J' : rank = '11'; break;
case 'Q' : rank = '12'; break;
case 'K' : rank = '13'; break;
default : break;
}
// return integer value for rank
return parseInt(rank);
}
// parse integer as rank
function parseIntAsRank(int) {
// parse as integer
rank = parseInt(int);
// assign letter ranks to letter cards
switch(rank) {
case 1 : rank = 'A'; break;
case 11 : rank = 'J'; break;
case 12 : rank = 'Q'; break;
case 13 : rank = 'K'; break;
default : break;
}
return rank;
}
// reset table
function reset(table) {
delete $table.dataset.move;
delete $table.dataset.selected;
delete $table.dataset.source;
delete $table.dataset.dest;
delete $fnd.dataset.played;
delete $fnd.dataset.unplayed;
delete $tab.dataset.played;
delete $tab.dataset.unplayed;
console.log('Table reset');
}
// timer funcion
function timer(action) {
// declare timer vars
var minutes = 0;
var seconds = 0;
var gameplay = d.body.dataset.gameplay;
// set timer attribute
$timer.dataset.action = action;
// switch case
switch (action) {
// start timer
case 'start' :
console.log('Starting Timer...');
// looping function
clock = setInterval(function() {
// increment
time++;
// parse minutes and seconds
minutes = parseInt(time / 60, 10);
seconds = parseInt(time % 60, 10);
minutes = minutes < 10 ? "0" + minutes : minutes;
seconds = seconds < 10 ? "0" + seconds : seconds;
// output to display
$timerSpan.textContent = minutes + ':' + seconds;
// if 10 seconds has passed decrement score by 2 pts
if ( time % 10 === 0 ) updateScore(-2);
}, 1000);
// add dataset to body
d.body.dataset.gameplay = 'active';
// unbind click to play button
if ( gameplay === 'paused')
$playPause.removeEventListener('click', playTimer);
// bind click to pause button
$playPause.addEventListener('click', pauseTimer = function(){
timer('pause');
});
break;
// pause timer
case 'pause' :
console.log('Pausing Timer...');
clearInterval(clock);
d.body.dataset.gameplay = 'paused';
// unbind click to pause button
if ( gameplay === 'active')
$playPause.removeEventListener('click', pauseTimer);
// bind click tp play button
$playPause.addEventListener('click', playTimer = function(){
timer('start');
});
break;
// stop timer
case 'stop' :
console.log('Stoping Timer...');
clearInterval(clock);
d.body.dataset.gameplay = 'over';
break;
// default
default : break;
}
console.log(time);
return;
}
// move counter
function countMove(moves) {
console.log('Move Counter', moves);
// set move attribute
$moveCount.dataset.moves = moves + 1;
// output to display
$moveCountSpan.textContent = moves + 1;
return;
}
// scoring function
/*
Standard scoring is determined as follows:
- Waste to Tableau 5
- Waste to Foundation 10
- Tableau to Foundation 10
- Turn over Tableau card 5
- Foundation to Tableau −15
- Recycle waste when playing by ones −100
(minimum score is 0)
Moving cards directly from the Waste stack to a Foundation awards 10 points. However, if the card is first moved to a Tableau, and then to a Foundation, then an extra 5 points are received for a total of 15. Thus in order to receive a maximum score, no cards should be moved directly from the Waste to Foundation.
For every 10 seconds of play, 2 points are taken away. Bonus points are calculated with the formula of 700,000 / (seconds to finish) if the game takes more than 30 seconds. If the game takes less than 30 seconds, no bonus points are awarded.
*/
function updateScore(points) {
console.log('Updating Score', points);
// get score
score = parseInt($score.dataset.score) + points;
// set minimum score to 0
score = score < 0 ? 0 : score;
// parse as integer
score = parseInt(score);
// set score attribute
$score.dataset.score = score;
// output to display
$score.children[1].textContent = score;
return score;
}
// calculate bonus points
function getBonus() {
if (time >= 30) bonus = parseInt(700000 / time);
console.log(bonus);
return bonus;
}
// check for win
function checkForWin(table) {
// if all foundation piles are full
if ( table['spades'].length +
table['hearts'].length +
table['diamonds'].length +
table['clubs'].length
=== 52 ) {
console.log('Game Has Been Won');
// stop timer
timer('stop');
// bonus points for time
updateScore(getBonus());
// throw confetti
throwConfetti();
// return true
return true;
}
else return false;
}
// check for auto win
function checkForAutoWin(table) {
// if all tableau cards are played and stock is empty
if ( parseInt($tab.dataset.unplayed) +
table['stock'].length +
table['waste'].length === 0) {
// show auto win button
$autoWin.style.display = 'block';
// bind click to auto win button
$autoWin.addEventListener('click', autoWin);
}
return;
}
// auto win
function autoWin() {
console.log('Huzzah!');
// hide auto win button
$autoWin.style.display = 'none';
// unbind click to auto win button
$autoWin.removeEventListener('click', autoWin);
// unbind click events
unbindClick(
'#stock .card:first-child,' +
'#waste .card:first-child,' +
'#fnd .card:first-child,' +
'#fnd #spades.pile[data-empty="true"],' +
'#fnd #hearts.pile[data-empty="true"],' +
'#fnd #diamonds.pile[data-empty="true"],' +
'#fnd #clubs.pile[data-empty="true"],' +
'#tab .card[data-played="true"],' +
'#tab .pile[data-empty="true"]'
);
// unbind double click events
unbindClick(
'#waste .card:first-child' +
'#tab .card:last-child',
'double'
);
// reset table
reset(table);
render(table);
// animate cards to foundation piles
autoWinAnimation(table);
// stop timer
timer('stop');
// bonus points for time
updateScore(getBonus());
}
// auto win animation
function autoWinAnimation(table) {
// set number of iterations
var i = parseInt($tab.dataset.played);
// create animation loop
function animation_loop() {
// get lowest ranking card
var bottomCards = []; // create array for the bottom cards
var els = d.querySelectorAll('#tab .card:last-child');
for (var e in els) { // loop through elements
e = els[e];
if (e.nodeType)
bottomCards.push( parseRankAsInt(e.dataset.rank) );
}
// get the lowest rank from array of bottom cards
var lowestRank = Math.min.apply(Math, bottomCards);
// parse integer as rank
var rank = parseIntAsRank(lowestRank);
// get element with rank
var e = d.querySelector('#tab .card[data-rank="'+rank+'"]');
// setup move
// get suit of card
var suit = e.dataset.suit;
// create card array with rank and suit
var card = [rank, suit];
// get destination pile
var dest = suit+'s';
// make move
if ( validateMove(card, dest) ) {
// set source pile
var pile = e.parentElement.parentElement;
$table.dataset.source = pile.dataset.pile;
// set dest pile
$table.dataset.dest = dest;
// make move
makeMove();
reset(table);
render(table, playedCards);
} else {
console.log('Move is Invalid. Try again...');
reset(table);
render(table, playedCards);
}
// let's do it again in 100ms
setTimeout(function() {
i--;
if (i !== 0) animation_loop();
// at the end lets celebrate!
else throwConfetti();
}, 100);
};
// run animation loop
animation_loop();
}
// throw confetti
/* Thanks to @gamanox
https://codepen.io/gamanox/pen/FkEbH
*/
function throwConfetti() {
console.log('Confetti!');
var COLORS, Confetti, NUM_CONFETTI, PI_2, canvas, confetti, context, drawCircle, drawCircle2, drawCircle3, i, range, xpos;
NUM_CONFETTI = 60;
COLORS = [[255, 255, 255], [255, 144, 0], [255, 255, 255], [255, 144, 0], [0, 277, 235]];
PI_2 = 2 * Math.PI;
canvas = d.getElementById("confetti");
context = canvas.getContext("2d");
window.w = 0;
window.h = 0;
window.resizeWindow = function() {
window.w = canvas.width = window.innerWidth;
return window.h = canvas.height = window.innerHeight;
};
window.addEventListener('resize', resizeWindow, false);
window.onload = function() {
return setTimeout(resizeWindow, 0);
};
range = function(a, b) {
return (b - a) * Math.random() + a;
};
drawCircle = function(x, y, r, style) {
context.beginPath();
context.moveTo(x, y);
context.bezierCurveTo(x - 17, y + 14, x + 13, y + 5, x - 5, y + 22);
context.lineWidth = 3;
context.strokeStyle = style;
return context.stroke();
};
drawCircle2 = function(x, y, r, style) {
context.beginPath();
context.moveTo(x, y);
context.lineTo(x + 10, y + 10);
context.lineTo(x + 10, y);
context.closePath();
context.fillStyle = style;
return context.fill();
};
drawCircle3 = function(x, y, r, style) {
context.beginPath();
context.moveTo(x, y);
context.lineTo(x + 10, y + 10);
context.lineTo(x + 10, y);
context.closePath();
context.fillStyle = style;
return context.fill();
};
xpos = 0.9;
d.onmousemove = function(e) {
return xpos = e.pageX / w;
};
window.requestAnimationFrame = (function() {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) {
return window.setTimeout(callback, 100 / 20);
};
})();
Confetti = (function() {
function Confetti() {
this.style = COLORS[~~range(0, 5)];
this.rgb = "rgba(" + this.style[0] + "," + this.style[1] + "," + this.style[2];
this.r = ~~range(2, 6);
this.r2 = 2 * this.r;
this.replace();
}
Confetti.prototype.replace = function() {
this.opacity = 0;
this.dop = 0.03 * range(1, 4);
this.x = range(-this.r2, w - this.r2);
this.y = range(-20, h - this.r2);
this.xmax = w - this.r;
this.ymax = h - this.r;
this.vx = range(0, 2) + 8 * xpos - 5;
return this.vy = 0.7 * this.r + range(-1, 1);
};
Confetti.prototype.draw = function() {
var ref;
this.x += this.vx;
this.y += this.vy;
this.opacity += this.dop;
if (this.opacity > 1) {
this.opacity = 1;
this.dop *= -1;
}
if (this.opacity < 0 || this.y > this.ymax) {
this.replace();
}
if (!((0 < (ref = this.x) && ref < this.xmax))) {
this.x = (this.x + this.xmax) % this.xmax;
}
drawCircle(~~this.x, ~~this.y, this.r, this.rgb + "," + this.opacity + ")");
drawCircle3(~~this.x * 0.5, ~~this.y, this.r, this.rgb + "," + this.opacity + ")");
return drawCircle2(~~this.x * 1.5, ~~this.y * 1.5, this.r, this.rgb + "," + this.opacity + ")");
};
return Confetti;
})();
confetti = (function() {
var j, ref, results;
results = [];
for (i = j = 1, ref = NUM_CONFETTI; 1 <= ref ? j <= ref : j >= ref; i = 1 <= ref ? ++j : --j) {
results.push(new Confetti);
}
return results;
})();
window.step = function() {
var c, j, len, results;
requestAnimationFrame(step);
context.clearRect(0, 0, w, h);
results = [];
for (j = 0, len = confetti.length; j < len; j++) {
c = confetti[j];
results.push(c.draw());
}
return results;
};
step();
// fix initial bug when firing
resizeWindow();
// fade in
canvas.style.opacity = 0;
var tick = function() {
canvas.style.opacity = +canvas.style.opacity + 0.01;
if ( +canvas.style.opacity < 1 ) {
( window.requestAnimationFrame &&
requestAnimationFrame(tick) ) ||
setTimeout(tick, 100)
}
};
tick();
}
Also see: Tab Triggers