packages feed

intricacy-0.6.2: Physics.hs

-- This file is part of Intricacy
-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of version 3 of the GNU General Public License as
-- published by the Free Software Foundation, or any later version.
--
-- You should have received a copy of the GNU General Public License
-- along with this program.  If not, see http://www.gnu.org/licenses/.

module Physics where

import qualified Data.Set as Set
import Data.Set (Set)
import qualified Data.Vector as Vector
import Control.Monad.State
import Control.Monad.Writer
import Data.Foldable (foldrM)
import Data.List
import Data.Vector (Vector, (!), (//))

import Hex
import Util
import GameState
import GameStateTypes

-- | a list of forces to try in order:
newtype ForceChoice = ForceChoice {getForceChoice :: [Force]}

type ForceChoices = Vector.Vector ForceChoice

forceIdx :: Force -> PieceIdx
forceIdx force = case force of (Push idx _) -> idx
			       (Torque idx _) -> idx

isPush,isTorque,forceIsNull :: Force -> Bool
isPush (Push _ _) = True
isPush _ = False
isTorque = not . isPush
forceIsNull (Push _ dir) = dir == zero
forceIsNull (Torque _ dir) = dir == 0

getForcedpp :: GameState -> Force -> PlacedPiece
getForcedpp s f = getpp s (forceIdx f)

-- |PlayerMove: if not NullPM, the direction should be non-zero
data PlayerMove = NullPM | HookPush HexDir | HookTorque TorqueDir | WrenchPush HexDir
    deriving (Eq, Ord, Show, Read)

toolForces :: GameState -> PlayerMove -> ForceChoices
toolForces st pm = Vector.fromList $
    [ ForceChoice (wmom++wmove)
    | (widx, PlacedPiece _ (Wrench mom)) <- epps
    , let wmom = if mom == zero then [] else [Push widx mom]
    , let wmove = case pm of {WrenchPush v -> [Push widx v]; _ -> []}
    , not $ null (wmom++wmove)
    ] ++ case pm of
	HookTorque ht -> [ ForceChoice [Torque hidx ht] | hidx <- hidxs ]
	HookPush hp -> [ ForceChoice [Push hidx hp] | hidx <- hidxs ]
	_ -> []
    where
	epps = enumVec $ placedPieces st
	hidxs = [ hidx | (hidx, PlacedPiece _ (Hook _ _)) <- epps ]

-- |Dominance: a propagand of a source force F can not block any progagand of
-- a source force which dominates F.
-- F dominates F' iff F and F' are spring forces, and the end of F is an
-- ancestor of the root of F'.
-- Note that by acyclicity of the connection digraph, domination is
-- antisymmetric.
type Dominance = Int -> Int -> Bool

envForces :: GameState -> (ForceChoices, Dominance)
envForces st@(GameState _ conns) =
    let rootedForces :: Vector (Maybe PieceIdx, Force)
	rootedForces = Vector.fromList [ (Just rootIdx, Push endIdx dir)
	    | c@(Connection (rootIdx,_) (endIdx,_) (Spring outDir natLen)) <- conns
	    , let curLen = connectionLength st c
	    , natLen /= curLen
	    , let dir = if natLen > curLen then outDir else neg outDir ]

    in ( Vector.map (ForceChoice . replicate 1 . snd) rootedForces,
	\f1 f2 -> Just True == do
		rootIdx <- fst $ rootedForces!f2
		return $ connGraphPathExists st (forceIdx.snd $ rootedForces!f1) rootIdx )


setTools :: PlayerMove -> GameState -> GameState
setTools pm st =
    let hf = case pm of
	    HookTorque dir -> TorqueHF dir
	    HookPush v -> PushHF v
	    _ -> NullHF
    in adjustPieces (\p -> case p of
		Hook arm _ -> Hook arm hf
		_ -> p) st

physicsTick :: PlayerMove -> GameState -> Writer [Alert] GameState
physicsTick pm st =
    let tfs = toolForces st pm
	(efs, dominates) = envForces st
    in do
	st' <- resolveForces tfs Vector.empty (\_ _->False) $ setTools pm st
	tell $ [AlertIntermediateState st']
	resolveForces Vector.empty efs dominates $ setTools NullPM st'

stepPhysics :: GameState -> GameState
stepPhysics = fst.runWriter.physicsTick NullPM

type Source = Int
data SourcedForce = SForce Source Force Bool Bool
    deriving (Eq, Ord, Show)
resolveForces :: ForceChoices -> ForceChoices -> Dominance -> GameState -> Writer [Alert] GameState
resolveForces plForces eForces eDominates st =
    let pln = Vector.length plForces
	dominates i j = case map (< pln) [i,j] of
		[True,False] -> True
		[False,False] -> eDominates (i-pln) (j-pln)
		_ -> False
	initGrps = fmap (propagate st True) plForces Vector.++
		fmap (propagate st False) eForces
	blockInconsistent :: Int -> Int -> StateT (Vector (Writer Any [Force])) (Writer [Alert]) ()
	blockInconsistent i j = do
	    grps <- mapM gets [(!i),(!j)]
	    blocks <- lift $ checkInconsistent i j $ map (fst.runWriter) grps
	    modify $ Vector.imap (\k -> if k `elem` blocks then (tell (Any True) >>) else id)
	checkInconsistent :: Int -> Int -> [[Force]] -> Writer [Alert] [Int]
	checkInconsistent i j fss =
	    let st' = foldr applyForce st $ nub $ concat fss
		(inconsistencies,cols) = runWriter $ sequence
		    [ tell cols >> return [f,f']
		    | [f,f'] <- sequence fss
		    , (True,cols) <- [
			    if forceIdx f == forceIdx f'
			    then (f /= f',[])
			    else let cols = collisions st' (forceIdx f) (forceIdx f')
				in (not $ null cols, cols) ]]
	    in do
		tell $ map AlertBlockingForce $ concat inconsistencies
		tell $ map AlertCollision cols
		return $ if null inconsistencies then []
		    else if i==j then [i]
		    else if dominates i j then [j]
		    else if dominates j i then [i]
		    else [i,j]

	stopWrench idx = setPiece idx (Wrench zero)
	stopBlockedWrenches blocked unblocked st' = foldr stopWrench st' $
            forcedWrenches blocked \\ forcedWrenches unblocked
            where forcedWrenches fs = [ forceIdx f
                    | f <- fs, isWrench.placedPiece $ getForcedpp st' f ]
	divertedWrenches fs = [ idx
	    | Push idx dir <- fs
	    , Wrench mom <- [placedPiece $ getpp st idx]
	    , mom `notElem` [zero,dir] ]
    in do
	let unresisted = [ s | (s, (_, Any False)) <- enumVec $ fmap runWriter initGrps ]

	-- check for inconsistencies within, and between pairs of, forcegroups
	grps <- sequence [ blockInconsistent i j
			| [i,j] <- sequence [unresisted,unresisted]
			, i <= j ]
		    `execStateT` initGrps

	let [blocked, unblocked] = map (nub.concat.(map (fst.runWriter)).Vector.toList) $
		(\(x,y) -> [x,y]) $ Vector.partition (getAny.snd.runWriter) grps
	tell $ map AlertBlockedForce blocked
	tell $ map AlertAppliedForce unblocked
	tell $ map AlertDivertedWrench $ divertedWrenches unblocked
	return $ stopBlockedWrenches blocked unblocked $ foldr applyForce st unblocked

resolveSinglePlForce :: Force -> GameState -> Writer [Alert] GameState
resolveSinglePlForce force st = resolveForces
    (Vector.singleton (ForceChoice [force])) Vector.empty
    (\_ _->False) st

applyForce :: Force -> GameState -> GameState
applyForce f s =
    let idx = forceIdx f
	pp' = applyForceTo (getpp s idx) f
	pp'' = case (placedPiece pp',f) of
		   ( Wrench _ , Push _ dir ) -> pp' {placedPiece = Wrench dir}
		   _ -> pp'
	in
	s { placedPieces = (placedPieces s) // [(idx, pp'')] }

collisionsWithForce :: GameState -> Force -> PieceIdx -> [HexPos]
collisionsWithForce st (Push idx dir) idx' =
    intersect (map (dir+^) $ footprintAtIgnoring st idx idx') (footprintAtIgnoring st idx' idx)
collisionsWithForce st force idx' =
    collisions (applyForce force st) (forceIdx force) idx'

applyForceTo :: PlacedPiece -> Force -> PlacedPiece
applyForceTo (PlacedPiece pos piece) (Push _ dir) =
    PlacedPiece (dir +^ pos) piece
applyForceTo (PlacedPiece pos (Pivot arms)) (Torque _ dir) =
    PlacedPiece pos (Pivot $ map (rotate dir) arms)
applyForceTo (PlacedPiece pos (Hook arm hf)) (Torque _ dir) =
    PlacedPiece pos (Hook (rotate dir arm) hf)
applyForceTo pp _ = pp

-- A force on a piece which resists it is immediately blocked
pieceResists :: GameState -> Force -> Bool
pieceResists st force =
    let idx = forceIdx force
	PlacedPiece _ piece = getpp st idx
        springs = springsEndAtIdx st idx
	fixed = case piece of
		     (Pivot _) -> isPush force
		     (Block _) -> null springs
		     (Wrench mom) -> case force of
			    Push _ v -> v /= mom
			    _ -> True
		     (Hook _ hf) -> case force of
			    Push _ v -> hf /= PushHF v
			    Torque _ dir -> hf /= TorqueHF dir
		     _ -> False
    in fixed

-- |transmittedForce: convert pushes into torques as appropriate
transmittedForce :: GameState -> Source -> HexPos -> HexDir -> Force
transmittedForce st idx cpos dir =
    let pp@(PlacedPiece _ piece) = getpp st idx
	rpos = cpos -^ placedPos pp
	armPush = case
		(dir `hexDot` ((rotate 1 rpos) -^ rpos)) `compare`
		(dir `hexDot` ((rotate (-1) rpos) -^ rpos)) of
	    GT -> Torque idx 1
	    LT -> Torque idx $ -1
	    EQ -> Push idx dir
    in case piece of
	    Pivot _ -> armPush
	    Hook _ (TorqueHF _) -> armPush
	    _ -> (Push idx dir)

-- |propagateForce: return forces a force causes via bumps and fully
-- compressed/extended springs
propagateForce :: GameState -> Bool -> Force -> [ForceChoice]
propagateForce st@(GameState _ conns) isPlSource force =
    bumps ++ springTransmissions
    where
	idx = forceIdx force
	bumps = [ ForceChoice $ map (transmittedForce st idx' cpos) dirs |
	    idx' <- ppidxs st
	    , idx' /= idx
	    , cpos <- collisionsWithForce st force idx'
	    , let dirs = case force of
		    Push _ dir -> [dir]
		    Torque _ dir -> [push,claw]
			where push = arm -^ rotate (-dir) arm
			      claw = rotate dir arm -^ arm
			      arm = cpos -^ placedPos (getpp st idx) ]
	springTransmissions =
	    case force of
		 Push _ dir -> [ ForceChoice [Push idx' dir] |
			c@(Connection (ridx,_) (eidx,_) (Spring sdir _)) <- conns
			, let root = idx == ridx
			, let end = idx == eidx
			, root || end
			, let idx' = if root then eidx else ridx
			, let pull = (root && dir == neg sdir) || (end && dir == sdir)
			, let push = (root && dir == sdir) || (end && dir == neg sdir)
			, (push && if isPlSource then springFullyCompressed st c else not $ springExtended st c) ||
			    (pull && if isPlSource then springFullyExtended st c else not $ springCompressed st c) ||
			    (not push && not pull) ]
		 _ -> []

-- |propagate: find forcegroup generated by a forcechoice, and note if the
-- group is blocked due to resistance. If there are multiple forces in a
-- forcechoice and the first results in a block due to resistance, try the
-- next instead.
propagate :: GameState -> Bool -> ForceChoice -> Writer Any [Force]
propagate st isPlSource fch = Set.toList `liftM` propagate' isPlSource Set.empty fch where
    propagate' isPlForce ps (ForceChoice (f:backups)) =
	if f `Set.member` ps then return ps
	else
	    let (ps', failed) = if pieceResists st f && (not isPlForce)
		    then (ps, Any True)
		    else runWriter $ foldrM
			    (flip $ propagate' False)
			    (f `Set.insert` ps)
			    $ propagateForce st isPlSource f
	    in if getAny failed
		then if null backups
		    then tell (Any True) >> return ps'
		    else propagate' isPlForce ps $ ForceChoice backups
		else return ps'
    propagate' _ _ (ForceChoice []) = error "null ForceChoice"