· 6 years ago · Aug 19, 2019, 03:22 AM
1/* ### TODO ###
2- Refactor code :) Always
3
4Optional Features:
5- HTML Drag & Drop API
6- Limit How Many Times Stock Can Be Reloaded (3x)
7- 3 Card Draw
8- High score
9- Options panel for user
10- Sound Fx
11
12*/
13
14// 0. DECLARE VARS
15
16 // document
17 var d = document;
18
19 // build deck
20 var deck = [];
21
22 // build suits
23 var suits = [];
24 suits['spades'] = [
25 // spades
26 ['A','spade'],
27 ['2','spade'],
28 ['3','spade'],
29 ['4','spade'],
30 ['5','spade'],
31 ['6','spade'],
32 ['7','spade'],
33 ['8','spade'],
34 ['9','spade'],
35 ['10','spade'],
36 ['J','spade'],
37 ['Q','spade'],
38 ['K','spade']
39 ];
40 suits['hearts'] = [
41 // hearts
42 ['A','heart'],
43 ['2','heart'],
44 ['3','heart'],
45 ['4','heart'],
46 ['5','heart'],
47 ['6','heart'],
48 ['7','heart'],
49 ['8','heart'],
50 ['9','heart'],
51 ['10','heart'],
52 ['J','heart'],
53 ['Q','heart'],
54 ['K','heart']
55 ];
56 suits['diamonds'] = [
57 // diamonds
58 ['A','diamond'],
59 ['2','diamond'],
60 ['3','diamond'],
61 ['4','diamond'],
62 ['5','diamond'],
63 ['6','diamond'],
64 ['7','diamond'],
65 ['8','diamond'],
66 ['9','diamond'],
67 ['10','diamond'],
68 ['J','diamond'],
69 ['Q','diamond'],
70 ['K','diamond']
71 ];
72 suits['clubs'] = [
73 // clubs
74 ['A','club'],
75 ['2','club'],
76 ['3','club'],
77 ['4','club'],
78 ['5','club'],
79 ['6','club'],
80 ['7','club'],
81 ['8','club'],
82 ['9','club'],
83 ['10','club'],
84 ['J','club'],
85 ['Q','club'],
86 ['K','club']
87 ];
88
89 // build stock pile
90 var s = [];
91
92 // build waste pile
93 var w = [];
94
95 // build foundations
96 var spades = [];
97 var hearts = [];
98 var diamonds = [];
99 var clubs = [];
100
101 // build tableau
102 var t = [];
103 t[1] = t[2] = t[3] = t[4] = t[5] = t[6] = t[7] = [];
104
105 // build table
106 var table = [];
107 table['stock'] = s;
108 table['waste'] = w;
109 table['spades'] = spades;
110 table['hearts'] = hearts;
111 table['diamonds'] = diamonds;
112 table['clubs'] = clubs;
113 table['tab'] = t;
114
115 // initial face up cards
116 var playedCards =
117 '#waste .card,' +
118 '#fnd .card,' +
119 '#tab .card:last-child';
120
121 // cache selectors
122 var $timer = d.querySelector('#score .timer');
123 var $timerSpan = d.querySelector('#score .timer span');
124 var $moveCount = d.querySelector('#score .move-count');
125 var $moveCountSpan = d.querySelector('#score .move-count span');
126 var $score = d.querySelector('#score .score');
127 var $scoreSpan = d.querySelector('#score .score span');
128 var $playPause = d.querySelector('#play-pause');
129 var $table = d.querySelector('#table');
130 var $upper = d.querySelector('#table .upper-row');
131 var $lower = d.querySelector('#table .lower-row');
132 var $stock = d.querySelector('#stock');
133 var $waste = d.querySelector('#waste');
134 var $fnd = d.querySelector('#fnd');
135 var $tab = d.querySelector('#tab');
136 var $autoWin = d.querySelector('#auto-win');
137
138 // other global vars
139 var clock = 0;
140 var time = 0;
141 var moves = 0;
142 var score = 0;
143 var bonus = 0;
144 var lastEventTime = 0;
145 var unplayedTabCards = [];
146
147// 1. CREATE DECK
148 deck = create(deck, suits);
149
150// 2. SHUFFLE DECK
151 deck = shuffle(deck);
152
153// 3. DEAL DECK
154 table = deal(deck, table);
155
156// 4. RENDER TABLE
157 render(table, playedCards);
158
159// 5. START GAMEPLAY
160 play(table);
161
162// ### EVENT HANDLERS ###
163 window.onresize = function(event) {
164 sizeCards();
165 };
166
167// ### FUNCTIONS ###
168
169 // create deck
170 function create(deck, suits) {
171 console.log('Creating Deck...');
172 // loop through each suit
173 for (var suit in suits) {
174 suit = suits[suit];
175 // loop through each card in suit
176 for (var card in suit) {
177 card = suit[card];
178 deck.push(card); // push card to deck
179 }
180 }
181 return deck;
182 }
183
184 // shuffle deck
185 function shuffle(deck) {
186 console.log('Shuffling Deck...');
187 // declare vars
188 var i = deck.length, temp, rand;
189 // while there remain elements to shuffle
190 while (0 !== i) {
191 // pick a remaining element
192 rand = Math.floor(Math.random() * i);
193 i--;
194 // and swap it with the current element
195 temp = deck[i];
196 deck[i] = deck[rand];
197 deck[rand] = temp;
198 }
199 return deck;
200 }
201
202 // deal deck
203 function deal(deck, table) {
204 console.log('Dealing Deck...');
205 // move all cards to stock
206 table['stock'] = deck;
207 // build tableau
208 var tabs = table['tab'];
209 // loop through 7 tableau rows
210 for (var row = 1; row <= 7; row++) {
211 // loop through 7 piles in row
212 for (var pile = row; pile <= 7; pile++) {
213 // build blank pile on first row
214 if (row === 1) tabs[pile] = [];
215 // deal card to pile
216 move(table['stock'], tabs[pile], false);
217 }
218 }
219 return table;
220 }
221
222 // move card
223 function move(source, dest, pop, selectedCards = 1) {
224 if (pop !== true) {
225 var card = source.shift(); // take card from bottom
226 dest.push(card); // push card to destination pile
227 } else {
228 while (selectedCards) {
229 // take card from the top of selection
230 var card = source[source.length - selectedCards];
231 // remove it from the selected pile
232 source.splice(source.length - selectedCards, 1);
233 // put it in the destination pile
234 dest.push(card);
235 // decrement
236 selectedCards--;
237 }
238 }
239 return;
240 }
241
242 // render table
243 function render(table, playedCards) {
244 console.log('Rendering Table...');
245
246 // check for played cards
247 playedCards = checkForPlayedCards(playedCards);
248
249 // check for empty piles
250 emptyPiles = checkForEmptyPiles(table);
251
252 // update stock pile
253 update(table['stock'], '#stock ul', playedCards, true);
254 // update waste pile
255 update(table['waste'], '#waste ul', playedCards);
256 // update spades pile
257 update(table['spades'], '#spades ul', playedCards);
258 // update hearts pile
259 update(table['hearts'], '#hearts ul', playedCards);
260 // update diamonds pile
261 update(table['diamonds'], '#diamonds ul', playedCards);
262 // update clubs pile
263 update(table['clubs'], '#clubs ul', playedCards);
264 // update tableau
265 var tabs = table['tab'];
266 // loop through tableau piles
267 for (var i = 1; i <= 7; i++) {
268 // update tableau pile
269 update(tabs[i], '#tab li:nth-child('+i+') ul', playedCards, true);
270 }
271
272 // get unplayed tab cards
273 unplayedTabCards = getUnplayedTabCards();
274
275 // size cards
276 sizeCards();
277
278 // show table
279 $table.style.opacity = '100';
280
281 console.log('Table Rendered:', table);
282 return;
283 }
284
285 // update piles
286 function update(pile, selector, playedCards, append) {
287 var e = d.querySelector(selector);
288 var children = e.children; // get children
289 var grandParent = e.parentElement.parentElement; // get grand parent
290 // reset pile
291 e.innerHTML = '';
292 // loop through cards in pile
293 for (var card in pile) {
294 card = pile[card];
295 // get html template for card
296 var html = getTemplate(card);
297 // create card in pile
298 createCard(card, selector, html, append);
299 }
300 // turn cards face up
301 flipCards(playedCards, 'up');
302 // count played cards
303 var played = countPlayedCards(children);
304 e.parentElement.dataset.played = played;
305 // count all played cards for #tab and #fnd piles
306 if ( grandParent.id === 'tab' || grandParent.id === 'fnd' ) {
307 var playedAll = parseInt(grandParent.dataset.played);
308 if ( isNaN(playedAll) ) playedAll = 0;
309 grandParent.dataset.played = playedAll + played;
310 }
311 // count unplayed cards
312 var unplayed = countUnplayedCards(children);
313 e.parentElement.dataset.unplayed = unplayed;
314 // count all unplayed cards for #tab and #fnd piles
315 if ( grandParent.id === 'tab' || grandParent.id === 'fnd' ) {
316 var unplayedAll = parseInt(grandParent.dataset.unplayed);
317 if ( isNaN(unplayedAll) ) unplayedAll = 0;
318 grandParent.dataset.unplayed = unplayedAll + unplayed;
319 }
320 return pile;
321 }
322
323 // get html template for card
324 function getTemplate(card) {
325 var r = card[0]; // get rank
326 var s = card[1]; // get suit
327 // get html template
328 var html = d.querySelector('.template li[data-rank="'+r+'"]').innerHTML;
329 // search and replace suit variable
330 html = html.replace('{{suit}}', s);
331 return html;
332 }
333
334 // create card in pile
335 function createCard(card, selector, html, append) {
336 var r = card[0]; // get rank
337 var s = card[1]; // get suit
338 // get pile based on selector
339 if ( selector.includes('#stock') ) var p = 'stock';
340 if ( selector.includes('#waste') ) var p = 'waste';
341 if ( selector.includes('#spades') ) var p = 'spades';
342 if ( selector.includes('#hearts') ) var p = 'hearts';
343 if ( selector.includes('#diamonds') ) var p = 'diamonds';
344 if ( selector.includes('#clubs') ) var p = 'clubs';
345 if ( selector.includes('#tab') ) var p = 'tab';
346 var e = d.createElement('li'); // create li element
347 e.className = 'card'; // add .card class to element
348 e.dataset.rank = r; // set rank atribute
349 e.dataset.suit = s; // set suit attribute
350 e.dataset.pile = p; // set pile attribute;
351 e.dataset.selected = 'false'; // set selected attribute
352 e.innerHTML = html; // insert html to element
353 // query for pile
354 var pile = d.querySelector(selector);
355 // append to pile
356 if (append) pile.appendChild(e);
357 // or prepend to pile
358 else pile.insertBefore(e, pile.firstChild);
359 return;
360 }
361
362 // check for played cards
363 function checkForPlayedCards(playedCards) {
364 // query
365 var els = d.querySelectorAll('.card[data-played="true"]');
366 for (var e in els) { // loop through elements
367 e = els[e];
368 if (e.nodeType) {
369 var r = e.dataset.rank;
370 var s = e.dataset.suit;
371 playedCards += ', .card[data-rank="'+r+'"][data-suit="'+s+'"]' ;
372 }
373 }
374 return playedCards;
375 }
376
377 // check for empty piles
378 function checkForEmptyPiles(table) {
379 // reset empty data on all piles
380 var els = d.querySelectorAll('.pile'); // query elements
381 for (var e in els) { // loop through elements
382 e = els[e];
383 if (e.nodeType) {
384 delete e.dataset.empty;
385 }
386 }
387 // declare var with fake pile so we always have one
388 var emptyPiles = '#fake.pile';
389 // check spades pile
390 if ( table['spades'].length === 0 ) {
391 emptyPiles += ', #fnd #spades.pile';
392 }
393 // check hearts pile
394 if ( table['hearts'].length === 0 ) {
395 emptyPiles += ', #fnd #hearts.pile';
396 }
397 // check diamonds pile
398 if ( table['diamonds'].length === 0 ) {
399 emptyPiles += ', #fnd #diamonds.pile';
400 }
401 // check clubs pile
402 if ( table['clubs'].length === 0 ) {
403 emptyPiles += ', #fnd #clubs.pile';
404 }
405 // check tableau piles
406 var tabs = table['tab'];
407 // loop through tableau piles
408 for (var i = 1; i <= 7; i++) {
409 // check tabeau pile
410 if ( tabs[i].length === 0 ) {
411 emptyPiles += ', #tab li:nth-child('+i+').pile';
412 }
413 }
414 // mark piles as empty
415 els = d.querySelectorAll(emptyPiles); // query elements
416 for (var e in els) { // loop through elements
417 e = els[e];
418 if (e.nodeType) {
419 e.dataset.empty = 'true'; // mark as empty
420 }
421 }
422 return emptyPiles;
423 }
424
425 // count played cards
426 function countPlayedCards(cards) {
427 var played = 0;
428 // loop through cards
429 for (var card in cards) {
430 card = cards[card];
431 if (card.nodeType) {
432 // check if card has been played
433 if (card.dataset.played === 'true') played++;
434 }
435 }
436 return played;
437 }
438
439 // count unplayed cards
440 function countUnplayedCards(cards) {
441 var unplayed = 0;
442 // loop through cards
443 for (var card in cards) {
444 card = cards[card];
445 if (card.nodeType) {
446 // check if card has been played
447 if (card.dataset.played !== 'true') unplayed++;
448 }
449 }
450 return unplayed;
451 }
452
453 // flip cards
454 function flipCards(selectors, direction) {
455 var els = d.querySelectorAll(selectors); // query all elements
456 for (var e in els) { // loop through elements
457 e = els[e];
458 if (e.nodeType) {
459 switch(direction) {
460 case 'up' :
461 if (e.dataset.played !== 'true') {
462 // if flipping over tableau card
463 if (e.dataset.pile === 'tab') {
464 // loop through unplayed cards
465 for (var card in unplayedTabCards) {
466 card = unplayedTabCards[card];
467 // if rank and suit matches
468 if ( e.dataset.rank === card[0] &&
469 e.dataset.suit === card[1] )
470 // score 5 points
471 updateScore(5);
472 }
473 }
474 e.className += ' up'; // add class
475 e.dataset.played = 'true'; // mark as played
476 }
477 break;
478 case 'down' :
479 e.className = 'card'; // reset class
480 delete e.dataset.played; // reset played data attribute
481 default : break;
482 }
483 }
484 }
485 return;
486 }
487
488 // get face down cards in tableau pile
489 function getUnplayedTabCards() {
490 // reset array
491 unplayedTabCards = [];
492 // get all face down card elements
493 var els = d.querySelectorAll('#tab .card:not([data-played="true"])');
494 for (var e in els) { // loop through elements
495 e = els[e];
496 if (e.nodeType) {
497 unplayedTabCards.push( [ e.dataset.rank, e.dataset.suit ] );
498 }
499 }
500 return unplayedTabCards;
501 }
502
503 // size cards
504 function sizeCards(selector = '.pile', ratio = 1.4) {
505 var s = selector;
506 var r = ratio;
507 var e = d.querySelector(s); // query element
508 var h = e.offsetWidth * r; // get height of element
509 // set row heights
510 $upper.style.height = h + 10 + 'px';
511 $lower.style.height = h + 120 + 'px';
512 // set height of elements
513 var els = d.querySelectorAll(s); // query all elements
514 for (var e in els) { // loop through elements
515 e = els[e];
516 if (e.nodeType) e.style.height = h + 'px'; // set height in css
517 }
518 }
519
520 // gameplay
521 function play(table) {
522 // check for winning table
523 if ( checkForWin(table) ) return;
524 // check for autowin
525 checkForAutoWin(table);
526 // bind click events
527 bindClick(
528 '#stock .card:first-child,' +
529 '#waste .card:first-child,' +
530 '#fnd .card:first-child,' +
531 '#tab .card[data-played="true"]'
532 );
533 // bind dbl click events
534 bindClick(
535 '#waste .card:first-child,' +
536 '#tab .card:last-child',
537 'double'
538 );
539 console.log('Make Your Move...');
540 console.log('......');
541 }
542
543 // bind click events
544 function bindClick(selectors, double) {
545 var elements = d.querySelectorAll(selectors); // query all elements
546 // loop through elements
547 for (var e in elements) {
548 e = elements[e];
549 // add event listener
550 if (e.nodeType) {
551 if (!double) e.addEventListener('click', select);
552 else e.addEventListener('dblclick', select);
553 }
554 }
555 return;
556 }
557
558 // unbind click events
559 function unbindClick(selectors, double) {
560 var elements = d.querySelectorAll(selectors); // query all elements
561 // loop through elements
562 for (var e in elements) {
563 e = elements[e];
564 // remove event listener
565 if (e.nodeType) {
566 if (!double) e.removeEventListener('click', select);
567 else e.removeEventListener('dblclick', select);
568 }
569 }
570 return;
571 }
572
573 // on click handler: select
574 var clicks = 0; // set counter for counting clicks
575 var clickDelay = 200; // set delay for double click
576 var clickTimer = null; // set timer for timeout function
577 function select(event) {
578
579 // prevent default
580 event.preventDefault();
581
582 // start timer
583 if ( $timer.dataset.action !== 'start' ) {
584 timer('start');
585 }
586
587 // if timestamp matches then return false
588 var time = event.timeStamp; // get timestamp
589 if ( time === lastEventTime ) {
590 console.log('Status: Timestamp Matches, False Click');
591 return false;
592 }
593 else {
594 lastEventTime = time; // cache timestamp
595 }
596
597 // get variables
598 var e = event.target; // get element
599 var isSelected = e.dataset.selected; // get selected attribute
600 var rank = e.dataset.rank; // get rank attribute
601 var suit = e.dataset.suit; // get suit attribute
602 var pile = e.dataset.pile; // get pile attribute
603 var action = e.dataset.action; // get action attribute
604
605 // create card array
606 if (rank && suit) var card = [rank,suit];
607
608 // count clicks
609 clicks++;
610
611 // single click
612 if (clicks === 1 && event.type === 'click') {
613 clickTimer = setTimeout(function() {
614 console.log('Single Click Detected', event);
615
616 // reset click counter
617 clicks = 0;
618
619 // if same card is clicked
620 if (e.dataset.selected === 'true') {
621 console.log('Status: Same Card Clicked');
622 // deselect card
623 delete e.dataset.selected;
624 delete $table.dataset.move;
625 delete $table.dataset.selected;
626 delete $table.dataset.source;
627 console.log('Card Deselected', card, e);
628 }
629
630 // if move is in progress
631 else if ($table.dataset.move) {
632 console.log('Status: A Move Is In Progess');
633 // get selected
634 var selected = $table.dataset.selected.split(',');
635 // update table dataset with destination pile
636 $table.dataset.dest = e.closest('.pile').dataset.pile;
637 // get destination card or pile
638 if ( card ) var dest = card;
639 else var dest = $table.dataset.dest;
640 // validate move
641 if ( validateMove(selected, dest) ) {
642 // make move
643 makeMove();
644 reset(table);
645 render(table, playedCards);
646 play(table);
647 } else {
648 console.log('Move is Invalid. Try again...');
649 reset(table);
650 render(table, playedCards);
651 play(table);
652 console.log('Card Deselected', card, e);
653 }
654 }
655
656 // if stock is clicked
657 else if (pile === 'stock') {
658 console.log('Status: Stock Pile Clicked');
659 // if stock isn't empty
660 if (table['stock'].length) {
661 // move card from stock to waste
662 move(table['stock'], table['waste']);
663 reset(table);
664 render(table, playedCards);
665 // if empty, then bind click to stock pile element
666 if (table['stock'].length === 0) bindClick('#stock .reload-icon');
667 // count move
668 countMove(moves++);
669 // return to play
670 play(table);
671 }
672 }
673
674 // if stock reload icon is clicked
675 else if (action === 'reload') {
676 console.log('Reloading Stock Pile');
677 // remove event listener
678 unbindClick('#stock .reload-icon');
679 // reload stock pile
680 if (table['waste'].length) {
681 table['stock'] = table['waste']; // move waste to stock
682 table['waste'] = [] // empty waste
683 }
684 // render table
685 render(table, playedCards);
686 // turn all stock cards face down
687 flipCards('#stock .card', 'down');
688 // update score by -100 pts
689 updateScore(-100);
690 // return to play
691 play(table);
692 }
693
694 // if no move is in progress
695 else {
696 // select card
697 e.dataset.selected = 'true';
698 $table.dataset.move = 'true';
699 $table.dataset.selected = card;
700 $table.dataset.source = e.closest('.pile').dataset.pile;
701 // if ace is selected
702 if (rank === 'A') {
703 console.log('Ace Is Selected');
704 bindClick('#fnd #'+suit+'s.pile[data-empty="true"]');
705 }
706 if (rank === 'K') {
707 console.log('King Is Selected');
708 bindClick('#tab .pile[data-empty="true"]');
709 }
710 }
711
712 }, clickDelay);
713 }
714
715 // double click
716 else if (event.type === 'dblclick') {
717 console.log('Double Click Detected', event);
718 clearTimeout(clickTimer); // prevent single click
719 clicks = 0; // reset click counter
720 // select card
721 e.dataset.selected = 'true';
722 $table.dataset.move = 'true';
723 $table.dataset.selected = card;
724 $table.dataset.source = e.closest('.pile').dataset.pile;
725 // get destination pile
726 if ( card) var dest = card[1]+'s';
727 // update table dataset with destination
728 $table.dataset.dest = dest;
729 // validate move
730 if ( validateMove(card, dest) ) {
731 // make move
732 makeMove();
733 reset(table);
734 render(table, playedCards);
735 play(table);
736 } else {
737 console.log('Move is Invalid. Try again...');
738 reset(table);
739 render(table, playedCards);
740 play(table);
741 console.log('Card Deselected', card, e);
742 }
743
744 }
745
746 }
747
748 // validate move
749 function validateMove(selected, dest) {
750 console.log ('Validating Move...', selected, dest);
751
752 // if selected card exists
753 if (selected) {
754 var sRank = parseRankAsInt(selected[0]);
755 var sSuit = selected[1];
756 }
757
758 // if destination is another card
759 if (dest.constructor === Array) {
760 console.log('Desitination appears to be a card');
761 var dRank = parseRankAsInt(dest[0]);
762 var dSuit = dest[1];
763 var dPile = $table.dataset.dest;
764 // if destination pile is foundation
765 if (['spades','hearts','diamonds','clubs'].indexOf(dPile) >= 0) {
766 // if rank isn't in sequence then return false
767 if (dRank - sRank !== -1) {
768 console.log('Rank sequence invalid');
769 console.log(dRank - sRank)
770 return false;
771 }
772 // if suit isn't in sequence then return false
773 if ( sSuit !== dSuit ) {
774 console.log('Suit sequence invalid');
775 return false;
776 }
777 }
778 // if destination pile is tableau
779 else {
780 // if rank isn't in sequence then return false
781 if (dRank - sRank !== 1) {
782 console.log('Rank sequence invalid');
783 return false;
784 }
785 // if suit isn't in sequence then return false
786 if ( ( (sSuit === 'spade' || sSuit === 'club') &&
787 (dSuit === 'spade' || dSuit === 'club') ) ||
788 ( (sSuit === 'heart' || sSuit === 'diamond') &&
789 (dSuit === 'heart' || dSuit === 'diamond') ) ) {
790 console.log('Suit sequence invalid');
791 return false;
792 }
793 }
794 // else return true
795 console.log('Valid move');
796 return true;
797
798 }
799
800 // if destination is foundation pile
801 if (['spades','hearts','diamonds','clubs'].indexOf(dest) >= 0) {
802 console.log('Destination appears to be empty foundation');
803
804 // get last card in destination pile
805 var lastCard = d.querySelector('#'+dest+' .card:first-child');
806 if (lastCard) {
807 var dRank = parseRankAsInt(lastCard.dataset.rank);
808 var dSuit = lastCard.dataset.suit;
809 }
810 // if suit doesn't match pile then return false
811 if ( sSuit + 's' !== dest ) {
812 console.log('Suit sequence invalid');
813 return false;
814 }
815 // if rank is ace then return true
816 else if ( sRank === 1 ) {
817 console.log('Valid Move');
818 return true;
819 }
820 // if rank isn't in sequence then return false
821 else if ( sRank - dRank !== 1 ) {
822 console.log('Rank sequence invalid');
823 return false;
824 }
825 // else return true
826 else {
827 console.log('Valid move');
828 return true;
829 }
830 }
831
832 // if destination is empty tableau pile
833 if ( dest >= 1 && dest <= 7 ) {
834 console.log('Destination appears tp be empty tableau');
835 return true;
836 }
837
838 }
839
840 // make move
841 function makeMove() {
842 console.log('Making Move...');
843
844 // get source and dest
845 var source = $table.dataset.source;
846 var dest = $table.dataset.dest;
847 console.log('From '+source+' pile to '+dest+' pile');
848
849 // if pulling card from waste pile
850 if ( source === 'waste') {
851 // if moving card to foundation pile
852 if ( isNaN(dest) ) {
853 console.log('Moving To Foundation Pile');
854 move(table[source], table[dest], true);
855 updateScore(10); // score 10 pts
856 }
857 // if moving card to tableau pile
858 else {
859 console.log('Moving To Tableau Pile');
860 move(table[source], table['tab'][dest], true);
861 updateScore(5); // score 5 pts
862 }
863 }
864
865 // if pulling card from foundation pile
866 else if (['spades','hearts','diamonds','clubs'].indexOf(source) >= 0) {
867 // only allow moves to tableau piles
868 if ( isNaN(dest) ) {
869 console.log('That move is not allowed');
870 return false;
871 }
872 // if moving card to tableau pile
873 else {
874 console.log('Moving To Tableau Pile');
875 move(table[source], table['tab'][dest], true);
876 updateScore(-15); // score -15 pts
877 }
878 }
879
880 // if pulling card from tableau pile
881 else {
882 // if moving card to foundation pile
883 if ( isNaN(dest) ) {
884 console.log('Moving To Foundation Pile');
885 move(table['tab'][source], table[dest], true);
886 updateScore(10); // score 10 pts
887 }
888 // if moving card to tableau pile
889 else {
890 console.log('Moving To Tableau Pile');
891 // get selected card
892 var selected = d.querySelector('.card[data-selected="true"');
893 // get cards under selected card
894 var selectedCards = [selected];
895 while ( selected = selected['nextSibling'] ) {
896 if (selected.nodeType) selectedCards.push(selected);
897 }
898 // move card(s)
899 move(
900 table['tab'][source],
901 table['tab'][dest],
902 true,
903 selectedCards.length
904 );
905 }
906 }
907
908 // unbind click events
909 unbindClick(
910 '#stock .card:first-child,' +
911 '#waste .card:first-child,' +
912 '#fnd .card:first-child,' +
913 '#fnd #spades.pile[data-empty="true"],' +
914 '#fnd #hearts.pile[data-empty="true"],' +
915 '#fnd #diamonds.pile[data-empty="true"],' +
916 '#fnd #clubs.pile[data-empty="true"],' +
917 '#tab .card[data-played="true"],' +
918 '#tab .pile[data-empty="true"]'
919 );
920 // unbind double click events
921 unbindClick(
922 '#waste .card:first-child' +
923 '#tab .card:last-child',
924 'double'
925 )
926
927 // count move
928 countMove(moves++);
929
930 // reset table
931 console.log('Ending Move...');
932
933 return;
934 }
935
936 // parse rank as integer
937 function parseRankAsInt(rank) {
938 // assign numerical ranks to letter cards
939 switch (rank) {
940 case 'A' : rank = '1'; break;
941 case 'J' : rank = '11'; break;
942 case 'Q' : rank = '12'; break;
943 case 'K' : rank = '13'; break;
944 default : break;
945 }
946 // return integer value for rank
947 return parseInt(rank);
948 }
949
950 // parse integer as rank
951 function parseIntAsRank(int) {
952 // parse as integer
953 rank = parseInt(int);
954 // assign letter ranks to letter cards
955 switch(rank) {
956 case 1 : rank = 'A'; break;
957 case 11 : rank = 'J'; break;
958 case 12 : rank = 'Q'; break;
959 case 13 : rank = 'K'; break;
960 default : break;
961 }
962 return rank;
963 }
964
965 // reset table
966 function reset(table) {
967 delete $table.dataset.move;
968 delete $table.dataset.selected;
969 delete $table.dataset.source;
970 delete $table.dataset.dest;
971 delete $fnd.dataset.played;
972 delete $fnd.dataset.unplayed;
973 delete $tab.dataset.played;
974 delete $tab.dataset.unplayed;
975 console.log('Table reset');
976 }
977
978 // timer funcion
979 function timer(action) {
980 // declare timer vars
981 var minutes = 0;
982 var seconds = 0;
983 var gameplay = d.body.dataset.gameplay;
984 // set timer attribute
985 $timer.dataset.action = action;
986 // switch case
987 switch (action) {
988 // start timer
989 case 'start' :
990 console.log('Starting Timer...');
991 // looping function
992 clock = setInterval(function() {
993 // increment
994 time++;
995 // parse minutes and seconds
996 minutes = parseInt(time / 60, 10);
997 seconds = parseInt(time % 60, 10);
998 minutes = minutes < 10 ? "0" + minutes : minutes;
999 seconds = seconds < 10 ? "0" + seconds : seconds;
1000 // output to display
1001 $timerSpan.textContent = minutes + ':' + seconds;
1002 // if 10 seconds has passed decrement score by 2 pts
1003 if ( time % 10 === 0 ) updateScore(-2);
1004 }, 1000);
1005 // add dataset to body
1006 d.body.dataset.gameplay = 'active';
1007 // unbind click to play button
1008 if ( gameplay === 'paused')
1009 $playPause.removeEventListener('click', playTimer);
1010 // bind click to pause button
1011 $playPause.addEventListener('click', pauseTimer = function(){
1012 timer('pause');
1013 });
1014 break;
1015 // pause timer
1016 case 'pause' :
1017 console.log('Pausing Timer...');
1018 clearInterval(clock);
1019 d.body.dataset.gameplay = 'paused';
1020 // unbind click to pause button
1021 if ( gameplay === 'active')
1022 $playPause.removeEventListener('click', pauseTimer);
1023 // bind click tp play button
1024 $playPause.addEventListener('click', playTimer = function(){
1025 timer('start');
1026 });
1027 break;
1028 // stop timer
1029 case 'stop' :
1030 console.log('Stoping Timer...');
1031 clearInterval(clock);
1032 d.body.dataset.gameplay = 'over';
1033 break;
1034 // default
1035 default : break;
1036 }
1037 console.log(time);
1038 return;
1039 }
1040
1041 // move counter
1042 function countMove(moves) {
1043 console.log('Move Counter', moves);
1044 // set move attribute
1045 $moveCount.dataset.moves = moves + 1;
1046 // output to display
1047 $moveCountSpan.textContent = moves + 1;
1048 return;
1049 }
1050
1051 // scoring function
1052 /*
1053 Standard scoring is determined as follows:
1054 - Waste to Tableau 5
1055 - Waste to Foundation 10
1056 - Tableau to Foundation 10
1057 - Turn over Tableau card 5
1058 - Foundation to Tableau −15
1059 - Recycle waste when playing by ones −100
1060 (minimum score is 0)
1061
1062 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.
1063
1064 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.
1065 */
1066 function updateScore(points) {
1067 console.log('Updating Score', points);
1068 // get score
1069 score = parseInt($score.dataset.score) + points;
1070 // set minimum score to 0
1071 score = score < 0 ? 0 : score;
1072 // parse as integer
1073 score = parseInt(score);
1074 // set score attribute
1075 $score.dataset.score = score;
1076 // output to display
1077 $score.children[1].textContent = score;
1078 return score;
1079 }
1080
1081 // calculate bonus points
1082 function getBonus() {
1083 if (time >= 30) bonus = parseInt(700000 / time);
1084 console.log(bonus);
1085 return bonus;
1086 }
1087
1088 // check for win
1089 function checkForWin(table) {
1090 // if all foundation piles are full
1091 if ( table['spades'].length +
1092 table['hearts'].length +
1093 table['diamonds'].length +
1094 table['clubs'].length
1095 === 52 ) {
1096 console.log('Game Has Been Won');
1097 // stop timer
1098 timer('stop');
1099 // bonus points for time
1100 updateScore(getBonus());
1101 // throw confetti
1102 throwConfetti();
1103 // return true
1104 return true;
1105 }
1106 else return false;
1107 }
1108
1109 // check for auto win
1110 function checkForAutoWin(table) {
1111 // if all tableau cards are played and stock is empty
1112 if ( parseInt($tab.dataset.unplayed) +
1113 table['stock'].length +
1114 table['waste'].length === 0) {
1115 // show auto win button
1116 $autoWin.style.display = 'block';
1117 // bind click to auto win button
1118 $autoWin.addEventListener('click', autoWin);
1119 }
1120 return;
1121 }
1122
1123 // auto win
1124 function autoWin() {
1125 console.log('Huzzah!');
1126 // hide auto win button
1127 $autoWin.style.display = 'none';
1128 // unbind click to auto win button
1129 $autoWin.removeEventListener('click', autoWin);
1130 // unbind click events
1131 unbindClick(
1132 '#stock .card:first-child,' +
1133 '#waste .card:first-child,' +
1134 '#fnd .card:first-child,' +
1135 '#fnd #spades.pile[data-empty="true"],' +
1136 '#fnd #hearts.pile[data-empty="true"],' +
1137 '#fnd #diamonds.pile[data-empty="true"],' +
1138 '#fnd #clubs.pile[data-empty="true"],' +
1139 '#tab .card[data-played="true"],' +
1140 '#tab .pile[data-empty="true"]'
1141 );
1142 // unbind double click events
1143 unbindClick(
1144 '#waste .card:first-child' +
1145 '#tab .card:last-child',
1146 'double'
1147 );
1148 // reset table
1149 reset(table);
1150 render(table);
1151 // animate cards to foundation piles
1152 autoWinAnimation(table);
1153 // stop timer
1154 timer('stop');
1155 // bonus points for time
1156 updateScore(getBonus());
1157 }
1158
1159 // auto win animation
1160 function autoWinAnimation(table) {
1161 // set number of iterations
1162 var i = parseInt($tab.dataset.played);
1163 // create animation loop
1164 function animation_loop() {
1165 // get lowest ranking card
1166 var bottomCards = []; // create array for the bottom cards
1167 var els = d.querySelectorAll('#tab .card:last-child');
1168 for (var e in els) { // loop through elements
1169 e = els[e];
1170 if (e.nodeType)
1171 bottomCards.push( parseRankAsInt(e.dataset.rank) );
1172 }
1173 // get the lowest rank from array of bottom cards
1174 var lowestRank = Math.min.apply(Math, bottomCards);
1175 // parse integer as rank
1176 var rank = parseIntAsRank(lowestRank);
1177 // get element with rank
1178 var e = d.querySelector('#tab .card[data-rank="'+rank+'"]');
1179
1180 // setup move
1181 // get suit of card
1182 var suit = e.dataset.suit;
1183 // create card array with rank and suit
1184 var card = [rank, suit];
1185 // get destination pile
1186 var dest = suit+'s';
1187
1188 // make move
1189 if ( validateMove(card, dest) ) {
1190 // set source pile
1191 var pile = e.parentElement.parentElement;
1192 $table.dataset.source = pile.dataset.pile;
1193 // set dest pile
1194 $table.dataset.dest = dest;
1195 // make move
1196 makeMove();
1197 reset(table);
1198 render(table, playedCards);
1199 } else {
1200 console.log('Move is Invalid. Try again...');
1201 reset(table);
1202 render(table, playedCards);
1203 }
1204 // let's do it again in 100ms
1205 setTimeout(function() {
1206 i--;
1207 if (i !== 0) animation_loop();
1208 // at the end lets celebrate!
1209 else throwConfetti();
1210 }, 100);
1211 };
1212 // run animation loop
1213 animation_loop();
1214 }
1215
1216 // throw confetti
1217 /* Thanks to @gamanox
1218 https://codepen.io/gamanox/pen/FkEbH
1219 */
1220 function throwConfetti() {
1221 console.log('Confetti!');
1222
1223 var COLORS, Confetti, NUM_CONFETTI, PI_2, canvas, confetti, context, drawCircle, drawCircle2, drawCircle3, i, range, xpos;
1224
1225 NUM_CONFETTI = 60;
1226
1227 COLORS = [[255, 255, 255], [255, 144, 0], [255, 255, 255], [255, 144, 0], [0, 277, 235]];
1228
1229 PI_2 = 2 * Math.PI;
1230
1231 canvas = d.getElementById("confetti");
1232
1233 context = canvas.getContext("2d");
1234
1235 window.w = 0;
1236
1237 window.h = 0;
1238
1239 window.resizeWindow = function() {
1240 window.w = canvas.width = window.innerWidth;
1241 return window.h = canvas.height = window.innerHeight;
1242 };
1243
1244 window.addEventListener('resize', resizeWindow, false);
1245
1246 window.onload = function() {
1247 return setTimeout(resizeWindow, 0);
1248 };
1249
1250 range = function(a, b) {
1251 return (b - a) * Math.random() + a;
1252 };
1253
1254 drawCircle = function(x, y, r, style) {
1255 context.beginPath();
1256 context.moveTo(x, y);
1257 context.bezierCurveTo(x - 17, y + 14, x + 13, y + 5, x - 5, y + 22);
1258 context.lineWidth = 3;
1259 context.strokeStyle = style;
1260 return context.stroke();
1261 };
1262
1263 drawCircle2 = function(x, y, r, style) {
1264 context.beginPath();
1265 context.moveTo(x, y);
1266 context.lineTo(x + 10, y + 10);
1267 context.lineTo(x + 10, y);
1268 context.closePath();
1269 context.fillStyle = style;
1270 return context.fill();
1271 };
1272
1273 drawCircle3 = function(x, y, r, style) {
1274 context.beginPath();
1275 context.moveTo(x, y);
1276 context.lineTo(x + 10, y + 10);
1277 context.lineTo(x + 10, y);
1278 context.closePath();
1279 context.fillStyle = style;
1280 return context.fill();
1281 };
1282
1283 xpos = 0.9;
1284
1285 d.onmousemove = function(e) {
1286 return xpos = e.pageX / w;
1287 };
1288
1289 window.requestAnimationFrame = (function() {
1290 return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) {
1291 return window.setTimeout(callback, 100 / 20);
1292 };
1293 })();
1294
1295 Confetti = (function() {
1296 function Confetti() {
1297 this.style = COLORS[~~range(0, 5)];
1298 this.rgb = "rgba(" + this.style[0] + "," + this.style[1] + "," + this.style[2];
1299 this.r = ~~range(2, 6);
1300 this.r2 = 2 * this.r;
1301 this.replace();
1302 }
1303
1304 Confetti.prototype.replace = function() {
1305 this.opacity = 0;
1306 this.dop = 0.03 * range(1, 4);
1307 this.x = range(-this.r2, w - this.r2);
1308 this.y = range(-20, h - this.r2);
1309 this.xmax = w - this.r;
1310 this.ymax = h - this.r;
1311 this.vx = range(0, 2) + 8 * xpos - 5;
1312 return this.vy = 0.7 * this.r + range(-1, 1);
1313 };
1314
1315 Confetti.prototype.draw = function() {
1316 var ref;
1317 this.x += this.vx;
1318 this.y += this.vy;
1319 this.opacity += this.dop;
1320 if (this.opacity > 1) {
1321 this.opacity = 1;
1322 this.dop *= -1;
1323 }
1324 if (this.opacity < 0 || this.y > this.ymax) {
1325 this.replace();
1326 }
1327 if (!((0 < (ref = this.x) && ref < this.xmax))) {
1328 this.x = (this.x + this.xmax) % this.xmax;
1329 }
1330 drawCircle(~~this.x, ~~this.y, this.r, this.rgb + "," + this.opacity + ")");
1331 drawCircle3(~~this.x * 0.5, ~~this.y, this.r, this.rgb + "," + this.opacity + ")");
1332 return drawCircle2(~~this.x * 1.5, ~~this.y * 1.5, this.r, this.rgb + "," + this.opacity + ")");
1333 };
1334
1335 return Confetti;
1336
1337 })();
1338
1339 confetti = (function() {
1340 var j, ref, results;
1341 results = [];
1342 for (i = j = 1, ref = NUM_CONFETTI; 1 <= ref ? j <= ref : j >= ref; i = 1 <= ref ? ++j : --j) {
1343 results.push(new Confetti);
1344 }
1345 return results;
1346 })();
1347
1348 window.step = function() {
1349 var c, j, len, results;
1350 requestAnimationFrame(step);
1351 context.clearRect(0, 0, w, h);
1352 results = [];
1353 for (j = 0, len = confetti.length; j < len; j++) {
1354 c = confetti[j];
1355 results.push(c.draw());
1356 }
1357 return results;
1358 };
1359
1360 step();
1361
1362 // fix initial bug when firing
1363 resizeWindow();
1364
1365 // fade in
1366 canvas.style.opacity = 0;
1367 var tick = function() {
1368 canvas.style.opacity = +canvas.style.opacity + 0.01;
1369 if ( +canvas.style.opacity < 1 ) {
1370 ( window.requestAnimationFrame &&
1371 requestAnimationFrame(tick) ) ||
1372 setTimeout(tick, 100)
1373 }
1374 };
1375 tick();
1376
1377 }