# 80d770ec2e29d1feccfa058380b99b48a657eb31b1f3db6d03dfc46498b4f758 Elan 1.7.0 guest default_profile valid

main
  let game be new Game(1000)
  call game.addPlayer(new HumanPlayer("Player A", 1000))
  variable anotherRound set to true
  while anotherRound
    call playOneRound(game)
    call game.setMessage("Points updated. Do you want to play another round? (press y or n)")
    call clearKeyBuffer()
    call display(game)
    let k be waitForKey().lowerCase()
    if k is 'y' then
      call game.setMessage("")
    else
      set anotherRound to false
    end if
  end while
end main

procedure playOneRound(game as Game)
  call game.newRound()
  call display(game)
  let dealer be game.dealer
  each player in game.players
    call player.startTurn()
    call display(game)
    while player.status is Status.active
      call player.nextAction(dealer.faceCard)
      call display(game)
    end while
  end each
  call dealer.play()
  while dealer.status is Status.active
    call dealer.nextAction(dealer.faceCard)
    call display(game)
  end while
  call game.updatePoints()
end procedure

procedure display(game as Game)
  let html be "<style>{styleSheet}</style>{htmlForGame(game)}"
  call displayHtml(html)
  call pause(1500)
end procedure

procedure updatePoints(dealer as Dealer, player as Player, outcome as Outcome)
  if outcome is Outcome.winDouble then
    call player.changePointsBy(2)
    call dealer.changePointsBy(-2)
  else if outcome is Outcome.win then
    call player.changePointsBy(1)
    call dealer.changePointsBy(-1)
  else if outcome is Outcome.lose then
    call player.changePointsBy(-1)
    call dealer.changePointsBy(1)
  end if
end procedure

function determineOutcome(dealer as Dealer, player as Player) returns Outcome
  let d be dealer.status
  let dTot be dealer.handTotal
  let p be player.status
  let pTot be player.handTotal
  let bust be Status.bust
  let bj be Status.blackjack
  let win be Outcome.win
  let winDouble be Outcome.winDouble
  let lose be Outcome.lose
  let draw be Outcome.draw
  return if p is bust then lose else if (p is bj) and (d isnt bj) then winDouble else if d is bust then win else if (d is bj) and (p is bj) then draw else if p is bj then winDouble else if d is bj then lose else if pTot > dTot then win else if pTot < dTot then lose else draw
end function

function dealCard(random as Float) returns Card
  let number be (random*52).floor()
  let rank be rankValue.keys()[number div 4]
  let suit be number mod 4
  return new Card() with rank set to rank, suit set to intAsSuit(suit)
end function

function intAsSuit(n as Int) returns Suit
  variable suit set to Suit.clubs
  if n is 1 then
    set suit to Suit.diamonds
  else if n is 2 then
    set suit to Suit.hearts
  else if n is 3 then
    set suit to Suit.spades
  end if
  return suit
end function

function htmlForGame(game as Game) returns String
  variable html set to "<div class='game'>"
  set html to html + htmlForPlayer(game.dealer)
  each player in game.players
    set html to html + htmlForPlayer(player)
  end each
  set html to html + "<div class='message'>{game.message}</div>"
  return html + "</div>"
end function

function htmlForPlayer(player as Player) returns String
  variable html set to "<div class='player'>"
  set html to html + "<div class='details'>{player.name} - {player.points} points {player.getMessage()}</div>"
  set html to html + "<div class='hand'>"
  each card in player.cards
    let suit be card.suit
    let rank be card.rank
    set html to html + htmlForCard(card)
  end each
  return html + "</div>"
end function

function htmlForCard(card as Card) returns String
  variable html set to "<div class='card'>"
  if card.faceDown then
    set html to "<div class='card reversed'>"
  else
    let rank be card.rank
    let suit be card.suit
    let colour be colours[suit]
    let symbol be symbols[suit]
    let u be htmlForSpot("u", colour, rank)
    let v be htmlForSpot("v", colour, symbol)
    variable grid set to ""
    each location in gridByRank[rank]
      set grid to grid + htmlForSpot(location, colour, symbol)
    end each
    set html to html + "{u}{v}{grid}"
  end if
  return html + "</div>"
end function

function htmlForSpot(id as String, colour as String, content as String) returns String
  return "<div class='{id} {colour}'>{content}</div>"
end function

