# f9dcff83372dc8408276b3d13e4d94c152045baea4e93c37ee2cdafa18102b34 Elan 2.0.0-alpha valid

main
  variable game set to 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)
    variable k set to waitForKey().lowerCase()
    if k.equals("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)
  variable dealer set to game.dealer
  variable faceCard set to dealer.faceCard.getValue()
  for player in game.players
    call player.startTurn()
    call display(game)
    while player.status is Status.active
      call player.nextAction(faceCard)
      call display(game)
    end while
  end for
  call dealer.play()
  call display(game)
  while dealer.status is Status.active
    call dealer.nextAction(faceCard)
    call display(game)
  end while
  call game.updatePoints()
end procedure

procedure display(game as Game)
  variable html set to $"<style>{styleSheet}</style>{htmlForGame(game)}"
  call displayHtml(html)
  call sleep(1.5)
end procedure

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

function determinePlayerOutcome(dealer as Dealer, player as Player) returns Outcome
  # These 'let' definitions are just to make the logic that follows them simpler to read
  variable d set to dealer.status
  variable dTotal set to dealer.handTotal
  variable p set to player.status
  variable pTotal set to player.handTotal
  variable bust set to Status.bust
  variable bj set to Status.blackjack
  variable win set to Outcome.win
  variable winDouble set to Outcome.winDouble
  variable lose set to Outcome.lose
  variable draw set to Outcome.draw
  variable playerOutcome set to draw
  if p is bust then
    set playerOutcome to lose
  elif (p is bj) and (d isnt bj) then
    set playerOutcome to winDouble
  elif d is bust then
    set playerOutcome to win
  elif (d is bj) and (p is bj) then
    set playerOutcome to draw
  elif p is bj then
    set playerOutcome to winDouble
  elif d is bj then
    set playerOutcome to lose
  elif pTotal > dTotal then
    set playerOutcome to win
  elif pTotal < dTotal then
    set playerOutcome to lose
  else
    # strictly, this 'else' clause is redundant - as the variable was initialised to 'draw' - but added for clarity
    set playerOutcome to draw
  end if
  return playerOutcome
end function

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

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

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

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

test 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

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

test test_htmlForGame
  variable c1 set to new Card("3", Suit.clubs, false)
  variable c2 set to new Card("K", Suit.spades, true)
  variable p set to new HumanPlayer("fred", 10) with cards set to [c1, c2]
  variable players set to (new List<of Player>()).withAppend(p)
  variable g2 set to 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 - hand total: 0</div><div class='hand'><div class='card black'><div class='u'>3</div><div class='v'>&amp;clubs;</div><div class='a'>&amp;clubs;</div><div class='b'>&amp;clubs;</div><div class='c'>&amp;clubs;</div></div><div class='card reversed'></div></div></div><div class='message'></div></div>"
end test

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'>"
  for card in player.cards
    variable suit set to card.suit
    variable rank set to card.rank
    set html to html + htmlForCard(card)
  end for
  return html + "</div></div>"
end function

test test_htmlForPlayer
  variable c1 set to new Card("3", Suit.clubs, false)
  variable c2 set to new Card("K", Suit.spades, true)
  variable p set to new HumanPlayer("charlie", 10) with cards set to [c1, c2]
  assert htmlForPlayer(p) is "<div class='player'><div class='details'>charlie - 10 points - hand total: 0</div><div class='hand'><div class='card black'><div class='u'>3</div><div class='v'>&amp;clubs;</div><div class='a'>&amp;clubs;</div><div class='b'>&amp;clubs;</div><div class='c'>&amp;clubs;</div></div><div class='card reversed'></div></div></div>"
end test

function htmlForCard(card as Card) returns String
  variable html set to ""
  if card.faceDown then
    set html to "<div class='card reversed'>"
  else
    variable rank set to card.rank
    variable suit set to card.suit
    variable colour set to colourForSuit(suit)
    variable symbol set to symbolForSuit(suit)
    set html to $"<div class='card {colour}'>"
    variable u set to htmlForSpot("u", rank)
    variable v set to htmlForSpot("v", symbol)
    variable grid set to ""
    for location in gridForRank(rank)
      if location.equals("royal") then
        set grid to grid + htmlForSpot(location, rank)
      else
        set grid to grid + htmlForSpot(location, symbol)
      end if
    end for
    set html to html + $"{u}{v}{grid}"
  end if
  return html + "</div>"
end function

test test_htmlForCard
  variable c1 set to new Card("3", Suit.clubs, false)
  assert htmlForCard(c1) is "<div class='card black'><div class='u'>3</div><div class='v'>&amp;clubs;</div><div class='a'>&amp;clubs;</div><div class='b'>&amp;clubs;</div><div class='c'>&amp;clubs;</div></div>"
  variable c2 set to new Card("K", Suit.spades, true)
  assert htmlForCard(c2) is "<div class='card reversed'></div>"
end test

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

test test_htmlForSpot
  assert htmlForSpot("c", "&amp;hearts;") is "<div class='c'>&amp;hearts;</div>"
  assert htmlForSpot("u", "10") is "<div class='u'>10</div>"
end test

class Game
  constructor(dealerStartPoints as Int)
    set this.dealer to new Dealer(dealerStartPoints)
    set this.players to new List<of Player>()
    set this.message to ""
  end constructor

  property dealer as Dealer

  property players as List<of Player>

  property message as String

  procedure newRound()
    variable dealer set to this.dealer
    call dealer.newHand()
    for player in this.players
      call player.newHand()
    end for
  end procedure

  procedure updatePoints()
    for player in this.players
      variable playerOutcome set to determinePlayerOutcome(this.dealer, player)
      call global.updatePoints(this.dealer, player, playerOutcome)
    end for
  end procedure

  procedure addPlayer(player as Player)
    variable players set to this.players
    call players.append(player)
  end procedure

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

end class

class Card
  property suit as Suit

  property rank as String

  property faceDown as Boolean

  constructor(rank as String, suit as Suit, facedown as Boolean)
    set this.rank to rank
    set this.suit to suit
    set this.faceDown to facedown
  end constructor

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

  procedure turnFaceDown()
    set this.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 this.status is Status.active then
      set this.hasTurn to true
    end if
  end procedure

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

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

  procedure draw()
    variable newCard set to dealCard(random())
    variable cards set to this.cards
    call cards.append(newCard)
    if newCard.rank.equals("A") then
      call addAce()
    else
      set this.handTotal to this.handTotal + rankValue()[newCard.rank]
    end if
    call evaluateStatus(newCard)
  end procedure

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

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

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

  abstract procedure newHand()

  procedure newHandHelper()
    set this.hasTurn to false
    set this.softAce to false
    set this.cards to new List<of Card>()
    set this.handTotal to 0
    set this.status to Status.active
    call draw()
    call draw()
  end procedure

  abstract function getMessage() returns String

  function statusAsString() returns String
    variable msg set to ""
    variable status set to this.status
    if this.hasTurn then
      set msg to msg + " - PLAYING"
    elif status is Status.standing then
      set msg to msg + " - STANDING"
    elif status is Status.blackjack then
      set msg to msg + " - BLACKJACK"
    elif 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 this.name to "Dealer"
    set this.points to startingPoints
    set this.faceCard to new Maybe<of Card>()
  end constructor

  property faceCard as Maybe<of Card>

  property hasPlayed as Boolean

  procedure play()
    call startTurn()
    variable hiddenCard set to this.cards[1]
    call hiddenCard.turnFaceUp()
    set this.hasPlayed to true
  end procedure

  procedure newHand()
    set this.hasPlayed to false
    call newHandHelper()
    variable faceCard set to this.faceCard
    call faceCard.set(this.cards[0])
    variable hiddenCard set to this.cards[1]
    call hiddenCard.turnFaceDown()
  end procedure

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

  function getMessage() returns String
    variable msg set to ""
    if this.hasPlayed then
      set msg to statusAsString() + $" - hand total: {this.handTotal}"
    end if
    return msg
  end function

end class

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

  procedure newHand()
    call newHandHelper()
  end procedure

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

  function getMessage() returns String
    variable msg set to statusAsString() + $"- hand total: {this.handTotal}"
    if this.hasTurn then
      set msg to msg + " - press 'd' to draw, 's' to stand"
    end if
    return msg
  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

function symbolForSuit(suit as Suit) returns String
  variable dict set to [Suit.clubs:"&amp;clubs;", Suit.diamonds:"&amp;diams;", Suit.hearts:"&amp;hearts;", Suit.spades:"&amp;spades;"]
  return dict[suit]
end function

function rankValue() returns Dictionary<of String, Int>
  return ["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]
end function

function gridForRank(rank as String) returns List<of String>
  variable dict set to ["A":["royal"], "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":["royal"], "Q":["royal"], "K":["royal"]]
  return dict[rank]
end function

function colourForSuit(suit as Suit) returns String
  variable dict set to [Suit.clubs:"black", Suit.diamonds:"red", Suit.hearts:"red", Suit.spades:"black"]
  return dict[suit]
end function

constant styleSheet set to ':root {    background-color: darkgreen;    padding-left: 5px;}.game {    padding: 5px;}.message, .details  {    color: white;    font-family: Arial, Helvetica, sans-serif;}.hand {        margin-top: 5px;        height: 150px;        padding-bottom: 10px;    }    .card {    position: relative;    float: left;    background-color: white;    width: 95px;    height:140px;    margin-right:10px;    padding: 5px;    border-radius: 5px;    font-family: Helvetica, sans-serif; }.royal,.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;}/* Standard spots */     .a,.b,.c,.d,.e,.f,.g,.h,.i,.l,.m,.n,.o,.p,.r,.s,.t  {font-size:  30px;}    /* columns */    .d,.n,.h,.p,.f {left: 18px }    .a,.b,.c,.l,.m,.s,.t {left: 43px;}    .e,.o,.i,.r,.g {left: 68px}    /* rows */    .d,.b,.e {top: 0px}    .s {top: 20px;}    .l {top: 28px;}    .n,.o {top: 37px;}    .h,.a,.i {top: 57px}    .p,.r {top: 75px;}    .m {top: 86px;}    .t {top: 93px;}    .f,.c,.g {top: 114px;}/* royals */    .royal {        position: absolute;        z-index: 1;        width: 95px;        height: 140px;        line-height: 140px;        font-size: 100px;    }/* corner summary */    .u {font-size: 15px; width: 15px; text-align: center; left: 0px; top: 2px;}    .v {font-size: 20px; width: 15px; text-align: center; left: 0px; top: 12px;}/* suit colors */    .red {color: red}    .black {color: black}/* back */    .card.reversed { background-color: rgba(0, 0, 255, 0.607);}'
