1+ const isLow = ( val , col , row , rows ) => {
2+ // Collect points in each cardinal direction
3+ const points = [ ]
4+ // TODO: If supporting diagonal checks, use this logic instead to loop
5+ // for (let x = -1; x <= 1; x++) {
6+ // for (let y = -1; y <= 1; y++) {
7+ // if(x != 0 && y != 0)
8+ // if(rows[row + y] && rows[row + y][col + x]
9+ // }
10+ // }
11+ if ( rows [ row - 1 ] && rows [ row - 1 ] [ col ] ) { points . push ( parseInt ( rows [ row - 1 ] [ col ] ) ) }
12+ if ( rows [ row + 1 ] && rows [ row + 1 ] [ col ] ) { points . push ( parseInt ( rows [ row + 1 ] [ col ] ) ) }
13+ if ( rows [ row ] && rows [ row ] [ col - 1 ] ) { points . push ( parseInt ( rows [ row ] [ col - 1 ] ) ) }
14+ if ( rows [ row ] && rows [ row ] [ col + 1 ] ) { points . push ( parseInt ( rows [ row ] [ col + 1 ] ) ) }
15+
16+ // NOTE - if the value is the same as a neighbor,
17+ // that isn't counted as a low (even though together, they can be a low)
18+ // ... this might be a concern for part 2 ....
19+ return ( val < Math . min ( ...points ) ) // value should be lower than any other points
20+ }
21+
122const findLocalLows = ( data ) => {
223 const lows = [ ]
324 const rows = data . split ( '\n' )
425 let checked = 0
526
6- const isLow = ( val , col , row ) => {
7- // Collect points in each cardinal direction
8- const points = [ ]
9- // TODO: If supporting diagonal checks, use this logic instead to loop
10- // for (let x = -1; x <= 1; x++) {
11- // for (let y = -1; y <= 1; y++) {
12- // if(x != 0 && y != 0)
13- // if(rows[row + y] && rows[row + y][col + x]
14- // }
15- // }
16- if ( rows [ row - 1 ] && rows [ row - 1 ] [ col ] ) { points . push ( parseInt ( rows [ row - 1 ] [ col ] ) ) }
17- if ( rows [ row + 1 ] && rows [ row + 1 ] [ col ] ) { points . push ( parseInt ( rows [ row + 1 ] [ col ] ) ) }
18- if ( rows [ row ] && rows [ row ] [ col - 1 ] ) { points . push ( parseInt ( rows [ row ] [ col - 1 ] ) ) }
19- if ( rows [ row ] && rows [ row ] [ col + 1 ] ) { points . push ( parseInt ( rows [ row ] [ col + 1 ] ) ) }
20-
21- // NOTE - if the value is the same as a neighbor,
22- // that isn't counted as a low (even though together, they can be a low)
23- // ... this might be a concern for part 2 ....
24- return ( val < Math . min ( ...points ) ) // value should be lower than any other points
25- }
26-
2727 rows . forEach ( ( row , rowIdx ) => {
2828 for ( let c = 0 ; c < row . length ; c ++ ) {
2929 const cell = parseInt ( row [ c ] )
30- if ( isLow ( cell , c , rowIdx ) ) {
30+ if ( isLow ( cell , c , rowIdx , rows ) ) {
3131 lows . push ( cell )
3232 console . debug ( `Found low at ${ c } ,${ rowIdx } : ${ cell } ` )
3333 }
@@ -39,6 +39,118 @@ const findLocalLows = (data) => {
3939 return lows
4040}
4141
42+ const flow = ( col , row , map , data , source ) => {
43+ // Don't test invalid points
44+ if ( col < 0 || col >= map . coords [ 0 ] . length ) {
45+ console . debug ( `${ col } ,${ row } is out of bounds` )
46+ // Exceeds map horizontally
47+ return {
48+ map,
49+ result : false
50+ }
51+ }
52+ if ( row < 0 || row >= map . coords . length ) {
53+ console . debug ( `${ col } ,${ row } is out of bounds` )
54+ // Exceeds map vertically
55+ return {
56+ map,
57+ result : false
58+ }
59+ }
60+
61+ // If the point is a peak, register and don't continue
62+ if ( parseInt ( data [ row ] [ col ] ) === 9 ) {
63+ console . debug ( `${ col } ,${ row } is a peak.` )
64+ // Peaks aren't part of basins
65+ map . coords [ row ] [ col ] = 'p'
66+ return {
67+ map,
68+ result : false
69+ }
70+ }
71+
72+ // If the point is higher than the source, we can't drain
73+ // BIG ASSUMPTION here about equal-height points
74+ if ( data [ row ] [ col ] >= source ) {
75+ console . debug ( `${ col } ,${ row } is higher (${ data [ row ] [ col ] } >= ${ source } ). Water can't flow uphill.` )
76+ return {
77+ map,
78+ result : false
79+ }
80+ }
81+
82+ // If the point already mapped to a basin, don't recalculate its flow
83+ if ( map . coords [ row ] && map . coords [ row ] [ col ] ) {
84+ console . debug ( `${ col } ,${ row } is already known to be in basin ${ map . coords [ row ] [ col ] } ` )
85+ return {
86+ map,
87+ result : map . coords [ row ] [ col ]
88+ }
89+ }
90+
91+ // If we've reached a low point, stop tracing
92+ if ( isLow ( data [ row ] [ col ] , col , row , data ) ) {
93+ console . debug ( `${ col } ,${ row } is a low point in basin.` )
94+ // register a basin with an area of 1
95+ map . basins . push ( 1 )
96+ // mark the low point to the basin
97+ map . coords [ row ] [ col ] = map . basins . length - 1
98+ console . debug ( `registered basin ${ map . basins . length - 1 } ` )
99+ return {
100+ map,
101+ result : map . coords [ row ] [ col ]
102+ }
103+ // HUGE ASSUMPTION that each basin only has 1 low point
104+ }
105+
106+ console . debug ( `checking where point ${ col } ,${ row } drains to` )
107+
108+ // Check the next points in each cardinal direction
109+ const drains = [ ]
110+ let result = false
111+ result = flow ( col + 1 , row , map , data , data [ row ] [ col ] ) // right
112+ map = result . map
113+ drains . push ( result . result )
114+ result = flow ( col - 1 , row , map , data , data [ row ] [ col ] ) // left
115+ map = result . map
116+ drains . push ( result . result )
117+ result = flow ( col , row - 1 , map , data , data [ row ] [ col ] ) // up
118+ map = result . map
119+ drains . push ( result . result )
120+ result = flow ( col , row + 1 , map , data , data [ row ] [ col ] ) // down
121+ map = result . map
122+ drains . push ( result . result )
123+
124+ const results = drains . filter ( ( c ) => c !== false )
125+ if ( results . length > 1 ) {
126+ console . warn ( 'Point has more than one possilbe drain.' )
127+ const uniqueDrains = [ ...new Set ( results ) ]
128+ if ( uniqueDrains . length > 1 ) {
129+ console . debug ( drains )
130+ throw new Error ( 'Point drains into multiple drains. Data might be bad.' )
131+ }
132+ // Otherwise, all drains go to the same basin, so that's the same as having 1 drain
133+ }
134+ if ( results . length === 0 ) {
135+ console . debug ( drains )
136+ throw new Error ( 'Point is not the low, but has no drains. Data might be bad.' )
137+ }
138+
139+ const basin = parseInt ( results [ 0 ] )
140+
141+ // Mark the point as belonging to the basin it drains into
142+ map . coords [ row ] [ col ] = basin
143+ // Track the area of the basin so we don't have to recalculate it later
144+ map . basins [ basin ] ++
145+
146+ // return the findings recursively
147+ return {
148+ map,
149+ result : map . coords [ row ] [ col ]
150+ }
151+ }
152+
42153module . exports = {
43- findLocalLows
154+ findLocalLows,
155+ flow
44156}
0 commit comments