package main import ( "fmt" "log" "os" "regexp" "git.annabunch.es/annabunches/adventofcode/2020/lib/util" ) const ( MATCH_NONE = iota MATCH_TOP MATCH_BOTTOM MATCH_LEFT MATCH_RIGHT ) type Tile struct { id int data []string rotation int flippedX bool flippedY bool } func NewTile() *Tile { return &Tile{ data: make([]string, 0), rotation: 0, flippedX: false, flippedY: false, } } func (t *Tile) print() { for _, line := range t.data { fmt.Println(line) } fmt.Println() } func (t *Tile) rotate() { newData := make([]string, len(t.data)) for i := len(t.data) - 1; i >= 0; i-- { for j, char := range t.data[i] { newData[j] += string(char) } } t.data = newData t.rotation++ if t.rotation > 3 { t.rotation = 0 } } func (t *Tile) flipX() { newData := make([]string, len(t.data)) for i := 0; i < len(newData); i++ { newData[i] = t.data[len(t.data)-1-i] } t.data = newData t.flippedX = !t.flippedX } func (t *Tile) flipY() { newData := make([]string, len(t.data)) for i := 0; i < len(newData); i++ { for j := 0; j < len(t.data[i]); j++ { newData[i] += string(t.data[i][len(t.data)-1-j]) } } t.data = newData t.flippedY = !t.flippedY } func (t *Tile) reset() { for t.rotation != 0 { t.rotate() } if t.flippedX { t.flipX() } if t.flippedY { t.flipY() } } func parseInput(input []string) map[int]*Tile { tileMap := make(map[int]*Tile) re := regexp.MustCompile("^Tile ([0-9]+):$") id := 0 tile := NewTile() for _, line := range input { if re.MatchString(line) { id = util.MustAtoi(re.FindStringSubmatch(line)[1]) tile.id = id continue } if line == "" { tileMap[id] = tile tile = NewTile() id = 0 continue } tile.data = append(tile.data, line) } // if needed, add the last one (might be missing our final blank line) if id != 0 { tileMap[id] = tile } return tileMap } func matchTiles(oldTile, newTile *Tile) int { for i := 0; i < 4; i++ { check := subMatchTiles(oldTile, newTile) if check != MATCH_NONE { return check } newTile.rotate() } newTile.reset() newTile.flipX() for i := 0; i < 4; i++ { check := subMatchTiles(oldTile, newTile) if check != MATCH_NONE { return check } newTile.rotate() } newTile.reset() newTile.flipY() for i := 0; i < 4; i++ { check := subMatchTiles(oldTile, newTile) if check != MATCH_NONE { return check } newTile.rotate() } newTile.reset() newTile.flipX() newTile.flipY() for i := 0; i < 4; i++ { check := subMatchTiles(oldTile, newTile) if check != MATCH_NONE { return check } newTile.rotate() } return MATCH_NONE } func subMatchTiles(oldTile, newTile *Tile) int { // check top if oldTile.data[0] == newTile.data[0] { return MATCH_TOP } // check bottom if oldTile.data[len(oldTile.data)-1] == newTile.data[len(newTile.data)-1] { return MATCH_BOTTOM } // check left match := true for i := 0; i < len(oldTile.data); i++ { if oldTile.data[i][0] != newTile.data[i][0] { match = false break } } if match { return MATCH_LEFT } // check right match = true for i := 0; i < len(oldTile.data); i++ { if oldTile.data[i][len(oldTile.data)-1] != newTile.data[i][len(newTile.data)-1] { match = false break } } if match { return MATCH_RIGHT } return MATCH_NONE } func arrangeTiles(tiles map[int]*Tile) map[[2]int]*Tile { grid := make(map[[2]int]*Tile) looseTiles := make([]*Tile, 0) for _, v := range tiles { looseTiles = append(looseTiles, v) } // arbitrarily place a first tile grid[[2]int{0, 0}] = looseTiles[0] looseTiles = looseTiles[1:] for len(looseTiles) > 0 { for coord, tile := range grid { if _, ok := grid[[2]int{coord[0] + 1, coord[1]}]; ok { if _, ok := grid[[2]int{coord[0] - 1, coord[1]}]; ok { if _, ok := grid[[2]int{coord[0], coord[1] + 1}]; ok { if _, ok := grid[[2]int{coord[0], coord[1] - 1}]; ok { continue } } } } for i, loose := range looseTiles { matched := matchTiles(tile, loose) // On a match, flip appropriately and set coords // check for already present tiles as well - skip if that's the case var newCoords [2]int willFlipX := true switch matched { case MATCH_TOP: newCoords = [2]int{coord[0], coord[1] + 1} case MATCH_BOTTOM: newCoords = [2]int{coord[0], coord[1] - 1} case MATCH_LEFT: newCoords = [2]int{coord[0] - 1, coord[1]} willFlipX = false case MATCH_RIGHT: newCoords = [2]int{coord[0] + 1, coord[1]} willFlipX = false } if matched != MATCH_NONE { if _, ok := grid[newCoords]; ok { continue } if willFlipX { loose.flipX() } else { loose.flipY() } grid[newCoords] = loose if i == len(looseTiles)-1 { looseTiles = looseTiles[:i] } else { looseTiles = append(looseTiles[:i], looseTiles[i+1:]...) } break // to avoid sequencing issues } } } } return grid } func findCorners(grid map[[2]int]*Tile) []*Tile { corners := make([]*Tile, 0) // min and max coord values minX, minY, maxX, maxY := findLimits(grid) corners = append(corners, grid[[2]int{minX, minY}]) corners = append(corners, grid[[2]int{maxX, minY}]) corners = append(corners, grid[[2]int{minX, maxY}]) corners = append(corners, grid[[2]int{maxX, maxY}]) return corners } func findLimits(grid map[[2]int]*Tile) (int, int, int, int) { var minX, minY, maxX, maxY int for coord, _ := range grid { if coord[0] > maxX { maxX = coord[0] } if coord[0] < minX { minX = coord[0] } if coord[1] > maxY { maxY = coord[1] } if coord[1] < minY { minY = coord[1] } } return minX, minY, maxX, maxY } func stripBorder(tile *Tile) []string { ret := make([]string, 0) for i := 1; i < len(tile.data)-1; i++ { ret = append(ret, tile.data[i][1:len(tile.data)-1]) } return ret } func combineTiles(grid map[[2]int]*Tile) *Tile { bigTile := NewTile() minX, minY, maxX, maxY := findLimits(grid) for j := maxY; j >= minY; j-- { row := make([]string, 8) for i := minX; i <= maxX; i++ { var tile *Tile tile, ok := grid[[2]int{i, j}] if !ok { log.Panicf("Couldn't find tile: %d, %d", i, j) } data := stripBorder(tile) for i, line := range data { row[i] = row[i] + line } } bigTile.data = append(bigTile.data, row...) } return bigTile } var monsterPattern = []string{ " # ", "# ## ## ###", " # # # # # # ", } func subFilterMonsters(image *Tile) int { numMonsters := 0 for i := 0; i < len(image.data)-2; i++ { for j := 0; j < len(image.data[0])-19; j++ { if monsterCheck(image, i, j) { removeMonster(image, i, j) numMonsters++ } } } return numMonsters } func removeMonster(image *Tile, i, j int) { for y := 0; y < 3; y++ { for x := 0; x < 20; x++ { if monsterPattern[y][x] == '#' { if j+x == len(image.data[i+y])-1 { image.data[i+y] = image.data[i+y][:j+x] + "O" } else { image.data[i+y] = image.data[i+y][:j+x] + "O" + image.data[i+y][j+x+1:] } } } } } func monsterCheck(image *Tile, i, j int) bool { data := image.data for y := 0; y < 3; y++ { for x := 0; x < 20; x++ { if data[i+y][j+x] == '.' && monsterPattern[y][x] == '#' { return false } } } return true } func filterMonsters(image *Tile) int { numMonsters := 0 for i := 0; i < 4; i++ { numMonsters = subFilterMonsters(image) if numMonsters > 0 { return numMonsters } image.rotate() } image.reset() image.flipX() for i := 0; i < 4; i++ { numMonsters = subFilterMonsters(image) if numMonsters > 0 { return numMonsters } } image.reset() image.flipY() for i := 0; i < 4; i++ { numMonsters = subFilterMonsters(image) if numMonsters > 0 { return numMonsters } } image.reset() image.flipX() image.flipY() for i := 0; i < 4; i++ { numMonsters = subFilterMonsters(image) if numMonsters > 0 { return numMonsters } } return numMonsters } func main() { step := os.Args[1] values := util.InputParserStrings(os.Args[2]) tileMap := parseInput(values) grid := arrangeTiles(tileMap) switch step { case "1": corners := findCorners(grid) product := 1 for _, tile := range corners { product *= tile.id } fmt.Println(product) case "2": image := combineTiles(grid) monsters := filterMonsters(image) if monsters == 0 { log.Panicf("Found no monsters") } count := 0 for _, line := range image.data { for _, char := range line { if char == '#' { count++ } } } fmt.Println(count) } }