waterdeep/src/Waterdeep/Logic.hs

198 lines
7.5 KiB
Haskell

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
module Waterdeep.Logic
( newGame
, waterdeepGame
) where
import Control.Applicative
import Control.Arrow ((&&&))
import Control.Monad
import Control.Monad.Random
import Control.Monad.State
import Control.Monad.Trans.Writer
import Data.List
import Data.Maybe
import Data.Monoid
import Data.Traversable (traverse)
import Lens.Family2
import Lens.Family2.State
import Lens.Family2.Stock
import System.Random.Shuffle
import Text.Printf
import Waterdeep.Actions
import Waterdeep.Types
import Waterdeep.Util
import qualified Data.IntMap as IM
import qualified Data.Map as M
import qualified Data.MultiSet as MS
newGame :: [(String,Faction,Lord)]
-> [Quest]
-> [IntrigueCard]
-> [Building]
-> StdGen
-> WaterdeepState
newGame players quests intrigues buildings rndgen =
WaterdeepState
{ _gameNumberOfPlayers = length players
, _gamePlayerStates = IM.fromAscList playerStates
, _gameFirstPlayer = 1
, _gameCurrentRound = 0
, _gameCurrentPlayer = 1
, _gameActivePlayer = 1
, _gameConsecutivePasses = 0
, _gameQuestDeck = []
, _gameQuestDiscard = quests
, _gameCliffwatchInn = []
, _gameIntrigueDeck = []
, _gameIntrigueDiscard = intrigues
, _gameBuildingDeck = []
, _gameBuildingDiscard = buildings
, _gameBuildersHall = IM.empty
, _gameBuildings = IM.fromAscList buildingStates
, _gameWaterdeepHarbor = IM.fromAscList [(1,[]),(2,[]),(3,[])]
, _gameStdGen = rndgen
}
where
playerStates = zipWith (\i p -> (i, newPlayerState i p)) [1..] players
buildingStates = zipWith (\i b -> (i, newBuildingState np b)) [1..] basicBuildings
np = noPlayerID
basicBuildings :: [Building]
basicBuildings =
[ basicBuilding "Aurora's Realms Shop" (takeResources 4 [Gold])
, basicBuilding "Blackstaff Tower" (takeResources 1 [Wizard])
, basicBuilding "Builder's Hall" buyBuilding
, basicBuilding "Castle Waterdeep" (becomeFirstPlayer >> drawIntrigue)
, basicBuilding "Field of Triumph" (takeResources 2 [Fighter])
, basicBuilding "The Grinning Lion Tavern" (takeResources 2 [Rogue])
, basicBuilding "The Plinth" (takeResources 1 [Cleric])
, basicBuilding "Cliffwatch Inn (2 Gold)" (chooseQuest >> takeResources 2 [Gold])
, basicBuilding "Cliffwatch Inn (Intrigue)" (chooseQuest >> drawIntrigue)
, basicBuilding "Cliffwatch Inn (Reset)" (replaceQuests >> chooseQuest)
]
basicBuilding :: String -> GameAction -> Building
basicBuilding title action =
Building
{ _buildingCost = 0
, _buildingTitle = title
, _buildingAction = action
, _buildingOwnerAction = noAction
, _buildingAccumType = NoAccumulation
}
waterdeepGame :: Waterdeep [PlayerID]
waterdeepGame = do
np <- use gameNumberOfPlayers
guard (np >= 2 && np <= 5)
restockBuildersHall
restockCliffwatchInn
forM_ [1..np] $ \p -> withActivePlayer p $ do
replicateM_ 2 $ drawQuest
replicateM_ 2 $ drawIntrigue
takeResources (3 + p) [Gold]
forM_ [1..8] $ \round -> do
beginRound round
-- TODO: Assign ambassador (if in play)
fix $ \loop -> do
assign gameActivePlayer =<< use gameCurrentPlayer
filterChoices assignAgent >>= \case
Just () -> do
filterChoices $ join $ solicitChoice "Complete a quest?" $
[ ("Yes", completeQuest)
, ("No", return ())
]
gameConsecutivePasses .= 0
advanceToNextPlayer
loop
Nothing -> do
gameConsecutivePasses += 1
passes <- use gameConsecutivePasses
when (passes < np) $ do
advanceToNextPlayer
loop
broadcast $ "Game over!"
scoreFinalPoints
determineWinners
advanceToNextPlayer :: Waterdeep ()
advanceToNextPlayer = do
p' <- nextPlayerID <$> use gameCurrentPlayer <*> get
gameCurrentPlayer .= p'
beginRound :: Int -> Waterdeep ()
beginRound round = do
broadcast $ "Starting round " ++ show round ++ "."
gameCurrentRound .= round
assign gameCurrentPlayer =<< use gameFirstPlayer
assign gameActivePlayer =<< use gameFirstPlayer
gameBuildings . traverse . buildingAgents .= []
gameBuildersHall . traverse . buildingAgents .= []
gameBuildersHall . traverse . buildingAccumulation += 1
gameWaterdeepHarbor . traverse .= []
np <- use gameNumberOfPlayers
let agents = initialAgents np round
forM_ [1..np] $ \p -> withActivePlayer p $ do
qs <- use $ activePlayerState . playerUsedPlotQuests
activePlayerState . playerUsedPlotQuests .= []
activePlayerState . playerActivePlotQuests <>= qs
activePlayerState . playerAgentsInPool .= agents
activePlayerState . playerCanUseOpSpace .= False
use (activePlayerState . playerHasLieutenant) >>= \case
True -> activePlayerState . playerAgentsInPool += 1 >> return ()
False -> return ()
--TODO: Start-of-round plot actions
scoreFinalPoints :: Waterdeep ()
scoreFinalPoints = do
np <- use gameNumberOfPlayers
forM_ [1..np] $ \p -> withActivePlayer p $ do
rs <- MS.toAscOccurList <$> use (activePlayerState . playerTavern)
name <- use activePlayerName
forM_ rs $ \(r, n) -> do
let pts = case r of { Gold -> n `div` 2; _ -> n; }
activePlayerState . playerScore += pts
broadcast $ printf "%s scored %d points for having %d %ss." name pts n (show r)
use (activePlayerState . playerLord . lordBonus) >>= \case
QuestBonus types pts -> do
quests <- concat <$> sequence
[ use $ activePlayerState . playerCompletedQuests
, use $ activePlayerState . playerActivePlotQuests
, use $ activePlayerState . playerUsedPlotQuests
]
let matches = length . filter ((`elem` types) . (view questType)) $ quests
activePlayerState . playerScore += matches * pts
when (matches > 0) $ broadcast $
printf "%s scored %d points for completing %d %s and/or %s quests."
name (matches * pts) matches (show (types !! 0)) (show (types !! 1))
BuildingBonus pts -> do
owned <- countOf (traverse.buildingOwner) (== p) <$> use gameBuildings
activePlayerState . playerScore += owned * pts
when (owned > 0) $ broadcast $
printf "%s scored %d points for controlling %d buildings."
name (owned * pts) owned
determineWinners :: Waterdeep [PlayerID]
determineWinners = do
playerStates <- toListOf traverse <$> use gamePlayerStates
let bestScore = maximum $ map (view playerScore) playerStates
let winners1 = filter ((== bestScore) . view playerScore) playerStates
let bestGold = maximum $ map playerGold winners1
let winners2 = filter ((== bestGold) . playerGold) winners1
broadcast $ "Winners: " ++ (intercalate ", " (winners2 ^.. traverse . playerName))
return (winners2 ^.. traverse . playerNumber)
where
playerGold = MS.occur Gold . view playerTavern
initialAgents :: Int -> Int -> Int
initialAgents players round =
if round >= 5 then startingAgents + 1 else startingAgents
where startingAgents = case players of { 2 -> 4; 3 -> 3; _ -> 2 }