Code Sketch


Set - the game
By: windiana
Category: Programming
def shapeRaute() : Picture = {
    scale(2,1) * rot(45) * trans(-7,-7) -> Picture.rectangle(14, 14)
}

def shapeEllipse() : Picture = {
    Picture.ellipse(20, 9)
}

def shapeSnake() : Picture = {
    val part = Picture.ellipse(12, 5)
    picStack(trans(-8,0) * rot(25) -> part, trans(8,0) * rot(25) -> part)
}

def innerShape(shape: Picture, c: Color, fill: Int) : Picture = {
    val innerfill = brit(if(fill==1) 0.8 else 0.0) * sat(if(fill==1) -0.8 else 0.0) * 
        fillColor(if(fill<2) c else white) * penColor(noColor) -> shape 
    val inner = fillColor(noColor) * penColor(c) -> shape 
    picStack(innerfill, inner)
}

def repeatShape(cnt: Int, shape: Picture) : Picture = {
    cnt match {
        case 1 => shape
        case 2 => picStack(trans(0,-12) -> shape, trans(0,12) -> shape)
        case 3 => picStack(trans(0,-25) -> shape, shape, trans(0,25) -> shape)
    }
}

def shapeNum(i: Int) : Picture = {
    i match {
        case 0 => shapeEllipse()
        case 1 => shapeRaute()
        case 2 => shapeSnake()
    }
}

def colorNum(i: Int) : Color = {
    i match {
        case 0 => green
        case 1 => purple
        case 2 => red
    }
}

def mydraw(pic: Picture) {
    // in order to avoid double call of draw(pic), we need to call pic.visible() instead
    if(!pic.isDrawn) {
        draw(pic)
    } else {
        pic.visible()
    }    
}

case class Card(cnt: Int, fill: Int, shape: Int, color: Int) {
    val border = trans(-28,-45) * penColor(black) * fillColor(white) -> Picture.rectangle(56, 90) 
    val drawShape = picStack(border, repeatShape(cnt+1, innerShape(shapeNum(shape),colorNum(color),fill)))
    val drawShape2 = picStack(border, repeatShape(cnt+1, innerShape(shapeNum(shape),colorNum(color),fill)))
    val mark = penColor(orange) * penWidth(4)-> Picture.ellipse(10, 10)
    val mark2 = penColor(orange) * penWidth(4) -> Picture.ellipse(10, 10)
    def drawAt(i: Int, j: Int) {
        // draw Card on normal board (calling draw() twice needs to be avoided)
        if(!drawShape.isDrawn) {
            val translation = trans(-770 + i*65, -80 + j*100)
            draw(translation -> drawShape)
            mark.setPosition(drawShape.position)
        }
    }
    def draw4dAt(i: Int, j: Int, k: Int, l: Int) {
        // draw Card on 4D-hypercube (calling draw() twice needs to be avoided)
        if(!drawShape2.isDrawn) {
            val translation = trans(-200 + (i-1)*110 + k*20 + l*370, (j-1) * 140 + k*15)
            draw(translation -> drawShape2)
            mark2.setPosition(drawShape2.position)
            if (mark.isDrawn) {
                mydraw(mark2)
            }
        }
    }
    def draw4d() {
        draw4dAt(cnt, fill, shape, color)
    }
    def markCard() {
        mydraw(mark)
        if (drawShape2.isDrawn) {
            mydraw(mark2)
        }
    }
    def unmark() {
        mark.invisible()
        mark2.invisible()
    }
    def invisible() {
        // removing a Card is hard, thus we make it only invisible
        drawShape.invisible()
        drawShape2.invisible()
        mark.invisible()
        mark2.invisible()
    }
    def getAttributes() : List[Int] = {
        List(cnt, fill, shape, color)
    }
    def isVisible() : Boolean = {
        drawShape.isVisible
    }
}

