Skip to content

Commit

Permalink
Apply colors to dots, refactor Taiji decoder
Browse files Browse the repository at this point in the history
The decoder now uses a two-pass system like the encoder, decompressing
array fill codes first before parsing the string. This removes edge case
checks that were initially needed for filling, and it helps ensure that
color based mechanics work as intended.
  • Loading branch information
alicerunsonfedora committed Jan 19, 2025
1 parent 5f66f17 commit 891cce5
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 49 deletions.
76 changes: 31 additions & 45 deletions Sources/PuzzleKit/Taiji/PKTaijiDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,23 @@ enum PKTaijiDecoder {

// swiftlint:disable:next function_body_length cyclomatic_complexity large_tuple
static func decode(from source: String) throws(DecodeError) -> (Int, [PKTaijiTile], PKTaijiMechanics) {
var sourceToParse = source
var tiles = [PKTaijiTile]()
var mechanics: PKTaijiMechanics = []
var boardWidth = 0
var state = State.initial
var widthString = ""
var filledSymbolicTile = false
var extendedAttrsChars = 0

for (charIndex, character) in source.enumerated() {
while sourceToParse.contains(Constants.fillEmpty) {
sourceToParse = try decompress(sourceToParse, character: Constants.fillEmpty)
}

while sourceToParse.contains(Constants.fillFixed) {
sourceToParse = try decompress(sourceToParse, character: Constants.fillFixed)
}

for (charIndex, character) in sourceToParse.enumerated() {
switch (character, state) {
case let (char, .initial) where Constants.digits.contains(char),
let (char, .getWidth) where Constants.digits.contains(char):
Expand All @@ -61,31 +69,6 @@ enum PKTaijiDecoder {
}
boardWidth = convertedNumber
state = .scanForTile
case (Constants.fillEmpty, .scanForTile):
state = .prefillArray(invisible: false)

// NOTE: Check for the changes here, because doing so after skipping the attributes is too much to
// handle.
if let lastTile = tiles.last, lastTile.state != .normal, lastTile.filled {
filledSymbolicTile = false
}
case (Constants.fillFixed, .scanForTile):
state = .prefillArray(invisible: true)
if let lastTile = tiles.last, lastTile.state != .invisible {
filledSymbolicTile = false
}
case let (char, .prefillArray(invisible)):
guard let index = Constants.upperAlphabet.firstIndex(of: char) else {
throw .invalidPrefillWidth
}
var amount = Constants.upperAlphabet.distance(to: index) + 1
if filledSymbolicTile { amount -= 1 }
let tileState = invisible ? PKTaijiTileState.invisible : PKTaijiTileState.normal
for _ in 1...amount {
tiles.append(PKTaijiTile(state: tileState))
}
state = .scanForTile
filledSymbolicTile = false
case let (char, .scanForTile) where Constants.dots.contains(char):
guard let index = Constants.dots.firstIndex(of: char) else {
throw .invalidConstantIndex(index: nil, constant: Constants.dots)
Expand All @@ -96,55 +79,43 @@ enum PKTaijiDecoder {
value = abs(9 - value)
}
var tile = PKTaijiTile.symbolic(.dot(value: value, additive: additive))
if let (extendedAttrs, readChars) = Self.getAttributes(after: charIndex, in: source) {
if let (extendedAttrs, readChars) = Self.getAttributes(after: charIndex, in: sourceToParse) {
tile = tile.applying(attributes: extendedAttrs)
state = .readExtendedAttributes
extendedAttrsChars = readChars
}
tiles.append(tile)
if tile.state != .fixed {
filledSymbolicTile = true
}
mechanics.insert(.dot)
case let (char, .scanForTile) where Constants.flowers.contains(char):
guard let index = Constants.flowers.firstIndex(of: char) else {
throw .invalidConstantIndex(index: nil, constant: Constants.dots)
}
let value = Constants.flowers.distance(to: index)
var tile = PKTaijiTile.symbolic(.flower(petals: value))
if let (extendedAttrs, readChars) = Self.getAttributes(after: charIndex, in: source) {
if let (extendedAttrs, readChars) = Self.getAttributes(after: charIndex, in: sourceToParse) {
tile = tile.applying(attributes: extendedAttrs)
state = .readExtendedAttributes
extendedAttrsChars = readChars
}
tiles.append(tile)
mechanics.insert(.flower)
if tile.state != .fixed {
filledSymbolicTile = true
}
case (Constants.diamond, .scanForTile):
var tile = PKTaijiTile.symbolic(.diamond)
if let (extendedAttrs, readChars) = Self.getAttributes(after: charIndex, in: source) {
if let (extendedAttrs, readChars) = Self.getAttributes(after: charIndex, in: sourceToParse) {
tile = tile.applying(attributes: extendedAttrs)
state = .readExtendedAttributes
extendedAttrsChars = readChars
}
tiles.append(tile)
if tile.state != .fixed {
filledSymbolicTile = true
}
mechanics.insert(.diamond)
case (Constants.dash, .scanForTile), (Constants.slash, .scanForTile):
var tile = PKTaijiTile.symbolic(.slashdash(rotates: character == Constants.slash))
if let (extendedAttrs, readChars) = Self.getAttributes(after: charIndex, in: source) {
if let (extendedAttrs, readChars) = Self.getAttributes(after: charIndex, in: sourceToParse) {
tile = tile.applying(attributes: extendedAttrs)
state = .readExtendedAttributes
extendedAttrsChars = readChars
}
tiles.append(tile)
if tile.state != .fixed {
filledSymbolicTile = true
}
mechanics.insert(.slashdash)
case let (char, .readExtendedAttributes):
extendedAttrsChars -= 1
Expand All @@ -155,15 +126,13 @@ enum PKTaijiDecoder {
let extendedAttrs = Constants.specialDigits + Constants.colors
if !extendedAttrs.contains(char) {
state = .scanForTile
filledSymbolicTile = false
extendedAttrsChars = 0
}
case let (char, .scanForTile) where Constants.specialDigits.contains(char):
let (filled, state) = Self.parseSpecialDigit(char)
var tile = PKTaijiTile(state: state)
tile.filled = filled
tiles.append(tile)
filledSymbolicTile = false
default:
break
}
Expand All @@ -172,6 +141,23 @@ enum PKTaijiDecoder {
return (boardWidth, tiles, mechanics)
}

private static func decompress(_ sourceToParse: String, character: Character) throws(DecodeError) -> String {
guard let plusIdx = sourceToParse.firstIndex(of: character) else { return sourceToParse }
var newSource = sourceToParse
let char = newSource[newSource.index(after: plusIdx)]
newSource.remove(at: newSource.index(after: plusIdx))
guard let charCode = char.asciiValue else { return sourceToParse }
let count = Int(charCode) - 64
guard (1...26).contains(count) else {
throw .invalidPrefillWidth
}
newSource.insert(
contentsOf: String(repeating: "0", count: count),
at: newSource.index(after: plusIdx))
newSource.remove(at: plusIdx)
return newSource
}

private static func parseSpecialDigit(_ digit: Character) -> (Bool, PKTaijiTileState) {
return switch digit {
case "0", "2":
Expand Down
24 changes: 20 additions & 4 deletions Sources/PuzzleKit/Taiji/PKTaijiPuzzleValidator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -175,16 +175,32 @@ public class PKTaijiPuzzleValidator {
let plusDots = symbolLUT.dotsPositive.filter { dot in
guard let dotRegion = regionMap[dot] else { return false }
return dotRegion == region
}.reduce(0) { accum, dotCoord in
sumDotTile(accum: accum, coordinate: dotCoord, additive: true)
}
let minusDots = symbolLUT.dotsNegative.filter { dot in
guard let dotRegion = regionMap[dot] else { return false }
return dotRegion == region
}.reduce(0) { accum, dotCoord in
}

if !options.contains(.ignoresColor) {
let allDots = plusDots + minusDots
guard !allDots.isEmpty else { return true }
let expectedColor = puzzle.tile(at: allDots[0])?.color
let allMatchColor = allDots.allSatisfy { coord in
let tile = puzzle.tile(at: coord)
return tile?.color == expectedColor
}
if !allMatchColor { return false }
}

let plusDotSum = plusDots.reduce(0) { accum, dotCoord in
sumDotTile(accum: accum, coordinate: dotCoord, additive: true)
}

let minusDotSum = minusDots.reduce(0) { accum, dotCoord in
sumDotTile(accum: accum, coordinate: dotCoord, additive: false)
}
let expectedSize = plusDots - minusDots

let expectedSize = plusDotSum - minusDotSum
if expectedSize == 0 { return true }

let regionData = regions[region]
Expand Down
17 changes: 17 additions & 0 deletions Tests/PuzzleKitTests/Taiji/PKTaijiPuzzleDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,5 +203,22 @@ struct TaijiPuzzleDecoderTests {
let emptyTiles = puzzle.tiles.count { tile in tile == .empty() }
#expect(emptyTiles == 42)
}

@Test("Complex Puzzle Decodes - Special Case 4")
func decoderDecodesComplexPuzzleSpecialCase_4() async throws {
let puzzle = try PKTaijiPuzzle(decoding: "3:Br2+B202+BBy2")
#expect(puzzle.mechanics == [.dot])

var redDot = PKTaijiTile(state: .normal, symbol: .dot(value: 2, additive: true), color: .red)
redDot.filled = true

var yellowDot = PKTaijiTile(state: .normal, symbol: .dot(value: 2, additive: true), color: .yellow)
yellowDot.filled = true

#expect(puzzle.tiles.first == redDot)
#expect(puzzle.tiles.last == yellowDot)
#expect(puzzle.tiles.count { $0.filled == false && $0.state == .normal } == 5)
#expect(puzzle.tiles.count == 9)
}
}

28 changes: 28 additions & 0 deletions Tests/PuzzleKitTests/Taiji/PKTaijiPuzzleValidator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ struct PKTaijiPuzzleValidatorTests {
#expect(validator.regionMap[.init(x: 6, y: 4)] == 5)
}

@Test("Region map constructs - special case A")
func regionMapConstructsSpecialCase1() async throws {
let puzzle = try PKTaijiPuzzle(decoding: "3:Br2+B202+BBy2")
let validator = PKTaijiPuzzleValidator(puzzle: puzzle)

#expect(puzzle.tiles.count == 9)
#expect(validator.regions.count == 3)
}

// MARK: - Flower constraints

@Test("Flower constraints", arguments: [
Expand Down Expand Up @@ -111,6 +120,25 @@ struct PKTaijiPuzzleValidatorTests {
}
}

@Test("Dots constraints (Base game, standalone)", arguments: [
("3:Br2+B202+BBy2", nil),
("3:Ar20Jy2202Jr20Ay2", nil),
("3:Cr2+B222+BBy2", VError.invalidRegionSize(1, 0)),
("3:Ar20Jr2202Jy20Ay2", VError.invalidRegionSize(1, 0)),
])
func validationDotConstraintsBaseGame(code: String, err: PKTaijiPuzzleValidatorError?) async throws {
let puzzle = try PKTaijiPuzzle(decoding: code)
if let err {
#expect(throws: err.self) {
try puzzle.validate(options: .baseGame).get()
}
} else {
#expect(throws: Never.self) {
try puzzle.validate(options: .baseGame).get()
}
}
}

// MARK: - Slashdash constraints

@Test("Slashdash constraints (WTT)")
Expand Down

0 comments on commit 891cce5

Please sign in to comment.