Skip to content

Commit ce919a3

Browse files
committed
Merge multiple user inputs during same tick
1 parent ffad06e commit ce919a3

File tree

6 files changed

+50
-10
lines changed

6 files changed

+50
-10
lines changed

src/Life.hs

+17-2
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ module Life
55
, World
66
, newWorld
77
, newWorldWithCells
8+
, merge
89
, size
910
, addresses
1011
, evolve
1112
, updateCells
1213
, cellAt
14+
, safeCellAt
1315
, cells
1416
, neighboringAddresses
1517
, fate
@@ -18,7 +20,6 @@ module Life
1820

1921
import Data.Array.IArray
2022
import Data.Array.Unboxed
21-
import Data.Word
2223

2324
data Cell = Alive | Dead deriving (Show, Eq)
2425
type Dimension = Int
@@ -32,6 +33,17 @@ instance Show World where
3233
newWorld :: Address -> World
3334
newWorld worldSize = newWorldWithCells worldSize (cycle [Dead])
3435

36+
merge :: World -> World -> World
37+
merge w1@(World ary1) w2@(World ary2) = World $ array
38+
((0,0), (maxX, maxY))
39+
[ ((x,y), boolAt (x,y) ary1 || boolAt (x,y) ary2) | x <- [0..maxX], y <- [0..maxY] ]
40+
41+
where (width1, height1) = size w1
42+
(width2, height2) = size w2
43+
boolAt ix ary | inRange (bounds ary) ix = ary ! ix
44+
| otherwise = False
45+
(maxX, maxY) = (max width1 width2 - 1, max height1 height2 - 1)
46+
3547
toCell :: Bool -> Cell
3648
toCell True = Alive
3749
toCell False = Dead
@@ -47,7 +59,6 @@ size :: World -> Address
4759
size (World ary) = (maxX + 1, maxY + 1)
4860
where (maxX, maxY) = snd $ bounds ary
4961

50-
5162
addresses :: World -> [Address]
5263
addresses (World ary) = indices ary
5364