function strategyA(p as Player, dealerFaceUp as Card) returns Action
  return Action.stand
end function

function strategyB(p as Player, dealerFaceUp as Card) returns Action
  return Action.draw
end function

class Game
  constructor(dealerStartPoints as Int)
    set property.dealer to new Dealer(dealerStartPoints)
  end constructor

  property dealer as Dealer

  property players as List<of Player>

  property message as String

  procedure newRound()
    call property.dealer.newHand()
    each player in property.players
      call player.newHand()
    end each
  end procedure

  procedure updatePoints()
    each player in property.players
      let outcome be determineOutcome(property.dealer, player)
      call global.updatePoints(property.dealer, player, outcome)
    end each
  end procedure

  procedure addPlayer(player as Player)
    call property.players.append(player)
  end procedure

  procedure setMessage(message as String)
    set property.message to message
  end procedure

end class

class Card
  property suit as Suit

  property rank as String

  property faceDown as Boolean

  procedure turnFaceUp()
    set property.faceDown to false
  end procedure

  procedure turnFaceDown()
    set property.faceDown to true
  end procedure

end class

abstract class Player
  property name as String

  property points as Int

  property cards as List<of Card>

  property handTotal as Int

  property softAce as Boolean

  property status as Status

  property hasTurn as Boolean

  procedure startTurn()
    if property.status is Status.active then
      set property.hasTurn to true
    end if
  end procedure

  procedure evaluateStatus(newCard as Card)
    if (cardCount() is 2) and (property.handTotal is 21) then
      set property.status to Status.blackjack
    else if (property.handTotal > 21) and (property.softAce) then
      set property.handTotal to property.handTotal - 10
      set property.softAce to false
    else if property.handTotal > 21 then
      set property.status to Status.bust
    else if property.handTotal is 21 then
      set property.status to Status.standing
    end if
    if property.status isnt Status.active then
      set property.hasTurn to false
    end if
  end procedure

  procedure stand()
    set property.status to Status.standing
    set property.hasTurn to false
  end procedure

  procedure draw()
    let newCard be dealCard(random())
    call property.cards.append(newCard)
    if newCard.rank is "A" then
      call addAce()
    else
      set property.handTotal to property.handTotal + rankValue[newCard.rank]
    end if
    call evaluateStatus(newCard)
  end procedure

  procedure addAce()
    if property.softAce then
      set property.handTotal to property.handTotal + 1
    else
      set property.handTotal to property.handTotal + 11
      set property.softAce to true
    end if
  end procedure

  function cardCount() returns Int
    return property.cards.length()
  end function

  procedure changePointsBy(amount as Int)
    set property.points to property.points + amount
  end procedure

  abstract procedure newHand()

  procedure newHandHelper()
    set property.cards to empty List<of Card>
    set property.handTotal to 0
    set property.status to Status.active
    call draw()
    call draw()
  end procedure

  abstract function getMessage() returns String

  function messageHelper() returns String
    variable msg set to "- hand total: {property.handTotal}"
    let status be property.status
    if property.hasTurn then
      set msg to msg + " - PLAYING"
    else if status is Status.standing then
      set msg to msg + " - STANDING"
    else if status is Status.blackjack then
      set msg to msg + " - BLACKJACK"
    else if status is Status.bust then
      set msg to msg + " - BUST"
    end if
    return msg
  end function

  abstract procedure nextAction(dealerFaceCard as Card)

end class

class Dealer inherits Player
  constructor(startingPoints as Int)
    set property.name to "Dealer"
    set property.points to startingPoints
  end constructor

  property faceCard as Card

  procedure play()
    call startTurn()
    let hiddenCard be property.cards[1]
    call hiddenCard.turnFaceUp()
  end procedure

  procedure newHand()
    call newHandHelper()
    set property.faceCard to property.cards[0]
    let hiddenCard be property.cards[1]
    call hiddenCard.turnFaceDown()
  end procedure

  procedure nextAction(faceCard as Card)
    if property.handTotal < 17 then
      call draw()
    else
      call stand()
    end if
  end procedure

  function getMessage() returns String
    return messageHelper()
  end function

end class

