aboutsummaryrefslogtreecommitdiff
path: root/2020/20b.hs
blob: 7bce2109d2e62754db7a7c8e5638a231edd4eaf7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
{-# LANGUAGE TupleSections #-}

module Day20B where

import Data.Maybe (isNothing, isJust)
import Data.List (transpose, sortOn, nub)
import Data.List.Split (splitOn)
import System.Environment (getArgs)
import qualified Data.Map.Strict as M
import Text.Printf (printf)
import Debug.Trace (trace)

-- Types

-- (y, x)
type Tile = [[Bool]];
type TileWithId = (Int, Tile);
type Coord = (Int, Int);
type Grid = M.Map Coord TileWithId;

-- Flip Vertical, Rotations (0-3)
data Orientation = Transform Bool Int;

orients :: [Orientation]
orients = [Transform f r | f <- [True, False], r <- [0..3]]

-- top, left, right, bottom
data Side = T | L | R | B
        deriving (Show, Eq);

sides :: [Side]
sides = [T, L, R, B]

complement :: Side -> Side
complement T = B
complement B = T
complement L = R
complement R = L

-- Tile manipulation
rotate :: Tile -> Tile
rotate = transpose . reverse

rotateN :: Int -> Tile -> Tile
rotateN 0 t = t
rotateN n t = rotateN (n - 1) (rotate t)

reorient :: Orientation -> Tile -> Tile
reorient (Transform False n) = rotateN n
reorient (Transform True n) = rotateN n . reverse

permutations :: Tile -> [Tile]
permutations t = [reorient o t | o <- orients]

--- Side / Edge manipulation
-- Get the border on the given side of the tile
border :: Side -> Tile -> [Bool]
border T t = head t
border B t = last t
border L t = map head t
border R t = map last t

-- Get all borders of a given tile
borders :: Tile -> [[Bool]]
borders t = map (`border` t) sides

-- Get the coord at the given side of that coord
getSideCoord :: Coord -> Side -> Coord
getSideCoord (x, y) T = (x, y - 1)
getSideCoord (x, y) B = (x, y + 1)
getSideCoord (x, y) L = (x - 1, y)
getSideCoord (x, y) R = (x + 1, y)

-- Return true if y is on the s of x
checkBorder :: Tile -> Tile -> Side -> Bool
checkBorder x y s = border (complement s) y == border s x

-- y is on the s of x (if it exists)
attemptCheckBorder :: Tile -> Maybe Tile -> Side -> Bool
attemptCheckBorder _ Nothing _ = True
attemptCheckBorder x (Just y) s = checkBorder x y s

--- Grid manipulations

-- Get the bounds of the grid
-- Returns (low, high)
squareBounds :: M.Map Coord a -> (Coord, Coord)
squareBounds g = (M.foldrWithKey (compCoord min) (0, 0) g,
                     M.foldrWithKey (compCoord max) (0, 0) g)
    where compCoord f (x', y') _ (x, y) = (f x' x, f y' y)

-- Get the corners of the grid
getCorners :: Grid -> [Coord]
getCorners g = [(lx, ly), (lx, hy), (hx, ly), (hx, hy)]
            where ((lx, ly), (hx, hy)) = squareBounds g

-- Returns true if the grid is rectangular
isRect :: Grid -> Bool
isRect g = all (isJust . (g M.!?)) (getCorners g)

-- Return true if given side is unfilled for coord of grid
sideIsUnfilled :: Side -> Grid -> Coord -> Bool
sideIsUnfilled s g c = isNothing $ M.lookup (getSideCoord c s) g

--- Checking and solving
fitsSpace :: Grid -> Coord -> Tile -> Bool
fitsSpace g c t = and [attemptCheckBorder t (maybeNoDefault snd $ M.lookup (getSideCoord c s) g) s | s <- sides]

-- Returns tiles that could be on the given side of the given coord
possibleNeighbours :: [TileWithId] -> Grid -> Coord -> Side -> [TileWithId]
possibleNeighbours ts g c s = filter (fitsSpace g c' . snd) ts
                                where c' = getSideCoord c s

-- Check if the entire grid is valid
isValid :: Grid -> Bool
isValid m = M.foldrWithKey checkSquare True m
              where checkSquare l (_, t) v = v && fitsSpace m l t

-- Add to the given side of each coord, returning all possible ways to do so and the remaining tiles from each.
fillSideOnce :: Side -> [TileWithId] -> Grid -> [([TileWithId], Grid)]
fillSideOnce _ [] m = [([], m)]
fillSideOnce s xs m | null mutations = [(xs, m)]
                    | otherwise = mutatedMaps
            where mutations :: [(Coord, [TileWithId])] -- list of squares to mutate, and possibilities
                  mutations = filter (not . null . snd) $ map getNextFill $ M.toList m
                  getNextFill :: (Coord, TileWithId) -> (Coord, [TileWithId])
                  getNextFill (c, _) | sideIsUnfilled s m c = (getSideCoord c s, possibleNeighbours xs m c s)
                                     | otherwise = (getSideCoord c s, [])
                  attempts :: [[(Coord, TileWithId)]] -- All the possible lists of mutations we can make
                  attempts = filter (allUnique . map (fst . snd)) $ map (zip $ map fst mutations) (oneFromEach $ map snd mutations)
                  mutatedMaps :: [([TileWithId], Grid)] -- new remaining tiles, new grid
                  mutatedMaps = map doAllMutations attempts 
                  doAllMutations :: [(Coord, TileWithId)] -> ([TileWithId], Grid)
                  doAllMutations = foldr mutateMap (xs, m)
                  mutateMap :: (Coord, TileWithId) -> ([TileWithId], Grid) -> ([TileWithId], Grid)
                  mutateMap (newC, (newId, newTile)) (re, grid) = (filter ((/= newId) . fst) re, M.insert newC (newId, newTile) grid)

fillRect :: [([TileWithId], Grid)] -> [([TileWithId], Grid)]
fillRect xs | not (null fullSols) = fullSols
            | otherwise = fillRect $ filter (not . (`elem` xs)) $ foldr (\s ps -> concatMap (uncurry $ fillSideOnce s) ps) xs sides
          where fullSols = filter (null . fst) xs

-- Pattern matching
type Pattern = [Coord] -- List of coord offsets that must be set.

data Square = Empty | Filled | Highlight
  deriving (Eq, Show);
type PlainGrid = M.Map Coord [[Square]]

toSquare :: Bool -> Square
toSquare True = Filled
toSquare False = Empty

monsterPattern :: Pattern
monsterPattern = [
  (0, 1),
  (1, 2),
  (4, 2),
  (5, 1),
  (6, 1),
  (7, 2),
  (10, 2),
  (11, 1),
  (12, 1),
  (13, 2),
  (16, 2),
  (17, 1),
  (18, 0),
  (18, 1),
  (19, 1)
  ]

patternAt :: Pattern -> Coord -> [Coord]
patternAt p c = map (addCoord c) p

-- Get the value at a specific coord
getSpecificCoord :: PlainGrid -> Coord -> Maybe Square
getSpecificCoord g (x, y) = getFromTile $ g M.!? (x `div` tileSize, y `div` tileSize)
                              where tileSize = length $ g M.! (0, 0)
                                    getFromTile Nothing = Nothing
                                    getFromTile (Just t) = Just $ t!!(y `mod` tileSize)!!(x `mod` tileSize)

-- Set the value at a specific coord
setSpecificCoord :: PlainGrid -> Coord -> Square -> PlainGrid
setSpecificCoord g (x, y) b = M.insert (tx, ty) t' g
                                where tileSize = length $ g M.! (0, 0)
                                      (tx, ty) = (x `div` tileSize, y `div` tileSize)
                                      (sx, sy) = (x `mod` tileSize, y `mod` tileSize)
                                      t = g M.! (tx, ty)
                                      r = t!!sy
                                      r' = take sx r ++ [b] ++ drop (sx + 1) r
                                      t' = take sy t ++ [r'] ++ drop (sy + 1) t

-- Returns true if the pattern is true at the given coord
patternTrueAt :: PlainGrid -> Coord -> Pattern -> Bool
patternTrueAt g b = all (areSet . addCoord b)
                      where areSet c = case getSpecificCoord g c of
                                         Just Filled -> True
                                         Just Highlight -> True
                                         _ -> False

-- Strip all borders from all tiles
stripBorders :: Grid -> PlainGrid
stripBorders = M.map strip
                 where strip (_, t) = map (map toSquare . init . tail) $ init $ tail t

-- Find all occurences of a pattern
findAllOccs :: PlainGrid -> Pattern -> [Coord]
findAllOccs g p = trace (show (lx, hy, hx, hy)) $ filter (\c -> patternTrueAt g c p) $ [(x, y) | x <- [lx..hx], y <- [ly..hy]]
                    where ((ltx, lty), (htx, hty)) = squareBounds g
                          tileSize = length $ head $ g M.! (0, 0)
                          (lx, ly) = (ltx * tileSize, lty * tileSize)
                          (hx, hy) = ((htx + 1) * tileSize, (hty + 1) * tileSize)

-- runghc --ghc-arg='-package split' --ghc-arg='-package extra' 20a.hs inputs/day20
main :: IO ()
main = do 
        args <- getArgs;
        ((si, st):ts) <- parseFile (head args);

        -- Expand the tiles to include all orientations
        let ts' = concatMap (\(i, t) -> map (i,) $ permutations t) ts;
        printf "%d possible tiles\n" (length ts');

        -- Get our starting states
        let starting = [(ts', M.singleton (0, 0) (si, t')) | t' <- permutations st];

        -- Get the first solution that uses all tiles
        let imgs = map snd $ filter (null . fst) $ fillRect starting;

        printf "Found %d images\n" (length imgs);

        -- Remove border tiles
        let imgs' = map stripBorders imgs;

        -- Find the monsters
        let ((df, ms):_) = sortOn ((0 -) . length . snd) $ map (\img -> (img, findAllOccs img monsterPattern)) imgs';

        printf "Found, at most, %d monsters\n" (length ms);

        -- Delete all tiles part of a monster
        let tiles = concatMap (patternAt monsterPattern) ms;
        let img' = foldr (\c i -> setSpecificCoord i c Highlight) df tiles;
        
        putStr $ prettyPrint img';

        let answer = length $ filter (== Filled) $ concatMap (concat . snd) $ M.toList img';
        printf "Tiles remaining = %d\n" answer;
        return ();

-- Utilities
maybeNoDefault :: (a -> b) -> Maybe a -> Maybe b
maybeNoDefault _ Nothing = Nothing
maybeNoDefault f (Just x) = Just (f x)

unwrap :: Maybe a -> a
unwrap (Just x) = x
unwrap _ = error "unwrap on null value"

addCoord :: Coord -> Coord -> Coord
addCoord (x, y) (x', y') = (x + x', y + y')

-- Get all possible combinations by picking one element from each of the sublists
oneFromEach :: [[a]] -> [[a]]
oneFromEach [] = [[]]
oneFromEach (xs:xss) = concat $ [ map (x : ) (oneFromEach xss) | x <- xs]

allUnique :: Eq a => [a] -> Bool
allUnique xs = length (nub xs) == length xs

-- Pretty printing
prettyPrintIds :: PlainGrid -> String
prettyPrintIds grid = unlines [concatMap show (getRow y) | y <- [lyc..hyc]]
        where ((_, lyc), (_, hyc)) = squareBounds grid
              getRow y = map fst $ M.toAscList $ M.filterWithKey (\(_, y') _ -> y' == y) grid

prettyPrint :: PlainGrid -> String
prettyPrint grid = concat [unlines $ printTiles (getRow y) | y <- [lyc..hyc]]
        where tileSize = length (grid M.! (0, 0))
              ((_, lyc), (_, hyc)) = squareBounds grid
              getRow y = map snd $ M.toAscList $ M.filterWithKey (\(_, y') _ -> y' == y) grid
              printTiles ts = [concatMap (rowToStr . (!!y)) ts | y <- [0..tileSize - 1]]

rowToStr :: [Square] -> String
rowToStr [] = ""
rowToStr (Filled:xs) = '#' : rowToStr xs
rowToStr (Empty:xs) = '.' : rowToStr xs
rowToStr (Highlight:xs) = 'O' : rowToStr xs

-- Parsing
parseTileDef :: String -> (Int, Tile)
parseTileDef s = (read n, map readLine ls)
        where (f:ls) = lines s
              n = drop 5 $ take (length f - 1) f
              readLine [] = []
              readLine ('.':xs) = False : readLine xs
              readLine ('#':xs) = True : readLine xs
              readLine _ = error "invalid input"

parseInput :: String -> [(Int, Tile)]
parseInput = map parseTileDef . splitOn "\n\n"

parseFile :: String -> IO [(Int, Tile)]
parseFile f = do
                c <- readFile f
                return $ parseInput c;