# 9b4db9c7c04abae34ee73a7669ea4d763b6e5989ccb8a58657387f7465748a01 Elan 2.0.0-alpha5 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
      assign 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
  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

function determinePlayerOutcome(dealer as Dealer, player as Player) returns Outcome
  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
    assign playerOutcome to lose
  elif (p is bj) and (d isnt bj) then
    assign playerOutcome to winDouble
  elif d is bust then
    assign playerOutcome to win
  elif (d is bj) and (p is bj) then
    assign playerOutcome to draw
  elif p is bj then
    assign playerOutcome to winDouble
  elif d is bj then
    assign playerOutcome to lose
  elif pTotal > dTotal then
    assign playerOutcome to win
  elif pTotal < dTotal then
    assign playerOutcome to lose
  else
    # strictly, this 'else' clause is redundant - as the variable was initialised to 'draw' - but added for clarity
    assign playerOutcome to draw
  end if
  return playerOutcome
end function

test test_determinePlayerOutcome
  let dbj be (new Dealer(0)).withStatus(Status.blackjack)
  assert dbj.status is Status.blackjack
  let d21 be (new Dealer(0)).withStatus(Status.standing).withHandTotal(21)
  assert d21.status is Status.standing
  assert d21.handTotal is 21
  let d17 be (new Dealer(0)).withStatus(Status.standing).withHandTotal(17)
  let dbu be (new Dealer(0)).withStatus(Status.bust)
  let pbj be (new HumanPlayer("", 0)).withStatus(Status.blackjack)
  assert pbj.status is Status.blackjack
  let p21 be (new HumanPlayer("", 0)).withStatus(Status.standing).withHandTotal(21)
  let p17 be (new HumanPlayer("", 0)).withStatus(Status.standing).withHandTotal(17)
  let pbu be (new HumanPlayer("", 0)).withStatus(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
  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

function intAsSuit(n as Int) returns Suit
  variable suit set to Suit.clubs
  if n is 1 then
    assign suit to Suit.diamonds
  elif n is 2 then
    assign suit to Suit.hearts
  elif n is 3 then
    assign 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'>"
  assign html to html + htmlForPlayer(game.dealer)
  for player in game.players
    assign html to html + htmlForPlayer(player)
  end for
  assign html to html + $"<div class='message'>{game.message}</div>"
  return html + "</div>"
end function

test test_htmlForGame
  let c1 be new Card("3", Suit.clubs, false)
  let c2 be new Card("K", Suit.spades, true)
  let p be (new HumanPlayer("fred", 10)).withCards([c1, c2])
  let players be (new List<of Player>()).withAppend(p)
  let g2 be (new Game(1)).withPlayers(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'>&clubs;</div><div class='a'>&clubs;</div><div class='b'>&clubs;</div><div class='c'>&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'>"
  assign html to html + $"<div class='details'>{player.name} - {player.points} points {player.getMessage()}</div>"
  assign html to html + "<div class='hand'>"
  for card in player.cards
    variable suit set to card.suit
    variable rank set to card.rank
    assign html to html + htmlForCard(card)
  end for
  return html + "</div></div>"
end function

test test_htmlForPlayer
  let c1 be new Card("3", Suit.clubs, false)
  let c2 be new Card("K", Suit.spades, true)
  let p be (new HumanPlayer("charlie", 10)).withCards([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'>&clubs;</div><div class='a'>&clubs;</div><div class='b'>&clubs;</div><div class='c'>&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
    assign 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)
    assign 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
        assign grid to grid + htmlForSpot(location, rank)
      else
        assign grid to grid + htmlForSpot(location, symbol)
      end if
    end for
    assign 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'>&clubs;</div><div class='a'>&clubs;</div><div class='b'>&clubs;</div><div class='c'>&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", "&hearts;") is "<div class='c'>&hearts;</div>"
  assert htmlForSpot("u", "10") is "<div class='u'>10</div>"
end test

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

  property dealer as Dealer

  property players as List<of Player>

  property message as String

  function withPlayers(p as List<of Player>) returns Game
    let copyOfThis be copy(this)
    assign copyOfThis.players to p
    return copyOfThis
  end function

  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
      call player.determineOutcomeAndUpdatePoints(this.dealer)
    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)
    assign this.message to message
  end procedure

  function toString() returns String
    return "a Game"
  end function

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)
    assign this.rank to rank
    assign this.suit to suit
    assign this.faceDown to facedown
  end constructor

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

  procedure turnFaceDown()
    assign this.faceDown to true
  end procedure

  function toString() returns String
    return $"{this.rank}{symbolForSuit(this.suit)}"
  end function

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
      assign this.hasTurn to true
    end if
  end procedure

  procedure determineOutcomeAndUpdatePoints(dealer as Dealer)
    variable playerOutcome set to determinePlayerOutcome(dealer, this)
    if playerOutcome is Outcome.winDouble then
      call this.changePointsBy(2)
      call dealer.changePointsBy(-2)
    elif playerOutcome is Outcome.win then
      call this.changePointsBy(1)
      call dealer.changePointsBy(-1)
    elif playerOutcome is Outcome.lose then
      call this.changePointsBy(-1)
      call dealer.changePointsBy(1)
    end if
  end procedure

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

  procedure stand()
    assign this.status to Status.standing
    assign 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 this.addAce()
    else
      assign this.handTotal to this.handTotal + rankValue()[newCard.rank]
    end if
    call this.evaluateStatus(newCard)
  end procedure

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

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

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

  abstract procedure newHand()

  procedure newHandHelper()
    assign this.hasTurn to false
    assign this.softAce to false
    assign this.cards to new List<of Card>()
    assign this.handTotal to 0
    assign this.status to Status.active
    call this.draw()
    call this.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
      assign msg to msg + " - PLAYING"
    elif status is Status.standing then
      assign msg to msg + " - STANDING"
    elif status is Status.blackjack then
      assign msg to msg + " - BLACKJACK"
    elif status is Status.bust then
      assign 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)
    assign this.name to "Dealer"
    assign this.points to startingPoints
    assign this.cards to new List<of Card>()
    assign this.faceCard to new Card("2", Suit.clubs, true)
  end constructor

  property faceCard as Card

  property hasPlayed as Boolean

  function withStatus(status as Status) returns Dealer
    let copyOfThis be copy(this)
    assign copyOfThis.status to status
    return copyOfThis
  end function

  function withHandTotal(ht as Int) returns Dealer
    let copyOfThis be copy(this)
    assign copyOfThis.handTotal to ht
    return copyOfThis
  end function

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

  procedure newHand()
    assign this.hasPlayed to false
    call this.newHandHelper()
    assign this.faceCard to 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 this.draw()
    else
      call this.stand()
    end if
  end procedure

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

  function toString() returns String
    return "the Dealer"
  end function

