From 344be871dbafa4dc83f127c162e9ccb940c1d839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Mon, 11 Dec 2023 15:03:01 +0100 Subject: [PATCH] Added solution for "Day 10: Pipe Maze", part 2 --- solvers/UPipeMaze.pas | 339 ++++++++++++++++++++++++----------- tests/UPipeMazeTestCases.pas | 142 ++++++++++++++- 2 files changed, 375 insertions(+), 106 deletions(-) diff --git a/solvers/UPipeMaze.pas b/solvers/UPipeMaze.pas index 9e73d2b..002184a 100644 --- a/solvers/UPipeMaze.pas +++ b/solvers/UPipeMaze.pas @@ -22,24 +22,52 @@ unit UPipeMaze; interface uses - Classes, SysUtils, USolver; + Classes, SysUtils, Generics.Collections, USolver; const CStartChar = 'S'; type - TDirection = (drEast, drSouth, drWest, drNorth); + TPointArray = array of TPoint; + + { TStepMapping } + + TStepMapping = class + private + FStartDirection, FNewDirection: TPoint; + FMapChar: Char; + FLeftSide, FRightSide: TPointArray; + public + property StartDirection: TPoint read FStartDirection; + property NewDirection: TPoint read FNewDirection; + property LeftSide: TPointArray read FLeftSide; + property RightSide: TPointArray read FRightSide; + constructor Create(const AStartDirection, ANewDirection: TPoint; const AMapChar: Char; const ALeftSide, + ARightSide: TPointArray); + constructor CreateReverse(constref AOriginal: TStepMapping); + function TryStep(const AStartDirection: TPoint; const AMapChar: Char): Boolean; + end; + + TStepMappings = specialize TObjectList; { TPipeMaze } TPipeMaze = class(TSolver) private - FWidth, FHeight: Integer; - FStrings: TStringList; + FStepMappings: TStepMappings; + FMap, FEnclosureMap: TStringList; + procedure InitStepMappings; procedure ParseMaze; function FindStart: TPoint; - procedure DoFirstStep(var APosition: TPoint; out OStartDirection: TDirection); - procedure DoStep(var APosition: TPoint; var ADirection: TDirection); + procedure DoFirstStep(var APosition: TPoint; out OStartDirection: TPoint); + procedure DoStep(var APosition: TPoint; var ADirection: TPoint); + procedure CountEnclosureInside; + function TryCountEnclosureSide(const AChar: Char; out OCount: Int64): Boolean; + procedure UpdateEnclosureMap(const APosition: TPoint; constref AStepMapping: TStepMapping); + function CheckMapBounds(const APosition: TPoint): Boolean; + function GetMapChar(const APosition: TPoint): Char; + function GetEnclosureMapChar(const APosition: TPoint): Char; + procedure SetEnclosureMapChar(const APosition: TPoint; const AChar: Char); public constructor Create; destructor Destroy; override; @@ -51,15 +79,68 @@ type implementation +{ TStepMapping } + +constructor TStepMapping.Create(const AStartDirection, ANewDirection: TPoint; const AMapChar: Char; const ALeftSide, + ARightSide: TPointArray); +begin + FStartDirection := AStartDirection; + FNewDirection := ANewDirection; + FMapChar := AMapChar; + FLeftSide := ALeftSide; + FRightSide := ARightSide; +end; + +constructor TStepMapping.CreateReverse(constref AOriginal: TStepMapping); +begin + FStartDirection := Point(-AOriginal.FNewDirection.X, -AOriginal.FNewDirection.Y); + FNewDirection := Point(-AOriginal.FStartDirection.X, -AOriginal.FStartDirection.Y); + FMapChar := AOriginal.FMapChar; + FLeftSide := AOriginal.FRightSide; + FRightSide := AOriginal.FLeftSide; +end; + +function TStepMapping.TryStep(const AStartDirection: TPoint; const AMapChar: Char): Boolean; +begin + Result := (FStartDirection = AStartDirection) and (FMapChar = AMapChar); +end; + { TPipeMaze } +procedure TPipeMaze.InitStepMappings; +var + i: Integer; +begin + FStepMappings.Add(TStepMapping.Create(Point(0, 1), Point(0, 1), '|', + TPointArray.Create(Point(1, 0)), + TPointArray.Create(Point(-1, 0)))); + FStepMappings.Add(TStepMapping.Create(Point(1, 0), Point(1, 0), '-', + TPointArray.Create(Point(0, -1)), + TPointArray.Create(Point(0, 1)))); + FStepMappings.Add(TStepMapping.Create(Point(-1, 0), Point(0, -1), 'L', + TPointArray.Create(Point(0, 1), Point(-1, 1), Point(-1, 0)), + [])); + FStepMappings.Add(TStepMapping.Create(Point(0, 1), Point(-1, 0), 'J', + TPointArray.Create(Point(1, 0), Point(1, 1), Point(0, 1)), + [])); + FStepMappings.Add(TStepMapping.Create(Point(1, 0), Point(0, 1), '7', + TPointArray.Create(Point(0, -1), Point(1, -1), Point(1, 0)), + [])); + FStepMappings.Add(TStepMapping.Create(Point(0, -1), Point(1, 0), 'F', + TPointArray.Create(Point(-1, 0), Point(-1, -1), Point(0, -1)), + [])); + + // Adds reverse step mappings. + for i := 0 to FStepMappings.Count - 1 do + FStepMappings.Add(TStepMapping.CreateReverse(FStepMappings[i])); +end; + procedure TPipeMaze.ParseMaze; var - start, current: TPoint; - nextDirection: TDirection; + start, current, nextDirection: TPoint; begin - FHeight := FStrings.Count; - FWidth := Length(FStrings[0]); + FEnclosureMap := TStringList.Create; + FEnclosureMap.AddStrings(FMap); start := FindStart; current := start; @@ -73,6 +154,9 @@ begin DoStep(current, nextDirection); Inc(FPart1); until current = start; + + CountEnclosureInside; + FEnclosureMap.Free; end; function TPipeMaze.FindStart: TPoint; @@ -82,12 +166,12 @@ var begin foundStart := False; i := 0; - while not foundStart and (i < FStrings.Count) do + while not foundStart and (i < FMap.Count) do begin j := 1; - while not foundStart and (j <= Length(FStrings[i])) do + while not foundStart and (j <= Length(FMap[i])) do begin - if FStrings[i][j] = CStartChar then + if FMap[i][j] = CStartChar then begin foundStart := True; Result.X := j; @@ -99,126 +183,171 @@ begin end; end; -procedure TPipeMaze.DoFirstStep(var APosition: TPoint; out OStartDirection: TDirection); +procedure TPipeMaze.DoFirstStep(var APosition: TPoint; out OStartDirection: TPoint); +var + stepMapping: TStepMapping; + step: TPoint; begin - // Checks going East. - if APosition.X < FWidth then + for stepMapping in FStepMappings do begin - if FStrings[APosition.Y][APosition.X + 1] = '-' then + step := APosition + stepMapping.StartDirection; + if CheckMapBounds(step) and stepMapping.TryStep(stepMapping.StartDirection, GetMapChar(step)) then begin - Inc(APosition.X); - OStartDirection := drEast; - Exit; - end - else if FStrings[APosition.Y][APosition.X + 1] = 'J' then - begin - Inc(APosition.X); - OStartDirection := drNorth; - Exit; - end - else if FStrings[APosition.Y][APosition.X + 1] = '7' then - begin - Inc(APosition.X); - OStartDirection := drSouth; - Exit; - end; - end; + UpdateEnclosureMap(APosition, stepMapping); + UpdateEnclosureMap(step, stepMapping); - // Checks going South. - if APosition.Y < FHeight - 1 then - begin - if FStrings[APosition.Y + 1][APosition.X] = '|' then - begin - Inc(APosition.Y); - OStartDirection := drSouth; - Exit; - end - else if FStrings[APosition.Y + 1][APosition.X] = 'L' then - begin - Inc(APosition.Y); - OStartDirection := drEast; - Exit; - end - else if FStrings[APosition.Y + 1][APosition.X] = 'J' then - begin - Inc(APosition.Y); - OStartDirection := drWest; - Exit; - end; - end; - - // Checks going West. - if APosition.X > 1 then - begin - if FStrings[APosition.Y][APosition.X - 1] = '-' then - begin - Dec(APosition.X); - OStartDirection := drWest; - Exit; - end - else if FStrings[APosition.Y][APosition.X - 1] = 'L' then - begin - Dec(APosition.X); - OStartDirection := drNorth; - Exit; - end - else if FStrings[APosition.Y][APosition.X - 1] = 'F' then - begin - Dec(APosition.X); - OStartDirection := drSouth; + APosition := step; + OStartDirection := stepMapping.NewDirection; Exit; end; end; end; -procedure TPipeMaze.DoStep(var APosition: TPoint; var ADirection: TDirection); +procedure TPipeMaze.DoStep(var APosition: TPoint; var ADirection: TPoint); +var + stepMapping: TStepMapping; begin - case ADirection of - drEast: begin - Inc(APosition.X); - case FStrings[APosition.Y][APosition.X] of - 'J': ADirection := drNorth; - '7': ADirection := drSouth; - end; + APosition := APosition + ADirection; + for stepMapping in FStepMappings do + begin + if stepMapping.TryStep(ADirection, GetMapChar(APosition)) then + begin + ADirection := stepMapping.NewDirection; + UpdateEnclosureMap(APosition, stepMapping); + Exit; end; - drSouth: begin - Inc(APosition.Y); - case FStrings[APosition.Y][APosition.X] of - 'L': ADirection := drEast; - 'J': ADirection := drWest; + end; +end; + +procedure TPipeMaze.CountEnclosureInside; +begin + if not TryCountEnclosureSide('l', FPart2) then + TryCountEnclosureSide('r', FPart2); +end; + +function TPipeMaze.TryCountEnclosureSide(const AChar: Char; out OCount: Int64): Boolean; +var + directions: TPointArray; + stack: specialize TStack; + i, j: Integer; + position, direction, neighbor: TPoint; + c: Char; +begin + directions := TPointArray.Create(Point(0, -1), Point(-1, 0), Point(0, 1), Point(1, 0)); + stack := specialize TStack.Create; + + OCount := 0; + Result := True; + + for i := 0 to FEnclosureMap.Count - 1 do + begin + for j := 1 to Length(FEnclosureMap[i]) do + begin + position := Point(j, i); + if GetEnclosureMapChar(position) = AChar then + begin + stack.Push(position); + SetEnclosureMapChar(position, '%'); + Inc(OCount); end; - end; - drWest: begin - Dec(APosition.X); - case FStrings[APosition.Y][APosition.X] of - 'L': ADirection := drNorth; - 'F': ADirection := drSouth; - end; - end; - drNorth: begin - Dec(APosition.Y); - case FStrings[APosition.Y][APosition.X] of - '7': ADirection := drWest; - 'F': ADirection := drEast; + + while stack.Count > 0 do + begin + position := stack.Pop; + + for direction in directions do + begin + if CheckMapBounds(position + direction) then + begin + // Checks the neighboring position. + neighbor := position + direction; + c := GetEnclosureMapChar(neighbor); + if (c <> '%') and (c <> '#') then + begin + stack.Push(neighbor); + SetEnclosureMapChar(neighbor, '%'); + Inc(OCount); + end; + end else + begin + // Exits after hitting an edge of the map. + Result := False; + stack.Free; + Exit; + end; + end; end; end; end; + + stack.Free; +end; + +procedure TPipeMaze.UpdateEnclosureMap(const APosition: TPoint; constref AStepMapping: TStepMapping); +var + offset, sidePosition: TPoint; + side: TPointArray; + c: Char; + i: Integer; +begin + SetEnclosureMapChar(APosition, '#'); + side := AStepMapping.LeftSide; + c := 'l'; + for i := 1 to 2 do + begin + for offset in side do + begin + sidePosition := APosition + offset; + if CheckMapBounds(sidePosition) and (GetEnclosureMapChar(sidePosition) <> '#') then + SetEnclosureMapChar(sidePosition, c); + end; + side := AStepMapping.RightSide; + c := 'r'; + end; +end; + +function TPipeMaze.CheckMapBounds(const APosition: TPoint): Boolean; +begin + Result := (0 <= APosition.Y) and (APosition.Y < FMap.Count) + and (0 < APosition.X) and (APosition.X <= Length(FMap[0])); +end; + +function TPipeMaze.GetMapChar(const APosition: TPoint): Char; +begin + Result := FMap[APosition.Y][APosition.X]; +end; + +function TPipeMaze.GetEnclosureMapChar(const APosition: TPoint): Char; +begin + Result := FEnclosureMap[APosition.Y][APosition.X]; +end; + +procedure TPipeMaze.SetEnclosureMapChar(const APosition: TPoint; const AChar: Char); +var + s: string; +begin + s := FEnclosureMap[APosition.Y]; + s[APosition.X] := AChar; + FEnclosureMap[APosition.Y] := s; end; constructor TPipeMaze.Create; begin - FStrings := TStringList.Create; + FStepMappings := TStepMappings.Create; + InitStepMappings; + FMap := TStringList.Create; end; destructor TPipeMaze.Destroy; begin - FStrings.Free; + FStepMappings.Free; + FMap.Free; inherited Destroy; end; procedure TPipeMaze.ProcessDataLine(const ALine: string); begin - FStrings.Add(ALine); + FMap.Add(ALine); end; procedure TPipeMaze.Finish; diff --git a/tests/UPipeMazeTestCases.pas b/tests/UPipeMazeTestCases.pas index 25c77ff..d4afcf9 100644 --- a/tests/UPipeMazeTestCases.pas +++ b/tests/UPipeMazeTestCases.pas @@ -90,6 +90,66 @@ type procedure TestPart1; end; + { TExample5PipeMaze } + + TExample5PipeMaze = class(TPipeMaze) + function GetDataFileName: string; override; + end; + + { TPipeMazeExample5TestCase } + + TPipeMazeExample5TestCase = class(TExampleEngineBaseTest) + protected + function CreateSolver: ISolver; override; + published + procedure TestPart2; + end; + + { TExample6PipeMaze } + + TExample6PipeMaze = class(TPipeMaze) + function GetDataFileName: string; override; + end; + + { TPipeMazeExample6TestCase } + + TPipeMazeExample6TestCase = class(TExampleEngineBaseTest) + protected + function CreateSolver: ISolver; override; + published + procedure TestPart2; + end; + + { TExample7PipeMaze } + + TExample7PipeMaze = class(TPipeMaze) + function GetDataFileName: string; override; + end; + + { TPipeMazeExample7TestCase } + + TPipeMazeExample7TestCase = class(TExampleEngineBaseTest) + protected + function CreateSolver: ISolver; override; + published + procedure TestPart2; + end; + + { TExample8PipeMaze } + + TExample8PipeMaze = class(TPipeMaze) + function GetDataFileName: string; override; + end; + + { TPipeMazeExample8TestCase } + + TPipeMazeExample8TestCase = class(TExampleEngineBaseTest) + protected + function CreateSolver: ISolver; override; + published + procedure TestPart2; + end; + implementation { TPipeMazeFullDataTestCase } @@ -106,7 +166,7 @@ end; procedure TPipeMazeFullDataTestCase.TestPart2; begin - AssertEquals(-1, FSolver.GetResultPart2); + AssertEquals(355, FSolver.GetResultPart2); end; { TPipeMazeExampleTestCase } @@ -178,6 +238,82 @@ begin AssertEquals(8, FSolver.GetResultPart1); end; +{ TExample5PipeMaze } + +function TExample5PipeMaze.GetDataFileName: string; +begin + Result := 'pipe_maze5.txt'; +end; + +{ TPipeMazeExample5TestCase } + +function TPipeMazeExample5TestCase.CreateSolver: ISolver; +begin + Result := TExample5PipeMaze.Create; +end; + +procedure TPipeMazeExample5TestCase.TestPart2; +begin + AssertEquals(4, FSolver.GetResultPart2); +end; + +{ TExample6PipeMaze } + +function TExample6PipeMaze.GetDataFileName: string; +begin + Result := 'pipe_maze6.txt'; +end; + +{ TPipeMazeExample6TestCase } + +function TPipeMazeExample6TestCase.CreateSolver: ISolver; +begin + Result := TExample6PipeMaze.Create; +end; + +procedure TPipeMazeExample6TestCase.TestPart2; +begin + AssertEquals(4, FSolver.GetResultPart2); +end; + +{ TExample7PipeMaze } + +function TExample7PipeMaze.GetDataFileName: string; +begin + Result := 'pipe_maze7.txt'; +end; + +{ TPipeMazeExample7TestCase } + +function TPipeMazeExample7TestCase.CreateSolver: ISolver; +begin + Result := TExample7PipeMaze.Create; +end; + +procedure TPipeMazeExample7TestCase.TestPart2; +begin + AssertEquals(8, FSolver.GetResultPart2); +end; + +{ TExample8PipeMaze } + +function TExample8PipeMaze.GetDataFileName: string; +begin + Result := 'pipe_maze8.txt'; +end; + +{ TPipeMazeExample8TestCase } + +function TPipeMazeExample8TestCase.CreateSolver: ISolver; +begin + Result := TExample8PipeMaze.Create; +end; + +procedure TPipeMazeExample8TestCase.TestPart2; +begin + AssertEquals(10, FSolver.GetResultPart2); +end; + initialization RegisterTest(TPipeMazeFullDataTestCase); @@ -185,4 +321,8 @@ initialization RegisterTest(TPipeMazeExample2TestCase); RegisterTest(TPipeMazeExample3TestCase); RegisterTest(TPipeMazeExample4TestCase); + RegisterTest(TPipeMazeExample5TestCase); + RegisterTest(TPipeMazeExample6TestCase); + RegisterTest(TPipeMazeExample7TestCase); + RegisterTest(TPipeMazeExample8TestCase); end.