|
@@ -0,0 +1,163 @@
|
|
1
|
+import sequtils, strformat, strutils, tables
|
|
2
|
+
|
|
3
|
+type
|
|
4
|
+ Point = tuple[x,y: int]
|
|
5
|
+ GroundType = enum
|
|
6
|
+ gtSand, gtClay, gtStillWater, gtFlowingWater
|
|
7
|
+
|
|
8
|
+var
|
|
9
|
+ grid = newTable[Point, GroundType]()
|
|
10
|
+ miny = int.high
|
|
11
|
+ maxy = int.low
|
|
12
|
+
|
|
13
|
+func down(point: Point): Point =
|
|
14
|
+ (point.x, point.y + 1)
|
|
15
|
+
|
|
16
|
+func isAt(grid: TableRef[Point, GroundType], gtype: GroundType, point: Point): bool =
|
|
17
|
+ grid.getOrDefault(point, gtSand) == gtype
|
|
18
|
+
|
|
19
|
+func supportsWater(grid: TableRef[Point, GroundType], point: Point): bool =
|
|
20
|
+ let ground = grid.getOrDefault(point, gtSand)
|
|
21
|
+ ground == gtClay or ground == gtStillWater
|
|
22
|
+
|
|
23
|
+func countWater(grid: TableRef[Point, GroundType]): tuple[all,still: int] =
|
|
24
|
+ for val in grid.values:
|
|
25
|
+ if val == gtFlowingWater:
|
|
26
|
+ result.all.inc
|
|
27
|
+ if val == gtStillWater:
|
|
28
|
+ result.all.inc
|
|
29
|
+ result.still.inc
|
|
30
|
+
|
|
31
|
+# Debug utility to print the grid nicely. Requires a small terminal font size.
|
|
32
|
+proc print(grid: TableRef[Point, GroundType]) =
|
|
33
|
+ for y in miny-1..maxy+1:
|
|
34
|
+ var line = fmt "{y:4} "
|
|
35
|
+ for x in 300..700:
|
|
36
|
+ case grid.getOrDefault((x, y), gtSand):
|
|
37
|
+ of gtSand: line &= " "
|
|
38
|
+ of gtClay: line &= "█"
|
|
39
|
+ of gtStillWater: line &= "~"
|
|
40
|
+ of gtFlowingWater: line &= "|"
|
|
41
|
+ echo line
|
|
42
|
+ echo ""
|
|
43
|
+
|
|
44
|
+# These all call each other so we have to forward declare them
|
|
45
|
+proc flowDown(grid: TableRef[Point, GroundType], point: Point): bool
|
|
46
|
+proc flowOut(grid: TableRef[Point, GroundType], point: Point): bool
|
|
47
|
+proc findEdge(grid: TableRef[Point, GroundType], point: Point, direction: int): tuple[enclosed: bool, last: int]
|
|
48
|
+
|
|
49
|
+# flowDown() is responsible for recursively following a stream of water
|
|
50
|
+# downwards until it hits something or surpasses the maximum y extent.
|
|
51
|
+#
|
|
52
|
+# The proc returns `true` if the downward flow has spread out (e.g.
|
|
53
|
+# has filled in the bottom row of a "U" shaped area). If this happens
|
|
54
|
+# the caller will also attempt to flow out to fill the next row.
|
|
55
|
+proc flowDown(grid: TableRef[Point, GroundType], point: Point): bool =
|
|
56
|
+ let
|
|
57
|
+ next = point.down
|
|
58
|
+ cell = grid.getOrDefault(next, gtSand)
|
|
59
|
+
|
|
60
|
+ if point.y > maxy:
|
|
61
|
+ return false
|
|
62
|
+
|
|
63
|
+ if cell == gtSand:
|
|
64
|
+ # There's nothing below us, keep flowing. Check to see if the flow
|
|
65
|
+ # below is fills out, and if it does flow out on this row too;
|
|
66
|
+ # otherwise, this is just a normal downward flow and we return false
|
|
67
|
+ # as our parents don't need to flow out.
|
|
68
|
+ if grid.flowDown(next):
|
|
69
|
+ return grid.flowOut(point)
|
|
70
|
+ else:
|
|
71
|
+ grid[point] = gtFlowingWater
|
|
72
|
+ return false
|
|
73
|
+
|
|
74
|
+ elif cell == gtClay or cell == gtStillWater:
|
|
75
|
+ # We've reached the bottom of a hole (or the existing water
|
|
76
|
+ # level within a complex shape), flow outwards
|
|
77
|
+ return grid.flowOut(point)
|
|
78
|
+
|
|
79
|
+ elif cell == gtFlowingWater:
|
|
80
|
+ # We've encountered some flowing water (i.e. we have a parallel
|
|
81
|
+ # stream that has already flowed out below us). There's no need
|
|
82
|
+ # to recheck anything, just join up with the other flow.
|
|
83
|
+ grid[point] = gtFlowingWater
|
|
84
|
+ return false
|
|
85
|
+
|
|
86
|
+# flowOut() is responsible for expanding a flow of water horizontally
|
|
87
|
+# after it hits something. It scans left and right to find the maximum
|
|
88
|
+# extents, and whether or not they're enclosed.
|
|
89
|
+#
|
|
90
|
+# If both sides are enclosed then all the cells inbetween fill with
|
|
91
|
+# still water, and we return true so that the row above us flows out.
|
|
92
|
+#
|
|
93
|
+# Sides that aren't enclosed are flowed down. In complex shapes these
|
|
94
|
+# may fill up areas below them. Where both sides fill up, or one side
|
|
95
|
+# is enclosed and the other fills up, we recursively call ourselves
|
|
96
|
+# to deal with the new situation.
|
|
97
|
+#
|
|
98
|
+# Finally, in other cases the cells inbetween the extents become
|
|
99
|
+# flowing water and we return false.
|
|
100
|
+proc flowOut(grid: TableRef[Point, GroundType], point: Point): bool =
|
|
101
|
+ let
|
|
102
|
+ left = grid.findEdge(point, -1)
|
|
103
|
+ right = grid.findEdge(point, 1)
|
|
104
|
+
|
|
105
|
+ if left.enclosed and right.enclosed:
|
|
106
|
+ # Both sides are enclosed, just fill'er up and make our
|
|
107
|
+ # parent recalculate.
|
|
108
|
+ for x in left.last..right.last:
|
|
109
|
+ grid[(x, point.y)] = gtStillWater
|
|
110
|
+ return true
|
|
111
|
+
|
|
112
|
+ var leftFilled, rightFilled: bool
|
|
113
|
+
|
|
114
|
+ if not left.enclosed:
|
|
115
|
+ leftFilled = grid.flowDown((left.last, point.y + 1))
|
|
116
|
+ if not right.enclosed:
|
|
117
|
+ rightFilled = grid.flowDown((right.last, point.y + 1))
|
|
118
|
+
|
|
119
|
+ if (left.enclosed or leftFilled) and (right.enclosed or rightFilled):
|
|
120
|
+ # We've filled in some holes and now our extents are no longer
|
|
121
|
+ # valid. Recursively call ourselves to figure out what's what.
|
|
122
|
+ return flowOut(grid, point)
|
|
123
|
+ else:
|
|
124
|
+ # We didn't fill anything, so this is the layer of flowing
|
|
125
|
+ # water on top of a platform.
|
|
126
|
+ for x in left.last..right.last:
|
|
127
|
+ grid[(x, point.y)] = gtFlowingWater
|
|
128
|
+ return false
|
|
129
|
+
|
|
130
|
+proc findEdge(grid: TableRef[Point, GroundType], point: Point, direction: int): tuple[enclosed: bool, last: int] =
|
|
131
|
+ var x = point.x
|
|
132
|
+ while grid.supportsWater((x, point.y + 1)) and not grid.isAt(gtClay, (x + direction, point.y)):
|
|
133
|
+ x += direction
|
|
134
|
+ result.last = x
|
|
135
|
+ result.enclosed = grid.getOrDefault((x + direction, point.y), gtSand) == gtClay
|
|
136
|
+
|
|
137
|
+for line in readFile("data/17.txt").strip.splitlines:
|
|
138
|
+ let
|
|
139
|
+ parts = line.split(", ")
|
|
140
|
+ first = parts[0].substr(2).parseInt
|
|
141
|
+ numbers = parts[1].substr(2).split("..")
|
|
142
|
+ isX = parts[1][0] == 'x'
|
|
143
|
+
|
|
144
|
+ for i in numbers[0].parseInt..numbers[1].parseInt:
|
|
145
|
+ var x,y: int
|
|
146
|
+ if isX:
|
|
147
|
+ x = i
|
|
148
|
+ y = first
|
|
149
|
+ else:
|
|
150
|
+ x = first
|
|
151
|
+ y = i
|
|
152
|
+
|
|
153
|
+ grid[(x, y)] = gtClay
|
|
154
|
+ miny = min(miny, y)
|
|
155
|
+ maxy = max(maxy, y)
|
|
156
|
+
|
|
157
|
+discard grid.flowDown((500, miny))
|
|
158
|
+
|
|
159
|
+# grid.print()
|
|
160
|
+
|
|
161
|
+let result = grid.countWater
|
|
162
|
+echo result.all
|
|
163
|
+echo result.still
|