def drawGrid() {
    // draw grid for 4D hypercube (more lines might help but could also distract)
    val w = 2*110+55+4
    val h = 2*140+90+4
    for(shape <- 0 to 2) {
        for(color <- 0 to 2) {
            draw(opac(-0.5) * penColor(black) * penWidth(1) * trans(-200 - w/2 + shape*20 + color*370, - h/2 + shape*15) -> 
                Picture.rectangle(w, h))
        }
    }
}

case class PlayerState(id: Int, game: Game, key: String) {
    var score = 0
    val button = Button(s"Player $id: Set!")({game.player_said_set(id)})
    var scoreLabel = Picture.textu(s"$score", 18, black)
    var keyLabel = penColor(black) -> Picture.text(s"$key", 18)
    val marker = penColor(orange) * penWidth(4) -> Picture.rectangle(120,30)
    val shape = picStack(Picture.widget(button), trans(60,60) -> scoreLabel, trans(35,0) -> keyLabel)
    def asShape() : Picture = {
        shape
    }
    def setScore(s: Int) {
        score = s
        scoreLabel.update(score)
    }
    def incScore(delta: Int) {
        setScore(score + delta)
    }
    def decScore(delta: Int) {
        setScore(score - delta)
    }
    def mark() {
        // since the shape is positioned outside PlayerState, we copy position
        marker.setPosition(shape.position)
        mydraw(marker)
    }
    def unmark() {
        marker.invisible()
    }
}

def genCards() = for(i <- 0 to 3*3*3*3-1) yield Card(i%3,(i/3)%3,(i/9)%3,(i/27)%3)

class Countdown(label: Picture, timeoutFn: () => Unit) {
    var active = false
    var downCounter = 10
    var secCounter = 0
    def start() {
        downCounter = 10
        label.update(s"$downCounter")
        secCounter = 0
        active = true
    }
    def stop() {
        active = false
        label.update("")
    }
    timer(100) {
        runInGuiThread {
            if(active) {
                secCounter += 1
                if(secCounter == 10) {
                    downCounter -= 1
                    label.update(s"$downCounter")
                    secCounter = 0
                    if(downCounter == 0) {
                        label.update("timeout!")
                        active = false
                        timeoutFn()
                    }
                }
            }
        }
    }
}

