package puz import ( "encoding/json" "errors" "slices" "strconv" "strings" ) type iPuzDimensions struct { Width int `json:"width"` Height int `json:"height"` } type iPuzClue struct { Number int `json:"number"` Clue string `json:"clue"` } type iPuzFile struct { Kind []string `json:"kind"` Title string `json:"title"` Author string `json:"author"` Copyright string `json:"copyright"` Notes string `json:"notes"` Puzzle [][]any `json:"puzzle"` Solution [][]string `json:"solution"` Dimensions iPuzDimensions `json:"dimensions"` Clues map[string][]iPuzClue `json:"clues"` } func unwrapCell(cell any) (any, error) { switch c := cell.(type) { case string: if c == "#" { return "#", nil } else if i, err := strconv.Atoi(c); err == nil { return i, nil } else { return nil, errors.New("invalid string") } case float64: return int(c), nil case map[string]any: if unwrappedCell, ok := c["cell"]; ok { return unwrapCell(unwrappedCell) // recursion babyyyyyy } } return nil, errors.New("unknown type") } func ParseIPuz(file []byte) (*Puzzle, error) { var parsed iPuzFile err := json.Unmarshal(file, &parsed) if err != nil { return nil, err } if len(parsed.Kind) == 0 || !slices.ContainsFunc(parsed.Kind, func(k string) bool { return strings.HasPrefix(k, "http://ipuz.org/crossword") }) { return nil, errors.New("ipuz file is not a crossword") } puzzle := new(Puzzle) puzzle.RebusCells = make(map[int]string) puzzle.Title = parsed.Title puzzle.Author = parsed.Author puzzle.Copyright = parsed.Copyright puzzle.Note = parsed.Notes puzzle.Width = parsed.Dimensions.Width puzzle.Height = parsed.Dimensions.Height puzzle.ClueCount = len(parsed.Clues["Across"]) + len(parsed.Clues["Down"]) clueToCellMap := make(map[int][2]int) state := strings.Builder{} for y, row := range parsed.Puzzle { for x, cell := range row { unwrappedCell, err := unwrapCell(cell) if err != nil { return nil, err } switch c := unwrappedCell.(type) { case string: state.WriteRune('.') case int: clueToCellMap[c] = [2]int{x, y} state.WriteRune('-') default: return nil, errors.New("unexpected type in .ipuz file") } } } puzzle.State = state.String() solution := strings.Builder{} for y, row := range parsed.Solution { for x, cell := range row { if cell == "#" { solution.WriteRune('.') } else if len(cell) > 1 { solution.WriteByte(cell[0]) puzzle.RebusCells[y*puzzle.Width+x] = cell puzzle.HasRebus = true } else if len(cell) == 1 { solution.WriteString(cell) } else { return nil, errors.New("invalid ipuz file") } } } puzzle.Solution = solution.String() puzzle.Clues = make([]*Clue, 0, puzzle.ClueCount) puzzle.Cells = make([][2]*Clue, puzzle.Width*puzzle.Height) for _, clue := range parsed.Clues["Across"] { cellCoords := clueToCellMap[clue.Number] clueCells := make([]int, 0) clueSolution := strings.Builder{} clueStruct := &Clue{ Clue: clue.Clue, Direction: DirectionAcross, Number: clue.Number, } for i := cellCoords[0]; i < puzzle.Width; i++ { cell := parsed.Solution[cellCoords[1]][i] if cell == "#" { break } cellIdx := cellCoords[1]*puzzle.Width + i clueCells = append(clueCells, cellIdx) puzzle.Cells[cellIdx][0] = clueStruct clueSolution.WriteByte(cell[0]) } clueStruct.Solution = clueSolution.String() clueStruct.Cells = clueCells puzzle.Clues = append(puzzle.Clues, clueStruct) } for _, clue := range parsed.Clues["Down"] { cellCoords := clueToCellMap[clue.Number] clueCells := make([]int, 0) clueSolution := strings.Builder{} clueStruct := &Clue{ Clue: clue.Clue, Direction: DirectionDown, Number: clue.Number, } for i := cellCoords[1]; i < puzzle.Height; i++ { cell := parsed.Solution[i][cellCoords[0]] if cell == "#" { break } cellIdx := i*puzzle.Width + cellCoords[0] clueCells = append(clueCells, cellIdx) puzzle.Cells[cellIdx][1] = clueStruct clueSolution.WriteByte(cell[0]) } clueStruct.Solution = clueSolution.String() clueStruct.Cells = clueCells puzzle.Clues = append(puzzle.Clues, clueStruct) } return puzzle, nil }