Set!

A presentation at EmberFest in September 2022 in Paris, France by Anne-Greeth van Herwijnen

Slide 1

Slide 1

SET

Slide 2

Slide 2

SET THE CARD GAME By Anne-Greeth Schot-van Herwijnen

Slide 3

Slide 3

Slide 4

Slide 4

SHAPE

Slide 5

Slide 5

SHAPE FILLING

Slide 6

Slide 6

SHAPE FILLING AMOUNT

Slide 7

Slide 7

SHAPE FILLING COLOR AMOUNT GREEN RED PURPLE

Slide 8

Slide 8

Slide 9

Slide 9

Slide 10

Slide 10

Slide 11

Slide 11

SO WHAT’S A SET?

Slide 12

Slide 12

isSet(cardA, cardB, cardC) { return ( this.validateProps(cardA.shape, cardB.shape, cardC.shape) && this.validateProps(cardA.amount, cardB.amount, cardC.amount) && this.validateProps(cardA.filling, cardB.filling, cardC.filling) && this.validateProps(cardA.color, cardB.color, cardC.color) ); }

Slide 13

Slide 13

validateProps(propCardA, propCardB, propCardC) { return (propCardA === propCardB && propCardB === propCardC) || (propCardA !== propCardB && propCardA !== propCardC && propCardB !== propCardC); }

Slide 14

Slide 14

Slide 15

Slide 15

Slide 16

Slide 16

Slide 17

Slide 17

~% EMBER GENERATE COMPONENT CARD

Slide 18

Slide 18

<button …attributes data-test-card type=”button” {{on “click” (fn this.handleClick @image) }} local-class=”button {{if @selected “selected” “”}} {{if @wrong “wrong”}} {{if @hint “hint”}}”> <img src={{@image}} alt={{@image}} local-class=”img”/> </button>

Slide 19

Slide 19

<button …attributes data-test-card type=”button” {{on “click” (fn this.handleClick @image) }} local-class=”button {{if @selected “selected” “”}} {{if @wrong “wrong”}} {{if @hint “hint”}}”> <img src={{@image}} alt={{@image}} local-class=”img”/> </button> .button.hint { border: 3px dashed #4caf50; } .button.selected { border: 3px solid orange; } .button.wrong { border: 3px solid red; animation: shake … transform: translateX(0, 0, 0); }

Slide 20

Slide 20

<button …attributes data-test-card type=”button” {{on “click” (fn this.handleClick @image) }} local-class=”button {{if @selected “selected” “”}} {{if @wrong “wrong”}} {{if @hint “hint”}}”> <img src={{@image}} alt={{@image}} local-class=”img”/> </button> .button.hint { border: 3px dashed #4caf50; } .button.selected { border: 3px solid orange; } .button.wrong { border: 3px solid red; animation: shake … transform: translateX(0, 0, 0); } @keyframes shake { 10%, 90% { transform: translateX(-1px); } 20%, 80% { transform: translateX(2px); } 30%, 50%, 70% { transform: translateX(-4px); } 40%, 60% { transform: translateX(4px); } }

Slide 21

Slide 21

~% EMBER GENERATE COMPONENT PLAYINGFIELD

Slide 22

Slide 22

<div local-class=”playing-field”> {{#each this.field as |card|}} <Card @image={{card.image}} @selected={{card.selected}} @wrong={{card.wrong}} @hint={{card.hint}} @onSelect={{this.selectCard}}/> {{/each}} </div>

Slide 23

Slide 23

<div local-class=”playing-field”> {{#each this.field as |card|}} <Card @image={{card.image}} @selected={{card.selected}} @wrong={{card.wrong}} @hint={{card.hint}} @onSelect={{this.selectCard}}/> {{/each}} if (this.args.easy) { </div> this.cards = this.getSimpleDeck(); this.field = […this.getCards(9)]; while (!this.hasSet) { this.cards = this.getSimpleDeck(); this.field = […this.getCards(9)]; } } else { this.cards = this.getDeck(); this.field = […this.getCards(12)]; while (!this.hasSet) { this.cards = this.getDeck(); this.field = […this.getCards(12)]; } }

Slide 24

Slide 24

<div local-class=”playing-field”> {{#each this.field as |card|}} <Card @image={{card.image}} @selected={{card.selected}} @wrong={{card.wrong}} @hint={{card.hint}} @onSelect={{this.selectCard}}/> {{/each}} if (this.args.easy) { </div> getRandomCard(cards) { const randomIndex = Math.floor(Math.random() * cards.length); let randomCard = cards[randomIndex]; cards.splice(randomIndex, 1); return { rc: randomCard, cards: cards }; } this.cards = this.getSimpleDeck(); this.field = […this.getCards(9)]; while (!this.hasSet) { this.cards = this.getSimpleDeck(); this.field = […this.getCards(9)]; } } else { this.cards = this.getDeck(); this.field = […this.getCards(12)]; while (!this.hasSet) { this.cards = this.getDeck(); this.field = […this.getCards(12)]; } }

Slide 25

Slide 25

@task *timerTask() { while (true) { yield new Promise((resolve) => setTimeout(resolve, 1000)); this.time = Math.floor((Date.now() - this.startTime) / 1000); } }

Slide 26

Slide 26

@action getHint() { const combinations = this.k_combinations(this.field, 3); let foundSet = combinations.find((comb) => this.isSet(…comb)); foundSet[Math.floor(Math.random() * 3)].hint = true; if (this.hintsActive < 3) { this.hintCounter++; this.hintsActive++; } }

Slide 27

Slide 27

let highscoresString = localStorage.getItem(‘highscores’); this.highscores = JSON.parse(highscoresString)?.sort((a, b) => a > b) localStorage.setItem(‘highscores’, JSON.stringify(this.highscores)); localStorage.clear();

Slide 28

Slide 28

“ember-web-app”: “^5.0.1”

Slide 29

Slide 29

“ember-web-app”: “^5.0.1”

Slide 30

Slide 30

Slide 31

Slide 31

Slide 32

Slide 32

MULTIPLAYER COLOR PICKING

Slide 33

Slide 33

Slide 34

Slide 34

Slide 35

Slide 35

SET-THE-GAME.NETLIFY.APP GITHUB: @MINTHAMIE TWITTER: @AGVANHERWIJNEN INSTAGRAM: @A.G.VANHERWIJNEN NOTIST: NOTI.ST/MINTHAMIE