crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

GridSilhouetteTests.swift (5842B)


      1 import Foundation
      2 import Testing
      3 
      4 @testable import Crossmate
      5 
      6 @Suite("Grid silhouette codec")
      7 struct GridSilhouetteTests {
      8 
      9     @Test("encodes a small symmetric grid to a stable segment")
     10     func encodesSymmetricSegment() {
     11         // 3×3 with only the centre blocked. Symmetric, so the first ⌈9/2⌉ = 5
     12         // cells (00001) pack into one byte 0x08 → base64url "CA"; side 3 → "3".
     13         var blocks = [Bool](repeating: false, count: 9)
     14         blocks[4] = true
     15         #expect(GridSilhouette.encode(side: 3, blocks: blocks) == "s3CA")
     16     }
     17 
     18     @Test("tags an asymmetric grid for a full dump")
     19     func tagsAsymmetricGrid() {
     20         // A single corner block has no 180° partner → not symmetric → "f".
     21         var blocks = [Bool](repeating: false, count: 9)
     22         blocks[0] = true
     23         let segment = GridSilhouette.encode(side: 3, blocks: blocks)
     24         #expect(segment?.first == "f")
     25         #expect(GridSilhouette.decode(segment ?? "") == GridSilhouette.Grid(side: 3, blocks: blocks))
     26     }
     27 
     28     @Test("round-trips a symmetric grid through decode")
     29     func roundTripsSymmetric() {
     30         var blocks = [Bool](repeating: false, count: 9)
     31         blocks[4] = true
     32         let grid = GridSilhouette.Grid(side: 3, blocks: blocks)
     33         let segment = GridSilhouette.encode(side: grid.width, blocks: grid.blocks)
     34         #expect(GridSilhouette.decode(segment ?? "") == grid)
     35     }
     36 
     37     @Test("round-trips a realistic 15×15 and stays compact")
     38     func roundTrips15x15() {
     39         // A symmetric scatter of blocks across a standard 15×15.
     40         let side = 15
     41         let n = side * side
     42         var blocks = [Bool](repeating: false, count: n)
     43         for k in [0, 4, 16, 30, 47, 88, 100, 112] {
     44             blocks[k] = true
     45             blocks[n - 1 - k] = true  // keep it symmetric
     46         }
     47         let grid = GridSilhouette.Grid(side: side, blocks: blocks)
     48         let segment = GridSilhouette.encode(side: side, blocks: blocks)
     49         #expect(segment?.first == "s")
     50         // ~2 tag/size chars + ⌈113 bits / 6⌉ ≈ 19 payload chars.
     51         #expect((segment?.count ?? .max) <= 24)
     52         #expect(GridSilhouette.decode(segment ?? "") == grid)
     53     }
     54 
     55     @Test("encodes a rectangular grid under the uppercase two-digit form")
     56     func encodesRectangularSegment() {
     57         // 2×3, all empty. Width≠height → uppercase `S`, width "2", height "3";
     58         // symmetric (all false), so ⌈6/2⌉ = 3 cells pack into 0x00 → "AA".
     59         let grid = GridSilhouette.Grid(width: 2, height: 3, blocks: [Bool](repeating: false, count: 6))
     60         let segment = GridSilhouette.encode(width: 2, height: 3, blocks: grid.blocks)
     61         #expect(segment == "S23AA")
     62         #expect(GridSilhouette.decode(segment ?? "") == grid)
     63     }
     64 
     65     @Test("tags an asymmetric rectangular grid with uppercase F")
     66     func tagsAsymmetricRectangle() {
     67         // A single corner block has no 180° partner → full dump → "F".
     68         var blocks = [Bool](repeating: false, count: 6)
     69         blocks[0] = true
     70         let segment = GridSilhouette.encode(width: 2, height: 3, blocks: blocks)
     71         #expect(segment?.first == "F")
     72         #expect(
     73             GridSilhouette.decode(segment ?? "")
     74                 == GridSilhouette.Grid(width: 2, height: 3, blocks: blocks)
     75         )
     76     }
     77 
     78     @Test("round-trips a realistic 21×22 Sunday grid")
     79     func roundTrips21x22() {
     80         // The shape that motivated rectangular support: a non-square Sunday.
     81         let width = 21
     82         let height = 22
     83         let n = width * height
     84         var blocks = [Bool](repeating: false, count: n)
     85         for k in [0, 5, 21, 64, 200, 230, 300, 410] {
     86             blocks[k] = true
     87             blocks[n - 1 - k] = true  // keep it 180°-symmetric
     88         }
     89         let grid = GridSilhouette.Grid(width: width, height: height, blocks: blocks)
     90         let segment = GridSilhouette.encode(width: width, height: height, blocks: blocks)
     91         #expect(segment?.first == "S")
     92         // width 21 → "l", height 22 → "m".
     93         #expect(segment?.dropFirst().prefix(2) == "lm")
     94         #expect(GridSilhouette.decode(segment ?? "") == grid)
     95     }
     96 
     97     @Test("encodes the largest supported side via base-36")
     98     func encodesLargeSide() {
     99         let side = GridSilhouette.maxSide  // 35 → base-36 'z'
    100         let blocks = [Bool](repeating: false, count: side * side)
    101         let segment = GridSilhouette.encode(side: side, blocks: blocks)
    102         #expect(segment?.dropFirst().first == "z")
    103         #expect(GridSilhouette.decode(segment ?? "") == GridSilhouette.Grid(side: side, blocks: blocks))
    104     }
    105 
    106     @Test("refuses a mismatched block count or out-of-range dimension")
    107     func refusesUnsupportedGrids() {
    108         #expect(GridSilhouette.encode(side: 3, blocks: [Bool](repeating: false, count: 6)) == nil)
    109         #expect(GridSilhouette.encode(side: 1, blocks: [false]) == nil)
    110         #expect(GridSilhouette.encode(side: 36, blocks: [Bool](repeating: false, count: 36 * 36)) == nil)
    111         #expect(GridSilhouette.encode(width: 2, height: 36, blocks: [Bool](repeating: false, count: 72)) == nil)
    112         #expect(GridSilhouette.encode(width: 2, height: 3, blocks: [Bool](repeating: false, count: 5)) == nil)
    113     }
    114 
    115     @Test("rejects malformed segments")
    116     func rejectsMalformedSegments() {
    117         #expect(GridSilhouette.decode("") == nil)
    118         #expect(GridSilhouette.decode("x3CA") == nil)   // unknown tag
    119         #expect(GridSilhouette.decode("s") == nil)      // no size/payload
    120         #expect(GridSilhouette.decode("s3") == nil)     // empty payload, no bits
    121         #expect(GridSilhouette.decode("s1CA") == nil)   // side 1 is below minSide
    122         #expect(GridSilhouette.decode("S2") == nil)     // rectangular tag, missing height
    123         #expect(GridSilhouette.decode("S21AA") == nil)  // height 1 is below minSide
    124     }
    125 }