My solutions to 2018's advent of code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

day17.nim 5.8KB

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