end class

class HumanPlayer inherits Player
  constructor(name as String, startingPoints as Int)
    assign this.name to name
    assign this.points to startingPoints
    assign this.cards to new List<of Card>()
  end constructor

  function withStatus(status as Status) returns HumanPlayer
    let copyOfThis be copy(this)
    assign copyOfThis.status to status
    return copyOfThis
  end function

  function withHandTotal(ht as Int) returns HumanPlayer
    let copyOfThis be copy(this)
    assign copyOfThis.handTotal to ht
    return copyOfThis
  end function

  function withCards(c as List<of Card>) returns HumanPlayer
    let copyOfThis be copy(this)
    assign copyOfThis.cards to c
    return copyOfThis
  end function

  procedure newHand()
    call this.newHandHelper()
  end procedure

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

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

  function toString() returns String
    return $"Player: {this.name}"
  end function

end class

enum Choice 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 dc set to [Suit.clubs:"&clubs;", Suit.diamonds:"&diams;", Suit.hearts:"&hearts;", Suit.spades:"&spades;"]
  return dc[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 dc 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 dc[rank]
end function

function colourForSuit(suit as Suit) returns String
  variable dc set to [Suit.clubs:"black", Suit.diamonds:"red", Suit.hearts:"red", Suit.spades:"black"]
  return dc[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}    .suit {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);}"