@@ -62,6 +73,10 @@ updateCells updates (World ary) = World $ (ary // boolUpdates) where
6273
cellAt :: World -> Address -> Cell
6374
cellAt (World ary) ix = toCell (ary ! ix)
6475

76+
safeCellAt :: World -> Address -> Cell
77+
safeCellAt w@(World ary) ix | inRange (bounds ary) ix = cellAt w ix
78+
| otherwise = Dead
79+
6580
cells :: World -> [Cell]
6681
cells (World ary) = map toCell (elems ary)
6782

src/Pattern.hs

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ module Pattern
88
import Data.List
99
import Life
1010

11-
data Pattern = Pattern ![(Address, Cell)] deriving (Show)
11+
data Pattern = Pattern { patternCells :: [(Address, Cell)] } deriving (Show)
1212

1313
loadPattern :: FilePath -> IO Pattern
1414
loadPattern filePath = do
@@ -17,7 +17,7 @@ loadPattern filePath = do
1717
parseChar ' ' = Alive
1818
mapCell y x cell = (x+1, ((x,y), parseChar cell))
1919
mapRow y row = (y+1, snd $ mapAccumL (mapCell y) 0 row)
20-
patternCells = snd $ mapAccumL mapRow 0 (lines fileContents)
20+
!patternCells = snd $ mapAccumL mapRow 0 (lines fileContents)
2121
return $ Pattern (concat patternCells)
2222

2323
drawPatternAt :: Address -> Pattern -> World -> World

src/Timeline/Slice.hs

+3-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ newSlice f w t = Slice { world = w, tick = t, projection = f w t, slHistory = [(
3737

3838
nextSlice :: Projector b -> Slice a -> Slice b
3939
nextSlice f s = let tick' = (tick s) + 1
40-
world' = evolve (head $ slUserWorlds s ++ [world s])
40+
world' | (not . null) (slUserWorlds s) = evolve (foldl1' merge $ slUserWorlds s)
41+
| otherwise = evolve (world s)
4142
in Slice { tick = tick'
4243
, projection = f world' tick'
4344
, world = world'
@@ -52,7 +53,7 @@ addUserInput :: Tick -> (World -> World) -> Slice a -> Maybe (Slice a)
5253
addUserInput inputTick f s = do worldAtTimeOfInput <- lookup inputTick (slHistory s)
5354
-- evaluating strictly so any exceptions will be raised while request is still being handled
5455
let !userWorld = evolveUpTo (f worldAtTimeOfInput) inputTick (tick s)
55-
return $ s { slUserWorlds = [ userWorld ] }
56+
return $ s { slUserWorlds = userWorld : slUserWorlds s }
5657
where evolveUpTo w t t' | t == t' = w
5758
| otherwise = evolveUpTo (evolve w) (t + 1) t'
5859

test/ArbitraryInstances.hs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module ArbitraryInstances where
22

33
import Control.Monad
4+
import Data.Function
5+
import Data.List
46
import Data.Word
57

68
import Test.QuickCheck.Gen
@@ -51,4 +53,4 @@ instance Arbitrary a => Arbitrary (Slice a) where
5153
arbitrary = liftM3 newSlice arbitrary arbitrary arbitrary
5254

5355
instance Arbitrary Pattern where
54-
arbitrary = liftM Pattern arbitrary
56+
arbitrary = liftM Pattern (liftM (nubBy ((==) `on` fst)) arbitrary)

test/LifeTests.hs

+10
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,14 @@ tests = testGroup "Life"
3535
(fate Dead neighbors == Alive) == (length (filter (==Alive) neighbors) == 3)
3636
, "an Alive cell with 2 or 3 live neighbors lives" `testProperty` \(NeighborList neighbors) ->
3737
(fate Alive neighbors == Alive) == (length (filter (==Alive) neighbors) `elem` [2,3])
38+
39+
, "merge produces world big enough for both worlds" `testProperty` \world1 world2 ->
40+
let (width1, height1) = size world1
41+
(width2, height2) = size world2
42+
in size (merge world1 world2) == (max width1 width2, max height1 height2)
43+
44+
, "merge favors life over death" `testProperty` \world1 world2 ->
45+
let mergedWorld = merge world1 world2
46+
mergedOk ix = (cellAt mergedWorld ix == Alive) == (safeCellAt world1 ix == Alive || safeCellAt world2 ix == Alive)
47+
in all mergedOk (addresses mergedWorld)
3848
]

test/Timeline/SliceTests.hs

+15-3
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,29 @@ tests = testGroup "Timeline.Slice" [
3535

3636
,"nextSlice evolves new world from current one" `testProperty` \slice (Blind projector) ->
3737
let _ = (slice, projector) :: (Slice (), Projector ())
38-
slice' = nextSlice projector slice
39-
in world slice' == evolve (world slice)
38+
in world (nextSlice projector slice) == evolve (world slice)
4039

4140
,"nextSlice evolves from userInput if present" `testProperty` \slice (Blind projector) input ->
4241
let _ = (slice, projector) :: (Slice (), Projector ())
42+
4343
Just sliceWithInput = addUserInput (tick slice) (drawPatternAt (0,0) input) slice
4444
slice' = nextSlice projector sliceWithInput
45-
worldWithInput = (drawPatternAt (0,0) input $ world slice)
45+
worldWithInput = drawPatternAt (0,0) input $ world slice
46+
4647
in world slice' == evolve worldWithInput &&
4748
world (nextSlice projector slice') == (evolve . evolve) worldWithInput
4849

50+
,"nextSlice unions multiple user inputs" `testProperty` \slice (Blind projector) input1 input2 ->
51+
let _ = (slice, projector) :: (Slice (), Projector ())
52+
53+
Just sliceWithInput1 = addUserInput (tick slice) (drawPatternAt (0,0) input1) slice
54+
Just sliceWithInput2 = addUserInput (tick slice) (drawPatternAt (0,0) input2) sliceWithInput1
55+
56+
worldWithInput1 = drawPatternAt (0,0) input1 (world slice)
57+
worldWithInput2 = drawPatternAt (0,0) input2 (world slice)
58+
59+
in world (nextSlice projector sliceWithInput2) == evolve (merge worldWithInput1 worldWithInput2)
60+
4961
,"nextSlice evolves world from user historical input" `testProperty` \slice (Blind projector) input ->
5062
let _ = (slice, projector) :: (Slice (), Projector ())
5163
futureSlice = foldl (.) id (replicate 3 $ nextSlice projector) slice

0 commit comments

Comments
 (0)