Skip to content

Commit

Permalink
good combat algorithm (going to repel it next commit, and build simpl…
Browse files Browse the repository at this point in the history
…er ones to test).
  • Loading branch information
flober committed Aug 2, 2024
1 parent 26c7a90 commit 2077f1d
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 21 deletions.
83 changes: 78 additions & 5 deletions src/Combat.hs
Original file line number Diff line number Diff line change
@@ -1,11 +1,84 @@
-- Combat phase simulation
{-# LANGUAGE OverloadedRecordDot #-}

module Combat where

import Control.Monad.Random
import Data.List (sortOn)
import Data.Ord (Down (Down))
import Model
import Utils (selectPlayer)

type Victor = Player
type Damage = Int
-- New type for Attacker
type Attacker = Contestant

-- `fight` simulates the combat and logs every move and intermediate combat state.
fight :: Player -> Player -> GameState -> ([CombatMove], [(Board, Board)], Victor, Damage)
fight p1 p2 gs = ([], [], p1, 5)
fight :: (MonadRandom m) => Player -> Player -> GameState -> m (CombatSimulation, CombatResult, Damage)
fight p1 p2 gs = do
(sequence, finalState) <- simulateCombat ((selectPlayer p1 gs).board, (selectPlayer p2 gs).board)
let result = determineCombatResult finalState
let damage = calculateDamage result finalState
return (CombatSimulation [] sequence, result, damage)

simulateCombat :: (MonadRandom m) => (Board, Board) -> m (CombatHistory, (Board, Board))
simulateCombat state = go state []
where
go state history
| combatEnded state = return (reverse history, state)
| otherwise = do
attacker <- chooseAttacker state
newState <- performAttack attacker state
go newState (state : history)

displayInOrder :: Int -> Board -> Board
displayInOrder i b = _

Check failure on line 33 in src/Combat.hs

View workflow job for this annotation

GitHub Actions / build

• Found hole: _ :: Board

chooseAttacker :: (MonadRandom m) => (Board, Board) -> m Attacker
chooseAttacker (board1, board2)
| length board1 > length board2 = return One
| length board2 > length board1 = return Two
| otherwise = do
r <- getRandomR (0, 1) :: (MonadRandom m) => m Int
return $ if r == 0 then One else Two

performAttack :: (MonadRandom m) => Attacker -> (Board, Board) -> m (Board, Board)
performAttack attacker (board1, board2) = do
let (attackingBoard, defendingBoard) = case attacker of
One -> (board1, board2)
Two -> (board2, board1)
defenderIndex <- selectRandomDefender defendingBoard
let (newAttackingBoard, newDefendingBoard) = atk (head attackingBoard) defenderIndex (attackingBoard, defendingBoard)
let rotatedAttackingBoard = tail newAttackingBoard ++ [head newAttackingBoard]
return $ case attacker of
One -> (rotatedAttackingBoard, newDefendingBoard)
Two -> (newDefendingBoard, rotatedAttackingBoard)

selectRandomDefender :: (MonadRandom m) => Board -> m Int
selectRandomDefender board = getRandomR (0, length board - 1)

atk :: CardInstance -> Int -> (Board, Board) -> (Board, Board)
atk attacker defenderIndex (attackerBoard, defenderBoard) =
let defender = defenderBoard !! defenderIndex
newAttacker = attacker {card = (attacker.card) {health = max 0 (attacker.card.health - defender.card.attack)}}
newDefender = defender {card = (defender.card) {health = max 0 (defender.card.health - attacker.card.attack)}}
newAttackerBoard = if newAttacker.card.health > 0 then newAttacker : tail attackerBoard else tail attackerBoard
newDefenderBoard =
take defenderIndex defenderBoard
++ ([newDefender | newDefender.card.health > 0])
++ drop (defenderIndex + 1) defenderBoard
in (newAttackerBoard, newDefenderBoard)

combatEnded :: (Board, Board) -> Bool
combatEnded (board1, board2) = null board1 || null board2

determineCombatResult :: (Board, Board) -> CombatResult
determineCombatResult (board1, board2)
| not (null board1) && null board2 = Loser Two
| null board1 && not (null board2) = Loser One
| otherwise = Tie

calculateDamage :: CombatResult -> (Board, Board) -> Damage
calculateDamage result (board1, board2) =
case result of
Loser One -> sum $ map (\ci -> ci.card.cardTier) board2
Loser Two -> sum $ map (\ci -> ci.card.cardTier) board1
Tie -> 0
2 changes: 1 addition & 1 deletion src/Controller.hs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ defPlayerState =
armor = 0,
alive = True,
phase = HeroSelect,
combatSequence = ([], 0)
combatSimulation = CombatSimulation [] []
}

runGame :: IO ()
Expand Down
29 changes: 24 additions & 5 deletions src/Logic.hs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
-- -- Logic: Handles game logic, executing user commands
-- Logic: Handles recruit phase logic, executing user commands
-- TODO: Modularize out the recruit logic, since Combat.hs is already separate.
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE ParallelListComp #-}

module Logic (module Logic) where

import Card (pool)
import Combat (CombatResult (..), fight)
import Control.Monad.Random
import Data.Functor ((<&>))
import Model
Expand All @@ -30,7 +32,8 @@ isGameOver gs = gs.playerState.alive /= gs.aiState.alive