class HumanPlayer inherits Player
  constructor(name as String, startingPoints as Int)
    set property.name to name
    set property.points to startingPoints
  end constructor

  procedure newHand()
    call newHandHelper()
  end procedure

  procedure nextAction(dealerFaceCard as Card)
    variable key set to ""
    call clearKeyBuffer()
    while key is ""
      set key to waitForKey().lowerCase()
      if key is "d" then
        call draw()
      else if key is "s" then
        call stand()
      else
        set key to ""
      end if
    end while
  end procedure

  function getMessage() returns String
    variable msg set to messageHelper()
    if property.hasTurn then
      set msg to msg + " - press 'd' to draw, 's' to stand"
    end if
    return msg
  end function

end class

class AutomatedPlayer inherits Player
  constructor(name as String, decisionFunc as Func<of Player, Card => Action>, startingPoints as Int)
    set property.name to name
    set property.decisionFunc to ref decisionFunc
    set property.points to startingPoints
  end constructor

  property decisionFunc as Func<of Player, Card => Action>

  procedure newHand()
    call newHandHelper()
  end procedure

  procedure nextAction(dealerFaceCard as Card)
    let act be decisionFunc(this, dealerFaceCard)
    if act is Action.draw then
      call draw()
    else
      call stand()
    end if
  end procedure

  function getMessage() returns String
    return messageHelper()
  end function

end class

enum Action stand, draw

enum Outcome undecided, lose, draw, win, winDouble

enum Status active, standing, blackjack, bust

enum Suit clubs, diamonds, hearts, spades

constant rankValue set to {"2":2, "3":3, "4":4, "5":5, "6":6, "7":7, "8":8, "9":9, "10":10, "J":10, "Q":10, "K":10, "A":11}

constant symbols set to {Suit.clubs:"&clubs;", Suit.diamonds:"&diams;", Suit.hearts:"&hearts;", Suit.spades:"&spades;"}

constant colours set to {Suit.clubs:"black", Suit.diamonds:"red", Suit.hearts:"red", Suit.spades:"black"}

constant gridByRank set to {"A":{"ace"}, "2":{"b", "c"}, "3":{"a", "b", "c"}, "4":{"d", "e", "f", "g"}, "5":{"a", "d", "e", "f", "g"}, "6":{"d", "e", "f", "g", "h", "i"}, "7":{"d", "e", "f", "g", "h", "i", "l"}, "8":{"d", "e", "f", "g", "h", "i", "l", "m"}, "9":{"a", "d", "e", "f", "g", "n", "o", "p", "r"}, "10":{"d", "e", "f", "g", "n", "o", "p", "r", "s", "t"}, "J":{"d", "g", "j"}, "Q":{"d", "g", "q"}, "K":{"d", "g", "k"}}

constant styleSheet set to ':root {\n    background-color: darkgreen;\n    padding-left: 5px;\n}\n\n.game .message, .player .details {\n    color: white;\n    font-family: Arial, Helvetica, sans-serif;\n}\n\n.hand {\n        margin-top: 5px;\n        height: 150px;\n        padding-bottom: 10px;\n    }\n    \n.card {\n    position: relative;\n    float: left;\n    background-color: white;\n    width: 95px;\n    height:140px;\n    margin-right:10px;\n    padding: 5px;\n    border-radius: 5px;\n    font-family: Helvetica, sans-serif; \n}\n.ace,.a,.b,.c,.d,.e,.f,.g,.h,.i,.j,.k,.l,.m,.n,.o,.p,.q,.r,.s,.t,.u,.v,.w,.x,.y,.z {position: absolute; text-align:center;}\n\n/* Standard spots */ \n    .a,.b,.c,.d,.e,.f,.g,.h,.i,.l,.m,.n,.o,.p,.r,.s,.t  {font-size:  30px;}\n\n    /* columns */\n    .d,.n,.h,.p,.f {left: 18px }\n    .a,.b,.c,.l,.m,.s,.t {left: 43px;}\n    .e,.o,.i,.r,.g {left: 68px}\n\n    /* rows */\n    .d,.b,.e {top: 0px}\n    .s {top: 20px;}\n    .l {top: 28px;}\n    .n,.o {top: 37px;}\n    .h,.a,.i {top: 57px}\n    .p,.r {top: 75px;}\n    .m {top: 86px;}\n    .t {top: 93px;}\n    .f,.c,.g {top: 114px;}\n\n/* royal pictures (temporary: just a large letter) */\n    .j,.q,.k,.ace {\n        top: 13px;\n        left: 22px;\n        height: 60px;\n        width: 60px;\n        font-size: 100px;\n        text-align: center;\n    }\n/* corner summary */\n    .u {font-size: 15px; width: 15px; text-align: center; left: 0px; top: 2px;}\n    .v {font-size: 20px; width: 15px; text-align: center; left: 0px; top: 12px;}\n\n/* suit colors */\n    .red {color: red}\n    .black {color: black}\n\n/* back */\n    .card.reversed { background-color: rgba(0, 0, 255, 0.607);}'