case class Game(numPlayers: Int) {
    val playerKeyLabels = List("Space", "Enter", "Backspace", "L", "", "", "")
    val playerKeys = Map((Kc.VK_SPACE,1),(Kc.VK_A,1),(Kc.VK_ENTER,2), (Kc.VK_F,2), (Kc.VK_BACK_SPACE,3), (Kc.VK_J,3), (Kc.VK_L,4))
    val players = for(i <- 0 until numPlayers) yield PlayerState(i+1,this, playerKeyLabels(i))
    val buttonMore = Button(s"More Cards")({onMoreCards()})
    val button4d = Button(s"Show 4D")({openCards.foreach{card => if(card.isVisible()) card.draw4d()}})
    val buttonNewGame = Button(s"New Game")({onNewGame()})
    val countdownLabel = Picture.textu("", 18, black)
    val countdown = new Countdown(countdownLabel, onTimeout)
    var activePlayer = -1
    val deck = util.Random.shuffle(genCards()).toBuffer
    var openCards = List[Card]()
    var markedCards = List[Card]()
    var blockAction = false
    buttonNewGame.setFocusable(false)  // Enter should never click button
    button4d.setFocusable(false)  // Enter should never click button
    buttonMore.setFocusable(false)  // Enter should never click button
    onKeyPress { k =>
        if (playerKeys.contains(k)) {
            val playerId = playerKeys(k)
            if (playerId <= players.size) {
                player_said_set(playerId)
            }
        }
    }
    def onNewGame() {
        if(!blockAction && activePlayer < 0) {
            openCards.foreach{card => card.invisible()}
            markedCards.foreach{card => card.unmark()}            
            openCards = List[Card]()
            markedCards = List[Card]()
            players.foreach{player => player.setScore(0)}
            deck.remove(0, deck.size)
            deck.appendAll(util.Random.shuffle(genCards()))            
            newCards(12)
            refresh()
        }
    }
    def onMoreCards() {
        if(!blockAction && activePlayer < 0 && 
                openCards.filter(card => card.isVisible()).size < 7*3){
            newCards(3)
            refresh()
        }
    }
    def onTimeout() {
        if(!blockAction && activePlayer >= 0) {
            failedSet()
        }
    }
    def player_said_set(id: Int) {
        if (!blockAction && activePlayer < 0) {
            activePlayer = id-1
            players(activePlayer).mark()
            countdown.start()
        }
    }
    def newCards(n: Int) {
        // we don't want to move cards already drawn, so we rather replace invisible ones
        if(openCards.filter(card => card.isVisible()).size + n <= openCards.size) {
            replaceCards(openCards.filter(card => !card.isVisible()).slice(0,n))
        } else {
            val newN = Math.min(n, deck.size)
            openCards = openCards ++ deck.slice(0,newN)
            deck.remove(0,newN)
        }
    }
    def replaceCards(cards: List[Card]) {
        val newN = Math.min(cards.size, deck.size)
        val cardMap = Map[Card,Card]() ++ cards.slice(0,newN).zip(deck.slice(0,newN))
        // replace cards in-place of list so positions of other cards don't change
        openCards = openCards.map(card => if(cards.contains(card)) cardMap(card) else card)
        deck.remove(0,newN)
    }
    def onCardClick(card: Card, game: Game) {
        if(!blockAction){
            card.draw4d()
            if(activePlayer >= 0 && !markedCards.contains(card) && markedCards.size < 3) {
                card.markCard()
                markedCards = markedCards ++ List(card)
                countdown.start()
                if (markedCards.size >= 3) {
                    countdown.stop()
                    checkSet()
                }
            }
        }
    }
    def checkSet() {
        var foundSet = true
        for(attribute <- 0 until 4) {
            var sum=0
            markedCards.foreach{
                card => sum += card.getAttributes()(attribute)
            }
            // adding three numbers (0,1,2) we check for all same or all different
            // by checking their sum against 0,3,6
            if(!List(0,3,6).contains(sum)){
                foundSet=false
            }
        }
        if (foundSet) {
            successSet()
        } else {
            failedSet()
        }
    }
    def successSet() {
        if(activePlayer >= 0) players(activePlayer).incScore(1)
        blockAction = true
        runInBackground {
            Thread.sleep(1000) // allow user to see third click
            markedCards.foreach{card => card.invisible()}
            if (openCards.filter(card => card.isVisible()).size < 12 && deck.size > 0) {
                replaceCards(markedCards)
            }
            nextSet()
            blockAction = false
        }
    }
    def failedSet() {
        if(activePlayer >= 0) players(activePlayer).decScore(1)
        blockAction = true
        runInBackground {
            Thread.sleep(1000) // allow user to see third click
            markedCards.foreach{card => card.unmark()}
            nextSet()     
            blockAction = false
        }
    }
    def nextSet() {
        markedCards = List[Card]()
        if(activePlayer >= 0) players(activePlayer).unmark()
        activePlayer = -1
        refresh()
    }
    def drawGame() {
        // this function is called only once to avoid double call of draw()
        drawGrid()
        players.zip(0 until players.size).foreach{
            case (player, idx) => draw(trans(-800 + idx*150, -200) -> player.asShape())
        }
        draw(trans(-800, 180) -> Picture.widget(buttonMore))
        draw(trans(-800 + 120, 180) -> Picture.widget(button4d))
        draw(trans(-800 + 220, 180) -> Picture.widget(buttonNewGame))
        draw(trans(-800 + 350, 205) -> countdownLabel)
        refresh()
    }
    def refresh() {
        openCards.zip(0 until openCards.size).foreach{
            case (card,i) => {
                card.drawAt(i/3,i%3);     
                card.drawShape.onMouseClick { (x, y) => onCardClick(card, game) }
                card.drawShape2.onMouseClick { (x, y) => onCardClick(card, game) }
            }
        }
    }
}

cleari()
toggleFullScreenCanvas()
val game = Game(2)
game.newCards(12)
game.drawGame()
activateCanvas()