Skip to content

Commit 0fab74f

Browse files
committed
refactor(day20): cleanup and docs
1 parent 777fac4 commit 0fab74f

File tree

6 files changed

+106
-98
lines changed

6 files changed

+106
-98
lines changed

day20/day20.solution.spec.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import {
2-
transportingMazeSolver,
3-
drawSolution,
4-
Location,
5-
} from './transportingMazeSolver'
1+
import { transportingMazeSolver, Location } from './transportingMazeSolver'
62
import * as fs from 'fs'
73
import * as path from 'path'
4+
import { drawSolution } from './drawSolution'
85

96
describe('Day 20: Part 1', () => {
107
it('should solve the puzzle', () => {

day20/drawSolution.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Location } from './transportingMazeSolver'
2+
3+
const split = (s: string, length: number) => {
4+
const ret = []
5+
for (let offset = 0, strLen = s.length; offset < strLen; offset += length) {
6+
ret.push(s.slice(offset, length + offset))
7+
}
8+
return ret
9+
}
10+
11+
export const drawSolution = (maze: string, finalLocation: Location) => {
12+
const width = maze.indexOf('\n')
13+
const mapAsString = maze.trimEnd().replace(/\n/g, '')
14+
let solution = mapAsString
15+
finalLocation.path.forEach(p => {
16+
solution =
17+
solution.substr(0, p.y * width + p.x) +
18+
'@' +
19+
solution.substr(p.y * width + p.x + 1)
20+
})
21+
console.log(split(solution, width).join('\n'))
22+
}

day20/findPortals.spec.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,28 @@ import * as path from 'path'
44

55
describe('Find the portals', () => {
66
it('should find all portals in the map', () => {
7-
const portals = findPortals(
8-
fs.readFileSync(
9-
path.resolve(process.cwd(), 'day20/example1.txt'),
10-
'utf-8',
11-
),
7+
const example = fs.readFileSync(
8+
path.resolve(process.cwd(), 'day20/example1.txt'),
9+
'utf-8',
1210
)
11+
const portals = findPortals({ maze: example, width: example.indexOf('\n') })
1312
expect(portals).toHaveLength(8)
1413
})
1514
it('should find the VT portal in the second example', () => {
16-
const portals = findPortals(
17-
fs.readFileSync(
18-
path.resolve(process.cwd(), 'day20/example2.txt'),
19-
'utf-8',
20-
),
15+
const example = fs.readFileSync(
16+
path.resolve(process.cwd(), 'day20/example2.txt'),
17+
'utf-8',
2118
)
19+
const portals = findPortals({ maze: example, width: example.indexOf('\n') })
2220
expect(portals.filter(({ label }) => label === 'VT')).toHaveLength(2)
2321
})
2422
it('should detect inner and outer portals', () => {
25-
const portals = findPortals(
26-
fs.readFileSync(
27-
path.resolve(process.cwd(), 'day20/example2.txt'),
28-
'utf-8',
29-
),
23+
const example = fs.readFileSync(
24+
path.resolve(process.cwd(), 'day20/example2.txt'),
25+
'utf-8',
3026
)
27+
28+
const portals = findPortals({ maze: example, width: example.indexOf('\n') })
3129
const aa = portals.find(({ label }) => label === 'AA')
3230
const zz = portals.find(({ label }) => label === 'ZZ')
3331
const cpOuter = portals.find(

day20/findPortals.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export type Portal = {
99

1010
type PortalPart = { pos: Position; letter: string; isPortalFor?: Position }
1111

12+
/**
13+
* Detects outer portal, they are located on the outside of the map
14+
*/
1215
const isOuterPortal = (maze: MazeString, pos: Position): boolean => {
1316
// Outside left
1417
if (pos.x === 2) return true
@@ -22,7 +25,7 @@ const isOuterPortal = (maze: MazeString, pos: Position): boolean => {
2225
}
2326

2427
/**
25-
* Determins if a coordinate is the entry of a portal,
28+
* Determines if a coordinate is the entry of a portal,
2629
* because it has a path tile ('.') as neighbour
2730
*/
2831
const isPortalEntry = (
@@ -46,26 +49,23 @@ const isPortalEntry = (
4649
return 0
4750
}
4851

49-
export const findPortals = (maze: string): Portal[] => {
50-
const width = maze.indexOf('\n')
51-
const mapAsString = maze.trimEnd().replace(/\n/g, '')
52-
52+
export const findPortals = (maze: MazeString): Portal[] => {
5353
// Find all the letters in the map
54-
const letters = mapAsString.split('').reduce((letters, p, i) => {
54+
const letters = maze.maze.split('').reduce((letters, p, i) => {
5555
if (/[A-Z]/.test(p)) {
56-
const y = Math.floor(i / width)
57-
const entry = isPortalEntry(mapAsString, width, i % width, y)
56+
const y = Math.floor(i / maze.width)
57+
const entry = isPortalEntry(maze.maze, maze.width, i % maze.width, y)
5858
letters.push({
5959
pos: {
60-
x: i % width,
60+
x: i % maze.width,
6161
y,
6262
},
6363
letter: p,
6464
isPortalFor:
6565
entry !== 0
6666
? ({
67-
x: entry % width,
68-
y: Math.floor(entry / width),
67+
x: entry % maze.width,
68+
y: Math.floor(entry / maze.width),
6969
} as Position)
7070
: undefined,
7171
})
@@ -80,6 +80,7 @@ export const findPortals = (maze: string): Portal[] => {
8080
const pair = letters.find(
8181
l => distanceTo(letter.pos, l.pos) === 1,
8282
) as PortalPart
83+
// Figure out the label based on the relative position of the pair
8384
let label = ''
8485
if (pair.pos.x < letter.pos.x) {
8586
label = `${pair.letter}${letter.letter}`
@@ -93,10 +94,7 @@ export const findPortals = (maze: string): Portal[] => {
9394
portals.push({
9495
label,
9596
pos: letter.isPortalFor as Position,
96-
isOuter: isOuterPortal(
97-
{ maze: mapAsString, width },
98-
letter.isPortalFor as Position,
99-
),
97+
isOuter: isOuterPortal(maze, letter.isPortalFor as Position),
10098
})
10199
return portals
102100
}, [] as Portal[])

day20/transportingMazeSolver.spec.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import {
2-
transportingMazeSolver,
3-
drawSolution,
4-
Location,
5-
} from './transportingMazeSolver'
1+
import { transportingMazeSolver, Location } from './transportingMazeSolver'
62
import * as fs from 'fs'
73
import * as path from 'path'
4+
import { drawSolution } from './drawSolution'
85

96
describe('Transporting maze solver', () => {
107
it('should solve the first example', () => {

day20/transportingMazeSolver.ts

Lines changed: 54 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export type Location = {
1313
export enum Tile {
1414
PATH = '.',
1515
WALL = '#',
16-
PORTAL = '@',
1716
}
1817

1918
export enum DIRECTION {
@@ -97,19 +96,16 @@ const exploreInDirection = (
9796
visited[newLocation.level][
9897
newLocation.pos.y * maze.width + newLocation.pos.x
9998
] = true
99+
100100
// Is this a portal?
101101
const portal = portals.find(({ pos }) => equals(pos, newLocation.pos))
102102
if (portal) {
103+
// Find the other side of the portal
103104
const pair = portals.find(
104105
p => p !== portal && p.label === portal.label,
105106
) as Portal
107+
106108
if (recursive) {
107-
// When you enter the maze, you are at the outermost level (0);
108-
// when at the outermost level, only the outer labels AA and ZZ
109-
// function (as the start and end, respectively); all other outer
110-
// labeled tiles are effectively walls. At any other level,
111-
// AA and ZZ count as walls, but the other outer labeled tiles
112-
// bring you one level outward.
113109
if (location.level === 0) {
114110
// Outermost level
115111
if (portal.label === END) {
@@ -125,23 +121,22 @@ const exploreInDirection = (
125121
...newLocation,
126122
status: 'Blocked',
127123
}
128-
} else {
129-
// Inner portal takes you one level deeper
130-
if (visited[newLocation.level + 1] === undefined) {
131-
visited[newLocation.level + 1] = []
132-
}
133-
visited[newLocation.level + 1][
134-
pair.pos.y * maze.width + pair.pos.x
135-
] = true
136-
return {
137-
pos: pair.pos,
138-
path: [...location.path, location.pos, portal.pos],
139-
status: 'Valid',
140-
level: newLocation.level + 1,
141-
}
124+
}
125+
// Inner portal takes you one level deeper
126+
if (visited[newLocation.level + 1] === undefined) {
127+
visited[newLocation.level + 1] = []
128+
}
129+
visited[newLocation.level + 1][
130+
pair.pos.y * maze.width + pair.pos.x
131+
] = true
132+
return {
133+
pos: pair.pos,
134+
path: [...location.path, location.pos, portal.pos],
135+
status: 'Valid',
136+
level: newLocation.level + 1,
142137
}
143138
} else {
144-
// Other level
139+
// Other levels
145140
if ([START, END].includes(portal.label)) {
146141
// Start and end are not accessible
147142
return {
@@ -160,20 +155,19 @@ const exploreInDirection = (
160155
status: 'Valid',
161156
level: newLocation.level - 1,
162157
}
163-
} else {
164-
// Inner portal takes you one level deeper
165-
if (visited[newLocation.level + 1] === undefined) {
166-
visited[newLocation.level + 1] = []
167-
}
168-
visited[newLocation.level + 1][
169-
pair.pos.y * maze.width + pair.pos.x
170-
] = true
171-
return {
172-
pos: pair.pos,
173-
path: [...location.path, location.pos, portal.pos],
174-
status: 'Valid',
175-
level: newLocation.level + 1,
176-
}
158+
}
159+
// Inner portal takes you one level deeper
160+
if (visited[newLocation.level + 1] === undefined) {
161+
visited[newLocation.level + 1] = []
162+
}
163+
visited[newLocation.level + 1][
164+
pair.pos.y * maze.width + pair.pos.x
165+
] = true
166+
return {
167+
pos: pair.pos,
168+
path: [...location.path, location.pos, portal.pos],
169+
status: 'Valid',
170+
level: newLocation.level + 1,
177171
}
178172
}
179173
} else {
@@ -203,19 +197,41 @@ export type MazeString = {
203197
width: number
204198
}
205199

200+
/**
201+
* Solves the maze using a depth-first search.
202+
*
203+
* First all portals are detected, then the solver starts at the tile labeled
204+
* with AA, and tries to find a way to ZZ. Portals are used to jump between
205+
* tiles.
206+
*
207+
* In recursive mode, the recursive rules apply to portals:
208+
* When you enter the maze, you are at the outermost level (0); when at the
209+
* outermost level, only the outer labels AA and ZZ function (as the start and
210+
* end, respectively); all other outer labeled tiles are effectively walls. At
211+
* any other level AA and ZZ count as walls, but the other outer labeled tiles
212+
* bring you one level outward.
213+
*/
206214
export const transportingMazeSolver = (
207215
maze: string,
208216
recursive = false,
209217
): Location | undefined => {
218+
// Input is a string with maze rows separated by newlines,
219+
// we assume that all lines are even spaced
210220
const width = maze.indexOf('\n')
221+
// In this solution we operate on one long string
211222
const mazeString: MazeString = {
212223
width,
213224
maze: maze.trimEnd().replace(/\n/g, ''),
214225
}
215-
const portals = findPortals(maze)
226+
227+
// Find all the portals in the maze
228+
const portals = findPortals(mazeString)
229+
230+
// We need to track visited postions for every level, starting at level 0
216231
const visited = [] as Visited
217232
visited[0] = []
218233

234+
// Find the start positing (the tile with the label 'AA')
219235
const startPos = portals.find(({ label }) => label === START) as Portal
220236
const queue = [
221237
{
@@ -227,6 +243,7 @@ export const transportingMazeSolver = (
227243
] as Location[]
228244
visited[0][startPos.pos.y * width + startPos.pos.x] = true
229245

246+
// Now explore all possible directions until all options are exhausted or the target is found
230247
while (queue.length > 0) {
231248
const location = queue.shift() as Location
232249
const e = exploreInDirection(
@@ -272,24 +289,3 @@ export const transportingMazeSolver = (
272289

273290
return undefined
274291
}
275-
276-
const split = (s: string, length: number) => {
277-
const ret = []
278-
for (let offset = 0, strLen = s.length; offset < strLen; offset += length) {
279-
ret.push(s.slice(offset, length + offset))
280-
}
281-
return ret
282-
}
283-
284-
export const drawSolution = (maze: string, finalLocation: Location) => {
285-
const width = maze.indexOf('\n')
286-
const mapAsString = maze.trimEnd().replace(/\n/g, '')
287-
let solution = mapAsString
288-
finalLocation.path.forEach(p => {
289-
solution =
290-
solution.substr(0, p.y * width + p.x) +
291-
'@' +
292-
solution.substr(p.y * width + p.x + 1)
293-
})
294-
console.log(split(solution, width).join('\n'))
295-
}

0 commit comments

Comments
 (0)