test intAsSuit
  assert intAsSuit(0) is Suit.clubs
  assert intAsSuit(1) is Suit.diamonds
  assert intAsSuit(2) is Suit.hearts
  assert intAsSuit(3) is Suit.spades
end test

test dealCard
  let c1 be dealCard(0)
  assert c1.rank is "2"
  assert c1.suit is Suit.clubs
  let c2 be dealCard(0.9999999)
  assert c2.rank is "A"
  assert c2.suit is Suit.spades
  let c3 be dealCard(0.5)
  assert c3.rank is "8"
  assert c3.suit is Suit.hearts
  let c4 be dealCard(0.24)
  assert c4.rank is "5"
  assert c4.suit is Suit.clubs
end test

test determineOutcome
  let dbj be new Dealer(0) with status set to Status.blackjack
  let d21 be new Dealer(0) with status set to Status.standing, handTotal set to 21
  let d17 be new Dealer(0) with status set to Status.standing, handTotal set to 17
  let dbu be new Dealer(0) with status set to Status.bust
  let pbj be new HumanPlayer("", 0) with status set to Status.blackjack
  let p21 be new HumanPlayer("", 0) with status set to Status.standing, handTotal set to 21
  let p17 be new HumanPlayer("", 0) with status set to Status.standing, handTotal set to 17
  let pbu be new HumanPlayer("", 0) with status set to Status.bust
  assert determineOutcome(dbj, pbj) is Outcome.draw
  assert determineOutcome(dbj, p21) is Outcome.lose
  assert determineOutcome(dbj, pbu) is Outcome.lose
  assert determineOutcome(d21, pbj) is Outcome.winDouble
  assert determineOutcome(d21, p21) is Outcome.draw
  assert determineOutcome(d21, p17) is Outcome.lose
  assert determineOutcome(d21, pbu) is Outcome.lose
  assert determineOutcome(dbu, pbj) is Outcome.winDouble
  assert determineOutcome(dbu, p17) is Outcome.win
  assert determineOutcome(dbu, pbu) is Outcome.lose
end test

[ghosted] test html generation
  assert htmlForSpot("c", "red", "&hearts;") is "<div class='c red'>&hearts;</div>"
  let c1 be new Card() with rank set to "3", suit set to Suit.clubs, faceDown set to false
  assert htmlForCard(c1) is "<div class='card'><div class='u black'>3</div><div class='v black'>&clubs;</div><div class='a black'>&clubs;</div><div class='b black'>&clubs;</div><div class='c black'>&clubs;</div></div>"
  let c2 be new Card() with rank set to "K", suit set to Suit.spades, faceDown set to true
  assert htmlForCard(c2) is "<div class='card reversed'></div>"
  let p be new HumanPlayer("fred", 10) with cards set to [c1, c2]
  assert htmlForPlayer(p) is "<div class='player'><div class='details'>fred - 10 points - hand total: 0</div><div class='hand'><div class='card'><div class='u black'>3</div><div class='v black'>&clubs;</div><div class='a black'>&clubs;</div><div class='b black'>&clubs;</div><div class='c black'>&clubs;</div></div><div class='card reversed'></div></div></div>"
  assert htmlForGame(new Game(1)) is "<div class='game'><div class='player'><div class='details'>Dealer - 1 points </div><div class='hand'></div></div></div>"
  let players be (empty List<of Player>).withAppend(p)
  let g2 be new Game(1) with players set to players
  assert htmlForGame(g2) is "<div class='game'><div class='player'><div class='details'>Dealer - 1 points </div><div class='hand'></div></div><div class='player'><div class='details'>fred - 10 points </div><div class='hand'><div class='card'><div class='u black'>3</div><div class='v black'>&clubs;</div><div class='a black'>&clubs;</div><div class='b black'>&clubs;</div><div class='c black'>&clubs;</div></div><div class='card reversed'></div></div></div></div>"
end test
