Code Sketch
Set - the game
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()