# db237dfb391e87835e608d86b7c9829f8b5251211f7caeda8633aa677789cfde Elan 1.6.3 guest default_profile valid

main
  call playGame()
end main

procedure playGame()
  variable dealer set to createDealer(1000)
  variable player set to createHumanPlayer(1000)
  variable gameNo set to 0
  variable cont set to true
  while cont
    call clearPrintedText()
    call playOneRound(dealer, player)
    let outcome be determineOutcome(dealer, player)
    call updateScores(dealer, player, outcome)
    print "You {outcome}"
    print "Game: {gameNo} Dealer score: {dealer.score} Player score: {player.score}"
    let prompt be "Enter p for play again, x for exit:"
    let inp be inputStringFromOptions(prompt, ["p", "x"])
    set cont to inp isnt "x"
  end while
end procedure

procedure simulateGame()
  let startPoints be 1000
  variable dealer set to createDealer(startPoints)
  variable playerA set to createPlayerA(startPoints)
  variable playerB set to createPlayerB(startPoints)
  call playAllHands(dealer, playerA, playerB)
  print dealer
  print playerA
  print playerB
  print "Dealer has {dealer.score} points"
  print "Player A has {playerA.score} points"
  print "Player B has {playerB.score} points"
end procedure

function getActionForPlayerA(hand as Hand, dealerFaceUp as Card) returns Action
  return Action.draw
end function

function getActionForPlayerB(hand as Hand, dealerFaceUp as Card) returns Action
  return Action.stand
end function

function createDealer(startPoints as Int) returns Dealer
  return new Dealer(startPoints)
end function

function createHumanPlayer(startPoints as Int) returns HumanPlayer
  return new HumanPlayer(startPoints)
end function

function createPlayerA(startPoints as Int) returns AutomatedPlayer
  return new AutomatedPlayer("Player A", ref getActionForPlayerA, startPoints)
end function

function createPlayerB(startPoints as Int) returns AutomatedPlayer
  return new AutomatedPlayer("Player B", ref getActionForPlayerB, startPoints)
end function

procedure playAllHands(dealer as Dealer, playerA as AutomatedPlayer, playerB as AutomatedPlayer)
  let dfc be dealCard(random())
  call playerA.newHand(dfc)
  call playerA.playHandOut()
  call playerB.newHand(dfc)
  call playerB.playHandOut()
  let statusB be playerB.hand.status
  call dealer.newHand(dfc)
  call dealer.playHandOut()
  let outcomeA be determineOutcome(dealer, playerA)
  call updateScores(dealer, playerA, outcomeA)
  let outcomeB be determineOutcome(dealer, playerB)
  call updateScores(dealer, playerB, outcomeB)
end procedure

procedure playOneRound(dealer as Dealer, player as HumanPlayer)
  call player.newHand()
  call dealer.newHand(dealCard(random()))
  call displayHands(dealer, player)
  call playersTurn(dealer, player)
  call pause(2000)
  call dealersTurn(dealer, player)
end procedure

procedure dealersTurn(dealer as Dealer, player as HumanPlayer)
  call dealer.start()
  call displayHands(dealer, player)
  call pause(2000)
  while dealer.hand.status is Status.playing
    call dealer.nextMove()
    call displayHands(dealer, player)
    call pause(1000)
  end while
end procedure

procedure playersTurn(dealer as Dealer, player as HumanPlayer)
  while player.isPlaying()
    call player.nextMove()
    call displayHands(dealer, player)
  end while
end procedure

procedure displayHands(dealer as Dealer, player as HumanPlayer)
  call clearPrintedText()
  let blankLines be "<br><br><br><br><br>"
  call displayHtml("<br><br><br><br><br>{dealer.asHtml()}<br>{player.asHtml()}")
end procedure

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

function determineOutcome(dealer as Dealer, player as Player) returns Outcome
  let d be dealer.hand.status
  let dTot be dealer.hand.total
  let p be player.hand.status
  let pTot be player.hand.total
  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 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 suit
end function

interface Player
  abstract property hand as Hand

  abstract procedure changeScoreBy(amount as Int)

end interface

abstract class Automated inherits Player
  property hand as Hand

  property score as Int

  property dealerFaceCard as Card

  property active as Boolean

  abstract function getAction() returns Action

  procedure nextMove()
    if getAction() is Action.draw then
      call property.hand.draw()
    else
      call property.hand.stand()
    end if
  end procedure

  procedure playHandOut()
    call start()
    while property.hand.status is Status.playing
      call nextMove()
    end while
  end procedure

  procedure start()
    set property.active to true
  end procedure

  procedure changeScoreBy(amount as Int)
    set property.score to property.score + amount
  end procedure

