waterdeep/src/Waterdeep/Logic.hs

294 lines
10 KiB
Haskell

{-# LANGUAGE GADTs #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
module Waterdeep.Logic
( newGame
, waterdeepGame
) where
import Control.Applicative
import Control.Arrow ((&&&))
import Control.Lens
import Control.Monad
import Control.Monad.Random
import Control.Monad.State
import Data.List
import Data.Maybe
import Data.Monoid
import System.Random.Shuffle
import Waterdeep.Types
import Waterdeep.Monad
import qualified Data.IntMap as IM
import qualified Data.Map as M
newGame :: [(String,Faction,Lord)]
-> [Quest]
-> [IntrigueCard]
-> [Building]
-> StdGen
-> WaterdeepState
newGame players quests intrigues buildings rndgen
| length players < 2 || length players > 5 =
error "This game requires 2-5 players."
| otherwise =
WaterdeepState
{ _gamePlayers = IM.fromAscList playerStates
, _gameFirstPlayer = 1
, _gameCurrentRound = 0
, _gameCurrentPlayer = noPlayerID
, _gameConsecutivePasses = 0
, _gameQuestDeck = []
, _gameQuestDiscard = quests
, _gameCliffwatchInn = []
, _gameIntrigueDeck = []
, _gameIntrigueDiscard = intrigues
, _gameBuildingDeck = []
, _gameBuildingDiscard = buildings
, _gameBasicBuildings = map (newBuildingState noPlayerID) basicBuildings
, _gameBuildersHall = []
, _gameAdvancedBuildings = []
, _gameWaterdeepHarbor = ([], [], [])
, _gameStdGen = rndgen
}
where
playerStates = [ (i, newPlayerState i p) | (i, p) <- zip [1..] players ]
newPlayerState :: PlayerID -> (String, Faction, Lord) -> PlayerState
newPlayerState i (name, faction, lord) =
PlayerState
{ _playerNumber = i
, _playerName = name
, _playerFaction = faction
, _playerLord = lord
, _playerScore = 0
, _playerTavern = M.empty
, _playerIncompleteQuests = []
, _playerCompletedQuests = []
, _playerActivePlotQuests = []
, _playerUsedPlotQuests = []
, _playerIntrigueCards = []
, _playerAgentsInPool = 0
, _playerHasLieutenant = False
, _playerHasAmbassador = False
}
newBuildingState :: PlayerID -> Building -> BuildingState
newBuildingState p b =
BuildingState
{ _building = b
, _buildingOwner = p
, _buildingAgents = []
, _buildingAccumulation = 0
}
basicBuildings :: [Building]
basicBuildings = []
waterdeepGame :: Waterdeep [PlayerID]
waterdeepGame = do
ps <- sort . IM.keys <$> use gamePlayers
restockBuildersHall
restockCliffwatchInn
forM_ ps $ \p -> do
replicateM_ 2 $ performAction p DrawQuest
replicateM_ 2 $ performAction p DrawIntrigue
replicateM_ (3 + p) $ performAction p $ TakeResource [Gold]
forM_ [1..8] $ \round -> do
beginRound round
fix $ \loop -> do
p <- use gameCurrentPlayer
success <- filterChoices $ performAction p AssignAgent
case success of
True -> do
filterChoices $ performAction p CompleteQuest
gameCurrentPlayer .= (p `mod` length ps) + 1
gameConsecutivePasses .= 0
loop
False -> do
passes <- gameConsecutivePasses <+= 1
when (passes < length ps) loop
performAction 1 $ TakeResource [Cleric, Fighter, Rogue, Wizard, Gold]
filterChoices $ performAction 1 $
ReturnResource [Wizard]
<> ReturnResource [Cleric, Fighter, Gold]
<> ReturnResource [Cleric, Fighter]
<> ReturnResource [Cleric]
-- performAction 2 $ ChooseQuest
scoreFinalPoints
notifyState
determineWinners
beginRound :: Int -> Waterdeep ()
beginRound round = do
gameCurrentRound .= round
gameCurrentPlayer <~ use gameFirstPlayer
gameBasicBuildings . traverse . buildingAgents .= []
gameAdvancedBuildings . traverse . buildingAgents .= []
gameBuildersHall . traverse . buildingAgents .= []
gameBuildersHall . traverse . buildingAccumulation += 1
players <- IM.size <$> use gamePlayers
forM_ [1..players] $ \p -> do
qs <- gamePlayer p . playerUsedPlotQuests <<.= []
gamePlayer p . playerActivePlotQuests %= (++ qs)
gamePlayer p . playerAgentsInPool .= initialAgents players round
notifyState
return ()
initialAgents :: Int -> Int -> Int
initialAgents players round =
if round >= 5 then startingAgents + 1 else startingAgents
where startingAgents = 6 - players
data ContWD a where
Done :: (a, WaterdeepState) -> ContWD a
Cont :: WaterdeepPrompt b -> (b -> ContWD a) -> ContWD a
-- |Permit only choices which lead to a True result.
-- Returns False if and only if no such choice exists.
-- If result is False, state remains unchanged.
filterChoices :: Waterdeep Bool -> Waterdeep Bool
filterChoices m = do
mw' <- filteredChoice =<< (runWaterdeepC Done Cont m <$> get)
case mw' of
Just w' -> put w' >> return True
Nothing -> return False
where
tryChoice :: ContWD Bool -> Bool
tryChoice (Done a) = fst a
tryChoice (Cont (NotifyState w) cont) = tryChoice $ cont ()
tryChoice (Cont (SolicitChoice w p t cs) cont) =
or $ map (tryChoice . cont . snd) cs
filteredChoice :: ContWD Bool -> Waterdeep (Maybe WaterdeepState)
filteredChoice (Done (a,w)) = if a then return (Just w) else return Nothing
filteredChoice (Cont (NotifyState w) cont) = filteredChoice $ cont ()
filteredChoice (Cont (SolicitChoice w p t cs) cont) = do
let cs' = filter (tryChoice . cont . snd) cs
if null cs'
then return Nothing
else filteredChoice =<< (cont <$> (put w >> solicitChoice p t cs'))
performAction :: PlayerID -> GameAction -> Waterdeep Bool
performAction p a = case a of
NoAction -> return True
ScorePoints n -> do
gamePlayer p . playerScore += n
return True
TakeResource rs -> do
r <- solicitChoice p "Take one item:" $ map (show &&& id) rs
gamePlayer p . playerTavern %= flip (M.unionWith (+)) (M.fromList [(r, 1)])
return True
ReturnResource rs -> do
tavern <- use $ gamePlayer p . playerTavern
let rs' = filter ((> 0) . maybe 0 id . flip M.lookup tavern) rs
if null rs'
then return False
else do
r <- solicitChoice p "Return one item:" $ map (show &&& id) rs'
let removeOne x = if x > 1 then Just (x-1) else Nothing
gamePlayer p . playerTavern %= M.update removeOne r
return True
ChooseQuest -> do
qs <- use gameCliffwatchInn
if null qs
then return False
else do
let names = qs ^.. traverse . questTitle
q <- solicitChoice p "Please choose a quest:" $ zip names qs
gameCliffwatchInn %= (\\ [q])
gamePlayer p . playerIncompleteQuests %= (++ [q])
restockCliffwatchInn
return True
DrawQuest -> do
mq <- drawQuest
case mq of
Nothing -> return False
Just q -> do
gamePlayer p . playerIncompleteQuests %= (++[q])
return True
DrawIntrigue -> do
mi <- drawIntrigue
case mi of
Nothing -> return False
Just i -> do
gamePlayer p . playerIntrigueCards %= (++[i])
return True
a1 `Then` a2 -> do
(&&) <$> performAction p a1 <*> performAction p a2
_ -> return False
scoreFinalPoints :: Waterdeep ()
scoreFinalPoints = do
ps <- toListOf traverse <$> use gamePlayers
forM_ ps $ \p -> do
let rs = p ^. playerTavern . to M.toAscList
forM_ rs $ \(r, n) -> do
let pts = case r of { Gold -> n `div` 2; _ -> n; }
gamePlayer (p ^. playerNumber) . playerScore += pts
case p ^. playerLord . lordBonus of
QuestBonus types pts -> do
let (cqs, apqs, upqs) = (p ^. playerCompletedQuests, p ^. playerActivePlotQuests, p ^. playerUsedPlotQuests)
let matches = length $ filter ((`elem` types) . (view questType)) (cqs ++ apqs ++ upqs)
gamePlayer (p ^. playerNumber) . playerScore += matches * pts
BuildingBonus pts -> do
ownedBuildings <- length <$> filter ((== (p ^. playerNumber)) . (view buildingOwner)) <$> use gameAdvancedBuildings
gamePlayer (p ^. playerNumber) . playerScore += ownedBuildings * pts
determineWinners :: Waterdeep [PlayerID]
determineWinners = do
-- TODO: Implement tie-breaker(s)
let compareScores a b = (b^.playerScore) `compare` (a^.playerScore)
ps <- sortBy compareScores <$> toListOf traverse <$> use gamePlayers
let bestScore = (head ps) ^. playerScore
return $ map (^.playerNumber) $ takeWhile (\x -> (x^.playerScore) == bestScore) ps
shufflePiles :: Lens WaterdeepState WaterdeepState [a] [a]
-> Lens WaterdeepState WaterdeepState [a] [a]
-> Waterdeep ()
shufflePiles deck discard = do
xs <- (++) <$> use discard <*> use deck
xs' <- shuffleM xs
deck .= xs'
discard .= []
return ()
shuffleQuests = shufflePiles gameQuestDeck gameQuestDiscard
shuffleIntrigues = shufflePiles gameIntrigueDeck gameIntrigueDiscard
shuffleBuildings = shufflePiles gameBuildingDeck gameBuildingDiscard
draw :: Lens WaterdeepState WaterdeepState [a] [a]
-> Lens WaterdeepState WaterdeepState [a] [a]
-> Waterdeep (Maybe a)
draw deck discard = do
out <- null <$> use deck
when out $ shufflePiles deck discard
listToMaybe <$> (deck %%= splitAt 1)
drawQuest = draw gameQuestDeck gameQuestDiscard
drawIntrigue = draw gameIntrigueDeck gameIntrigueDiscard
drawBuilding = draw gameBuildingDeck gameBuildingDiscard
restockCliffwatchInn :: Waterdeep ()
restockCliffwatchInn = do
num <- length <$> use gameCliffwatchInn
when (num < 4) $ do
mq <- drawQuest
case mq of
Nothing -> return ()
Just q -> do
gameCliffwatchInn %= (++ [q])
restockCliffwatchInn
restockBuildersHall :: Waterdeep ()
restockBuildersHall = do
num <- length <$> use gameBuildersHall
when (num < 3) $ do
mb <- drawBuilding
case mb of
Nothing -> return ()
Just b -> do
gameBuildersHall %= (++ [newBuildingState noPlayerID b])
restockBuildersHall