package main import ( "bufio" "fmt" "math" "os" "strconv" "strings" "time" ) func main() { start := time.Now() partOne() duration := time.Since(start) partTwo() duration2 := time.Since(start) fmt.Printf("p1: %s, p2: %s\n", duration, duration2-duration) } type tile struct { id int neighbors map[int]int // key matches the edge key, so we can skip existing matches if we want edges map[int]string image map[int]string } // edges are always defined clockwise starting from 0:north // as long as there's only ever one match for an edge, I don't _think_ I need to keep track of piece orientation...? func parseTile(header string, body []string) tile { id, _ := strconv.Atoi(strings.Trim(header, "Tile :")) t := tile{ id: id, neighbors: make(map[int]int, 4), edges: make(map[int]string, 4), image: make(map[int]string, 8), } // make edges and extract picture data... left := []byte{} right := []byte{} for i := range body { left = append(left, body[i][0]) right = append(right, body[i][9]) // image has edges removed; still want 0-indexed for sanity later! if i != 0 && i != 9 { t.image[i-1] = body[i-1][1:9] } } t.edges[0] = body[0] t.edges[1] = string(right) t.edges[2] = body[9] t.edges[3] = string(left) return t } // return is match, inverted func testEdge(one, two string) (bool, bool) { if one == two { return true, false } // go doesn't have a built-in "string reversal" method, so we just check it the hard way // 9 is our magic number for a 10-character string, which we know they all are. // This might not be 100% sound for unicode (byte != rune), but our string is ASCII so it's... fine. for i := range one { if one[i] != two[9-i] { return false, false } } return true, true } // this is wasteful in that it happily checks edges that we've already determined neighbors for, // but it's performant enough that I don't really care to fix it func findPartners(id int, tiles map[int]tile) map[int]tile { currentTile := tiles[id] for tile := range tiles { if tile != id { for edgeID := range tiles[tile].edges { for i := range currentTile.edges { if ok, _ := testEdge(tiles[tile].edges[edgeID], currentTile.edges[i]); ok { tiles[tile].neighbors[edgeID] = tiles[id].id tiles[id].neighbors[i] = tiles[tile].id } } } } } return tiles } // Starting from a corner, we can "walk" through the image by: // stepping to the next tile in a known direction // checking where _that_ tile thinks we came from // then re-orienting ourselves and stepping again. // For example, if we take a corner and go to its 0 neighbor, // and that neighbor thinks our previous tile was 3, // a "straight line" along the edge would be 1. Repeat as necessary until you hit an edge. // Once we hit an edge, return to the start of the row, // step to its 90° neighbor, then walk parallel to the initial edge. // Repeat until we reach the far corner. // Additionally, we need to "line up" the edges, so there's some alignment checking that has to happen. func mergeTiles(corner int, tiles map[int]tile) []string { // just to make the counting a little easier sideLength := int(math.Sqrt(float64(len(tiles)))) result := make([]string, sideLength*8) headOfRow := tiles[corner] currentRowDirection := 0 nextRowDirection := 0 // what an absurd way to "pick" between two keys in a two-key map for i := range headOfRow.neighbors { if currentRowDirection == 0 { currentRowDirection = i } else if nextRowDirection == 0 { nextRowDirection = i } } currentTile := tiles[corner] image := imageFromEdge(currentTile, 0) for row, s := range image { result[row] = result[row] + s } for i := 0; i < sideLength; i = i + 1 { nextID := currentTile.neighbors[currentRowDirection] for e, id := range tiles[nextID].neighbors { if id == currentTile.id { _, invert := testEdge(currentTile.edges[currentRowDirection], tiles[nextID].edges[e]) image := imageFromEdge(tiles[nextID], (e+1)%4) if !invert { for row, s := range image { result[row] = result[row] + s } } else { for row, s := range image { result[7-row] = result[7-row] + s } } currentRowDirection = (e + 2) % 4 // flip 180 around the compass } } currentTile = tiles[nextID] } return result } // returns the image "rotated" so that the provided edge is on "top" (the 0 position) func imageFromEdge(t tile, edge int) map[int]string { if edge == 1 { newImage := make(map[int]string, 8) for i := 0; i < 8; i = i + 1 { b := []byte{t.image[0][7-i], t.image[1][7-i], t.image[2][7-i], t.image[3][7-i], t.image[4][7-i], t.image[5][7-i], t.image[6][7-i], t.image[7][7-i]} newImage[i] = string(b) } return newImage } if edge == 2 { newImage := make(map[int]string, 8) for i := 0; i < 8; i = i + 1 { b := []byte{t.image[7-i][7], t.image[7-i][6], t.image[7-i][5], t.image[7-i][4], t.image[7-i][3], t.image[7-i][2], t.image[7-i][1], t.image[7-i][0]} newImage[i] = string(b) } return newImage } if edge == 3 { newImage := make(map[int]string, 8) for i := 0; i < 8; i = i + 1 { b := []byte{t.image[7][i], t.image[6][i], t.image[5][i], t.image[4][i], t.image[3][i], t.image[2][i], t.image[1][i], t.image[0][i]} newImage[i] = string(b) } return newImage } // edge == 0 does nothing return t.image } func partOne() { f, _ := os.Open("input") reader := bufio.NewReader(f) scanner := bufio.NewScanner(reader) buffer := []string{} tiles := make(map[int]tile) for scanner.Scan() { line := scanner.Text() if line == "" { tile := parseTile(buffer[0], buffer[1:]) tiles[tile.id] = tile buffer = []string{} } else { buffer = append(buffer, line) } } // catch that last tile // weirdly, I'd think the trailing newline in the input would have done this for us tile := parseTile(buffer[0], buffer[1:]) tiles[tile.id] = tile corners := 1 for id := range tiles { tiles = findPartners(id, tiles) if len(tiles[id].neighbors) == 2 { corners = corners * id } } fmt.Println(corners) } func partTwo() { f, _ := os.Open("testinput") reader := bufio.NewReader(f) scanner := bufio.NewScanner(reader) buffer := []string{} tiles := make(map[int]tile) for scanner.Scan() { line := scanner.Text() if line == "" { tile := parseTile(buffer[0], buffer[1:]) tiles[tile.id] = tile buffer = []string{} } else { buffer = append(buffer, line) } } // catch that last tile tile := parseTile(buffer[0], buffer[1:]) tiles[tile.id] = tile corner := 0 for id := range tiles { tiles = findPartners(id, tiles) // grab a corner to start the merge from if len(tiles[id].neighbors) == 2 && corner == 0 { corner = id } } for _, line := range mergeTiles(corner, tiles) { fmt.Println(line) } }