packages feed

scroll-1.20150313: Level/Border.hs

{-# LANGUAGE RankNTypes #-}

module Level.Border (border, isBoundry, addBorder, addCap, joinCap, checkBounded) where

import qualified Data.Vector as V
import Data.Vector ((!))
import qualified Data.Vector.Mutable as MV
import Control.Monad.ST
import Control.Monad
import Data.Monoid
import Data.Char
import Control.Applicative

import Types

addBorder :: Level -> V.Vector (V.Vector Char)
addBorder = padForCap . erodeLeftEdge . V.modify transform
	. V.fromList . map V.fromList 
	. trimTrailingEmptyLines
	. padSameWidth . map deTab

border :: Char
border = '|'

cap :: Char
cap = '_'

isBorder :: Char -> Bool
isBorder c = c == border

isCap :: Char -> Bool
isCap c = c == cap

isBoundry :: Char -> Bool
isBoundry c = isBorder c || isCap c

deTab :: String -> String
deTab = concatMap go
  where
	go '\t' = replicate 8 ' '
	go c = [c]

padSameWidth :: [String] -> [String]
padSameWidth l = map pad l
  where
	width = maximum (map length l)
	pad s = let w = length s
		in s ++ replicate (width - w) ' '

-- Trailing empty lines break the border generation, so remove.
trimTrailingEmptyLines :: [String] -> [String]
trimTrailingEmptyLines = reverse . dropWhile (all isSpace) . reverse

type Transform = forall s. MV.MVector s (V.Vector Char) -> ST s ()

transform :: Transform
transform v = sequence_ $ map (\a -> a v)
	[ constrict
	, outsideBorder
	, widenHalls
	, openPasses
	, erodeEdge
	]

constrict :: Transform
constrict v
	| MV.length v == 0 = return ()
	| otherwise = do
		l1 <- MV.read v 0
		-- When constricting, leave at least the center fifth free.
		-- This keeps centered headers from being constricted to
		-- much.
		let maxconstrict = V.length l1 - 1
		loop 0 maxconstrict
  where
	loop y maxconstrict
		| y >= MV.length v = return ()
		| otherwise = do
			l <- MV.read v y
			let l' = V.modify (go maxconstrict) l
			MV.write v y l'
			loop (y + 1) maxconstrict
	
	go maxconstrict lv = do
		loopy id maxconstrict lv 0
		loopy (\n -> MV.length lv - 1 - n) maxconstrict lv 0
	
	loopy f maxconstrict lv n
		| n <= maxconstrict = do
			let x = f n
			c <- MV.read lv x
			if isSpace c
				then do
					MV.write lv x '|'
					loopy f maxconstrict lv (n + 1)
				else return ()
		| otherwise = return ()

-- Constrict over-does it for blank lines. Widen them back out,
-- to be as wide as the line below them. Note that we work from
-- the bottom up, so that multiple blank lines all open as wide
-- as the text below.
--
-- If the whole level ends with a blank line or lines, they're also
-- windened into halls, based on the first non-blank line above.
widenHalls :: Transform
widenHalls v = do
	forM_ (reverse [0..MV.length v - 2]) $ \y -> do
		lv <- MV.read v y
		when (isHall lv) $ do
			below <- MV.read v (y + 1)
			MV.write v y $ hallAbove below

hallAbove :: V.Vector Char -> V.Vector Char
hallAbove = V.map (\c -> if isBorder c then c else ' ')

-- Note that lines that are somehow all border are treated as halls,
-- which is good, since otherwise there would be no way through!
isHall :: V.Vector Char -> Bool
isHall = V.all isSpace . deBorderLeft . V.reverse . deBorderLeft

deBorderLeft :: V.Vector Char -> V.Vector Char
deBorderLeft = V.dropWhile isBorder

deBorderRight :: V.Vector Char -> V.Vector Char
deBorderRight = V.reverse . deBorderLeft . V.reverse

-- Often there will be a long line, like this one.||||
-- that is followed by a shorter line.||||||||||||||||
--
-- This can happen at the end of a||||||||
-- paragraph, but also||||||||||||||||||||
-- sometimes in the middle of one.||||||||
--
-- In this case, the border goes too far in and makes the game
-- too hard, so open up a pass.
--
-- We'll only do it if the shorter line is at least 3 letters
-- shorter than the longer.
openPasses :: Transform
openPasses v = forM_ [0..MV.length v - 2] $ \y -> do
	lv <- MV.read v y
	below <- MV.read v (y + 1)
	let belowc = countborder below
	let delta = countborder lv - belowc
	when (delta >= 3) $ do
		let lv' = V.concat
			[ deBorderRight lv
			, V.replicate (delta - 1) ' '
			, V.replicate (belowc + 1) border
			]
		MV.write v y lv'
  where
	countborder = V.length . V.takeWhile isBorder . V.reverse

outsideBorder :: Transform
outsideBorder v = forM_ [0..MV.length v - 1] $ \y -> do
	lv <- MV.read v y
	let lv' = V.cons border $ V.snoc lv border
	MV.write v y lv'

erodeEdge :: Transform
erodeEdge v = do
	looplines v erodeRight
	looplines v addArt

-- inefficient
erodeLeftEdge :: V.Vector (V.Vector Char) -> V.Vector (V.Vector Char)
erodeLeftEdge = V.map (V.reverse) . V.modify erodeEdge . V.map (V.reverse) 

data Area = Area
	{ areaAbove :: V.Vector Char
	, areaHere :: V.Vector Char
	, areaBelow :: V.Vector Char
	}
	deriving (Show)

type AreaTransformer = forall s. Area -> Int -> MV.MVector s Char -> ST s ()

looplines
	:: forall s. MV.MVector s (V.Vector Char)
	-> AreaTransformer
	-> ST s ()
looplines v f = forM_ [0..MV.length v - 1] $ \y -> do
	area <- Area
		<$> saferead (pred y)
		<*> MV.read v y
		<*> saferead (succ y)
	MV.write v y $ V.modify (f area (V.length (areaHere area) - 1))
		(areaHere area)
  where
	saferead y
		| y >= 0 && y <= MV.length v - 1 = MV.read v y
		-- use a dummy, all blank line at top and bottom
		| otherwise = V.map (\_ -> ' ') <$> MV.read v 0

-- The border can be eroded away, as long as there is other border inside
-- it, both on its line, and above and below it.
--
-- Or, the lines above or below may have whitespace, which continues
-- till the end of the line.
--
--         |     |       |         |
--         || -> |       |||||| -> |
--         |     |       |||||     |||||
erodeRight :: AreaTransformer
erodeRight area x mv
	| x < 2 = return ()
	| bordering (areaHere area) && bwhite (areaAbove area) && bwhite (areaBelow area) = do
		MV.write mv x ' '
		erodeRight area (x-1) mv
	| otherwise = return ()
  where
	bordering vec = isBorder (vec ! pred x)
	bwhite vec = bordering vec || V.all isSpace (V.drop x vec)

-- After erosion is finished, go back through and find corners in
-- the border, inserting some additional ascii art.
--
--      |     |,    ||    ||
--      || -> ||    |  -> |`
addArt :: AreaTransformer
addArt area x mv
	| x < 2 = return ()
	| isBorder (areaHere area ! x) = return () 
	| isSpace (areaHere area ! pred x) = addArt area (pred x) mv
	| isBorder (areaAbove area ! x) = MV.write mv x '`'
	| isBorder (areaBelow area ! x) = MV.write mv x ','
	| otherwise = return ()

padForCap :: V.Vector (V.Vector Char) -> V.Vector (V.Vector Char)
padForCap = V.map pad
  where
	pad l = V.replicate 2 ' ' <> l <> V.replicate 2 ' '

addCap :: forall s. MV.MVector s (MV.MVector s Char) -> ST s ()
addCap v = do
	len <- MV.length <$> MV.read v 0
	let mid = replicate (len - 6) '_'
	let a ... b = a ++ mid ++ b
	let cap1 = vec $ "  _"..."_  "
	let cap2 = vec $ "=(_"..."_)="

	MV.write v 0 =<< cap1
	MV.write v 1 =<< cap2
	above <- V.freeze =<< MV.read v (end - 2)
	c <- cap1
	joinCap above c
	MV.write v (end-1) c
	MV.write v end =<< cap2
  where
	vec = V.thaw . V.fromList
	end = MV.length v - 1

-- Updates the line that joins the cap to the bottom of the scroll,
-- drawing border symbols to match the line just above.
--
--       | lorem ipsum dolor ||||||||||||||| 
--   ____|___________________|||||||||||||||_______
-- =(______________________________________________)=
--
-- Also inserts some spaces in the line, as both ascii art and to let
-- the player get into the scroll endcap area. (But, they can only
-- get there if a message is displayed.) That is how the game is won!
--
-- This needs to be called each time the scroll is rolled up more.
joinCap :: forall s. V.Vector Char -> MV.MVector s Char -> ST s ()
joinCap above v = forM_ [2..V.length above - 3] $ \x ->
	MV.write v x (joiner x)
  where
	leftborder = findborder above
	rightborder = V.length above - 1 - findborder (V.reverse above)
	findborder = V.length . V.takeWhile (not . isBorder)
	dashy x = x == leftborder + 1 || x == rightborder -1 || x `mod` 5 == 0 || pred x `mod` 5 == 0
	joiner x = 
		if isBorder (above ! x) || x == leftborder || x == rightborder
			then border
			else if dashy x || x < leftborder || x > rightborder
				then cap
				else ' '

-- Check that the specified posixition has border to the left and right,
-- and is not itself embedded in a border, and is empty.
checkBounded :: World -> Pos -> ST RealWorld Bool
checkBounded w (x,y) = do
	l <- MV.read w y
	if x >= MV.length l
		then return False
		else do
			c <- MV.read l x
			if not (isSpace c)
				then return False
				else do
					let len = MV.length l - 1
					ok <- searchBoundry x l succ len
					if ok
						then searchBoundry x l pred len
						else return False

searchBoundry :: forall s. Int -> MV.MVector s Char -> (Int -> Int) -> Int -> ST s Bool
searchBoundry x l f len =
	let x' = f x
	in if x' <= len && x' >= 0
		then do
			c <- MV.read l x'
			if isBoundry c
				then return True
				else searchBoundry x' l f len
		else return False