-- Performed when we first transition to a new game phase.
enter :: (MonadRandom m) => Phase -> Player -> GameState -> m GameState
enter Recruit Player gs = do -- Entering Player's recruit phase triggers AI to perform same logic
enter Recruit Player gs = do
-- Entering Player's recruit phase triggers AI to perform same logic
newPlayerState <- enter' gs.playerState
newAIState <- enter' gs.aiState
return
Expand All @@ -49,11 +52,27 @@ enter Recruit Player gs = do -- Entering Player's recruit phase triggers AI to p
frozen = False,
shop = if ps.frozen then ps.shop else newShop
}
enter Combat Player gs = do
let newPlayerState = (selectPlayer Player gs) { phase = Combat }
return gs {playerState = newPlayerState}
-- fight! And then
-- 1) provide the render phase with simulation sequence
-- 2) unleash the damage
enter Combat Player gs = do
let (sim, combatResult, dmg) = fight Player AI gs
let gs' = gs {playerState = gs.playerState {phase = Combat, combatSimulation = sim}}
case combatResult of
Tie -> return gs'
Loser loser -> return $ updatePlayer loser loserState' gs'
where
loserState = selectPlayer loser gs
(hp', armor') = dealDmg dmg (loserState.hp, loserState.armor)
loserState' = (selectPlayer loser gs) {hp = hp', armor = armor'} -- Note: Damage dealing happens before combat sequence is played
enter _ _ _ = error "Other phases should not be enterable"

dealDmg :: Int -> (Health, Armor) -> (Health, Armor)
dealDmg n (hp, armor) = (hp - hpDmg, armor - armorDmg)
where
armorDmg = min n armor
hpDmg = n - armorDmg

-- START: Utility Methods for PlayerAction Functions --
-- Determinisitc functions should only be used when the usage permits only the happy path

Expand Down
20 changes: 15 additions & 5 deletions src/Model.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ data Card = Card
baseCost :: CardCost,
attack :: Attack,
health :: Health
}
} deriving Show

data CardInstance = CardInstance
{ cardId :: UUID,
card :: Card
}
} deriving Show

type Gold = Int

Expand All @@ -51,7 +51,7 @@ type UserName = String
data Phase = HeroSelect | Recruit | Combat | EndScreen deriving (Eq)

-- For now, GameState just keeps track of the solo player and one AI.
data Player = Player | AI
data Player = Player | AI deriving (Show, Eq)

data Config = Config { maxBoardSize :: Int, maxHandSize :: Int }
data GameState = GameState
Expand All @@ -61,8 +61,18 @@ data GameState = GameState
turn :: Turn
}

data CombatSimulation = CombatSimulation {combatMoves :: [CombatMove], boardSequences :: [(Board, Board)]} deriving Show

-- TODO: The client can replay the same combat if provided the same seed
-- However, for testing purposes, it will be nice to manually write out the attack sequence
data CombatMove =
Attack Int Int -- Player1's ith minion attacks Player2's jth minion
Attack Int Int -- Player1's ith minion attacks Player2's jth minion;
deriving Show

data Contestant = One | Two deriving (Show, Eq)
data CombatResult = Loser Contestant | Tie deriving (Show, Eq)
type Damage = Int
type CombatHistory = [(Board, Board)]

data PlayerState = PlayerState
{ tier :: TavernTier,
Expand All @@ -78,7 +88,7 @@ data PlayerState = PlayerState
alive :: Bool,
rerollCost :: Gold,
phase :: Phase,
combatSequence :: ([CombatMove], Int)
combatSimulation :: CombatSimulation
}

type Index = Int
Expand Down
27 changes: 22 additions & 5 deletions src/View.hs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,30 @@ render gs p =
case (selectPlayer p gs).phase of
Recruit -> putStrLn $ renderRecruit gs p
HeroSelect -> putStrLn "heroselect todo"
Combat -> forM_ [1..10] $ \n -> do
print n
threadDelay 500000 -- 500 milliseconds $"combat todo"
Combat -> forM_ (replayCombat gs.playerState.combatSimulation) $ \s -> do
putStrLn s
threadDelay $ 500 * 1000 -- 500 milliseconds
EndScreen -> if (selectPlayer p gs).alive then putStrLn "Victory! Ending now." else putStrLn "You loss. Ending now."

replayCombat :: [CombatMove] -> [(Board, Board)] -> [String]
replayCombat _ bs = []
-- [String] contains each slice of the readily renderable combat!
-- [CombatMove] is ignored for now. But, they are required to flavor the UI
replayCombat :: CombatSimulation -> [String]
replayCombat (CombatSimulation _ bs) = map renderBoardState bs

renderBoardState :: ([CardInstance], [CardInstance]) -> String
renderBoardState (board1, board2) =
intercalate "\n" $
[ hBorder,
"|" ++ alignMid (rowWidth - 2) "Combat Simulation" ++ "|",
hBorder,
"| Player 1: " ++ alignMid maxRowContentWidth (intercalate " | " (map renderCard board1)) ++ " |",
hBorder,
"| Player 2: " ++ alignMid maxRowContentWidth (intercalate " | " (map renderCard board2)) ++ " |",
hBorder
]

renderCard :: CardInstance -> String
renderCard ci = abbrev maxCardNameDisplayLength (show ci.card.cardName) ++ "(" ++ show ci.card.attack ++ "/" ++ show ci.card.health ++ ")"

hBorder :: [Char]
hBorder = "+" ++ replicate (rowWidth - 2) '-' ++ "+"
Expand Down

0 comments on commit 2077f1d

Please sign in to comment.