end class

class Dealer inherits Automated
  constructor(startingPoints as Int)
    set property.score to startingPoints
  end constructor

  procedure newHand(faceCard as Card)
    set property.dealerFaceCard to faceCard
    set property.active to false
    set property.hand to new Hand()
    call property.hand.addCard(faceCard)
    call property.hand.draw()
  end procedure

  function getAction() returns Action
    return if property.hand.total < 17 then Action.draw else Action.stand
  end function

  function asHtml() returns String
    return if property.active then "Dealer: {property.hand.asHtml()}" else "Dealer: {property.dealerFaceCard.asHtml()} {unicode(9633)}"
  end function

end class

class HumanPlayer inherits Player
  constructor(startingPoints as Int)
    set property.score to startingPoints
  end constructor

  property score as Int

  property hand as Hand

  procedure newHand()
    set property.hand to new Hand()
    call property.hand.draw()
    call property.hand.draw()
    call property.hand.setStatus(Status.playing)
  end procedure

  function isPlaying() returns Boolean
    return property.hand.status is Status.playing
  end function

  procedure nextMove()
    let inp be inputStringFromOptions("Enter s to stand, d to draw card:", ["s", "d"])
    if inp is "d" then
      call property.hand.draw()
    else
      call property.hand.stand()
    end if
  end procedure

  procedure changeScoreBy(amount as Int)
    set property.score to property.score + amount
  end procedure

  function asHtml() returns String
    return "You: {property.hand.asHtml()}"
  end function

end class

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

  property name as String

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

  procedure newHand(faceCard as Card)
    set property.dealerFaceCard to faceCard
    set property.active to false
    set property.hand to new Hand()
    call property.hand.draw()
    call property.hand.draw()
  end procedure

  function getAction() returns Action
    return decisionFunc(property.hand, property.dealerFaceCard)
  end function

  function asHtml() returns String
    return "{property.name}: {property.hand.asHtml()}"
  end function

end class

class Hand
  property softAce as Boolean

  property cards as List<of Card>

  property count as Int

  property total as Int

  property status as Status

  function isPairOf8s() returns Boolean
    return (property.count is 2) and (property.cards[0].value() is 8) and (property.cards[1].value() is 8)
  end function

  procedure setStatus(status as Status)
    set property.status to status
  end procedure

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

  procedure draw()
    set property.status to Status.playing
    let card be dealCard(random())
    call addCard(card)
  end procedure

  procedure addCard(card as Card)
    call property.cards.append(card)
    set property.count to property.count + 1
    if card.isAce() then
      call addAce()
    else
      set property.total to property.total + card.value()
    end if
    if property.total > 21 then
      call useUpSoftAceIfPossible()
    else if card.isAce() then
      set property.softAce to true
    end if
    if (property.count is 2) and (property.total is 21) and property.softAce then
      set property.status to Status.blackjack
    else if property.total > 21 then
      set property.status to Status.bust
    else if property.total is 21 then
      set property.status to Status.standing
    end if
  end procedure

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

  private procedure useUpSoftAceIfPossible()
    if property.softAce then
      set property.total to property.total - 10
      set property.softAce to false
    else
      set property.status to Status.bust
    end if
  end procedure

  function asHtml() returns String
    variable str set to ""
    each card in property.cards
      set str to str + card.asHtml() + " "
    end each
    set str to "{str}({property.total}) {property.status}"
    return str
  end function

end class

record Card
  property suit as Int

  property rank as String

  function value() returns Int
    return rankValue[property.rank]
  end function

  function isAce() returns Boolean
    return property.rank is "A"
  end function

  function asHtml() returns String
    let blackSuit be (property.suit is 0) or (property.suit is 3)
    let colour be if blackSuit then "black" else "red"
    let tag be htmlEscChars[property.suit]
    return "{property.rank}<span style='color: {colour}'>{tag}</span>"
  end function

end record

enum Action stand, draw

enum Outcome undecided, lose, draw, win, winDouble

enum Status pending, playing, standing, blackjack, bust

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 htmlEscChars set to {"&clubs;", "&diams;", "&hearts;", "&spades;"}
