Skip to content

Commit

Permalink
Add more docs and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Oleksii Oliinyk committed Jun 27, 2023
1 parent 2675829 commit 1a19b46
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 17 deletions.
11 changes: 10 additions & 1 deletion Sources/Match3Kit/Grid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public struct Grid<Filling>: Codable where Filling: GridFilling {

@inlinable
public func allIndices(of filling: Filling) -> [Index] {
allIndices().filter { cell(at: $0).filling == filling }
allIndices().filter { self[$0].filling == filling }
}

@inlinable
Expand All @@ -242,6 +242,15 @@ public struct Grid<Filling>: Codable where Filling: GridFilling {

// MARK: - Modification

public subscript(index: Index) -> Cell {
get {
columns[index.column][index.row]
}
set {
columns[index.column][index.row] = newValue
}
}

public mutating func setCell(_ cell: Cell, at index: Index) {
columns[index.column][index.row] = cell
}
Expand Down
43 changes: 37 additions & 6 deletions Sources/Match3Kit/Matcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,38 @@
// Copyright © 2020 Alexey. All rights reserved.
//

/// Matches detector
/// `Matcher` is a generic class used to identify matching elements in a `Grid`.
/// It requires a set of `GridFilling` elements to match against and a minimum series length for a match to be considered.
///
/// Example usage:
///
/// ```swift
/// let fillings: Set<String> = ["X", "Y", "Z"]
/// let matcher = Matcher(fillings: fillings, minSeries: 3)
/// let matches = matcher.findAllMatches(on: myGrid)
/// ```
///
/// It can also find matches for a subset of indices or at a specific index.
///
/// ```swift
/// let indices: [Index] = [Index(column: 0, row: 0), Index(column: 1, row: 0)]
/// let subsetMatches = matcher.findMatched(on: myGrid, indices: indices)
///
/// let index = Index(column: 0, row: 0)
/// let singleMatch = matcher.findMatches(on: myGrid, at: index)
/// ```
///
/// `Matcher` can be subclassed to change the matching logic. For example, you could create a subclass that matches based on some property of the `GridFilling` other than equality.
///
/// ```swift
/// class CustomMatcher: Matcher<String> {
/// override func match(cell: Grid<String>.Cell, with cellToMatch: Grid<String>.Cell) -> Bool {
/// // Custom matching logic
/// }
/// }
/// ```
///
/// - Note: `Matcher` does not modify the `Grid` or the `GridFilling`. It only identifies matches. The responsibility for handling matches (e.g., removing them from the grid, updating the score) lies elsewhere.
open class Matcher<Filling: GridFilling> {

public private(set) var fillings: Set<Filling>
Expand All @@ -29,11 +60,11 @@ open class Matcher<Filling: GridFilling> {
findMatched(on: grid, indices: grid.allIndices())
}

public func findMatched<Indices: Collection>(on grid: Grid<Filling>,
indices: Indices) -> Set<Index> where Indices.Element == Index {
indices.reduce(Set()) { result, index in
result.union(findMatches(on: grid, at: index))
}
public func findMatched(
on grid: Grid<Filling>,
indices: some Collection<Index>
) -> Set<Index> {
Set(indices.flatMap { findMatches(on: grid, at: $0) })
}

public func findMatches(on grid: Grid<Filling>, at index: Index) -> Set<Index> {
Expand Down
168 changes: 168 additions & 0 deletions Tests/Match3KitTests/GridTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//
// GridTests.swift
//
//
// Created by Alexey Oleynik on 27.06.23.
//

import XCTest

@testable import Match3Kit

final class SizeTests: XCTestCase {

func testInitialization() {
let size = Size(columns: 5, rows: 10)
XCTAssertEqual(size.columns, 5)
XCTAssertEqual(size.rows, 10)
}

func testBounds() {
let size = Size(columns: 5, rows: 10)
XCTAssertEqual(size.lowerBound, -1)
XCTAssertEqual(size.upperBound, 10)
XCTAssertEqual(size.leftBound, -1)
XCTAssertEqual(size.rightBound, 5)
}

func testIsOnBounds() {
let size = Size(columns: 5, rows: 10)
// Testing within bounds
XCTAssertTrue(size.isOnBounds(Index(column: 0, row: 0)))
XCTAssertTrue(size.isOnBounds(Index(column: 4, row: 9)))
// Testing at bounds
XCTAssertFalse(size.isOnBounds(Index(column: -1, row: 0)))
XCTAssertFalse(size.isOnBounds(Index(column: 5, row: 0)))
XCTAssertFalse(size.isOnBounds(Index(column: 0, row: -1)))
XCTAssertFalse(size.isOnBounds(Index(column: 0, row: 10)))
// Testing out of bounds
XCTAssertFalse(size.isOnBounds(Index(column: -2, row: 0)))
XCTAssertFalse(size.isOnBounds(Index(column: 6, row: 0)))
XCTAssertFalse(size.isOnBounds(Index(column: 0, row: -2)))
XCTAssertFalse(size.isOnBounds(Index(column: 0, row: 11)))
}
}

final class IndexTests: XCTestCase {

func testInitialization() {
let index = Index(column: 5, row: 10)
XCTAssertEqual(index.column, 5)
XCTAssertEqual(index.row, 10)
}

func testZero() {
let zeroIndex = Index.zero
XCTAssertEqual(zeroIndex.column, 0)
XCTAssertEqual(zeroIndex.row, 0)
}

func testNeighbors() {
let index = Index(column: 5, row: 10)
XCTAssertEqual(index.upper, Index(column: 5, row: 11))
XCTAssertEqual(index.lower, Index(column: 5, row: 9))
XCTAssertEqual(index.right, Index(column: 6, row: 10))
XCTAssertEqual(index.left, Index(column: 4, row: 10))
}

func testCrossNeighbors() {
let index = Index(column: 5, row: 10)
XCTAssertEqual(index.crossNeighbors, [
Index(column: 4, row: 9),
Index(column: 6, row: 11),
Index(column: 4, row: 11),
Index(column: 6, row: 9)
])
}

func testIsNeighboring() {
let index = Index(column: 5, row: 10)
let neighboringIndex = Index(column: 6, row: 10)
let nonNeighboringIndex = Index(column: 7, row: 10)

XCTAssertTrue(index.isNeighboring(with: neighboringIndex))
XCTAssertFalse(index.isNeighboring(with: nonNeighboringIndex))
}

func testSequences() {
let index = Index(column: 5, row: 10)
let expectedUpperSequence = Array(11...20).map { Index(column: 5, row: $0) }
let expectedLowerSequence = Array(0..<10).reversed().map { Index(column: 5, row: $0) }
let expectedRightSequence = Array(6...15).map { Index(column: $0, row: 10) }
let expectedLeftSequence = Array(-5..<5).reversed().map { Index(column: $0, row: 10) }

XCTAssertEqual(Array(index.upperSequence().prefix(10)), expectedUpperSequence)
XCTAssertEqual(Array(index.lowerSequence().prefix(10)), expectedLowerSequence)
XCTAssertEqual(Array(index.rightSequence().prefix(10)), expectedRightSequence)
XCTAssertEqual(Array(index.leftSequence().prefix(10)), expectedLeftSequence)
}
}

final class GridTests: XCTestCase {

// Define a simple filling type for the purpose of testing
struct TestFilling: GridFilling {
var pattern: Match3Kit.Pattern {
.init(indices: [])
}
}

func testInitialization() {
let size = Size(columns: 5, rows: 5)
let columns: [[Grid<TestFilling>.Cell]] = Array(repeating: Array(repeating: Grid<TestFilling>.Cell(id: UUID(), filling: TestFilling()), count: size.rows), count: size.columns)
let grid = Grid<TestFilling>(size: size, columns: columns)

XCTAssertEqual(grid.size, size)
XCTAssertEqual(grid.columns.count, size.columns)
grid.columns.forEach { column in
XCTAssertEqual(column.count, size.rows)
}
}

func testCellAccess() {
let size = Size(columns: 5, rows: 5)
let columns: [[Grid<TestFilling>.Cell]] = Array(repeating: Array(repeating: Grid<TestFilling>.Cell(id: UUID(), filling: TestFilling()), count: size.rows), count: size.columns)
var grid = Grid<TestFilling>(size: size, columns: columns)

let index = Index(column: 2, row: 2)
let cell = grid.cell(at: index)

XCTAssertEqual(cell, grid.columns[index.column][index.row])

let newCell = Grid<TestFilling>.Cell(id: UUID(), filling: TestFilling())
grid.setCell(newCell, at: index)

XCTAssertEqual(newCell, grid.cell(at: index))
}

func testAllIndices() {
let size = Size(columns: 5, rows: 5)
let columns: [[Grid<TestFilling>.Cell]] = Array(repeating: Array(repeating: Grid<TestFilling>.Cell(id: UUID(), filling: TestFilling()), count: size.rows), count: size.columns)
let grid = Grid<TestFilling>(size: size, columns: columns)

let allIndices = grid.allIndices()

XCTAssertEqual(allIndices.count, size.columns * size.rows)
for i in 0..<size.columns {
for j in 0..<size.rows {
XCTAssertTrue(allIndices.contains(Index(column: i, row: j)))
}
}
}

func testSwapCells() {
let size = Size(columns: 5, rows: 5)
let columns: [[Grid<TestFilling>.Cell]] = Array(repeating: Array(repeating: Grid<TestFilling>.Cell(id: UUID(), filling: TestFilling()), count: size.rows), count: size.columns)
var grid = Grid<TestFilling>(size: size, columns: columns)

let index1 = Index(column: 2, row: 2)
let index2 = Index(column: 3, row: 3)
let cell1 = grid.cell(at: index1)
let cell2 = grid.cell(at: index2)

grid.swapCell(at: index1, with: index2)

XCTAssertEqual(cell1, grid.cell(at: index2))
XCTAssertEqual(cell2, grid.cell(at: index1))
}
}
10 changes: 0 additions & 10 deletions Tests/Match3KitTests/Match3KitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,3 @@ enum Toys: String, GridFilling, CaseIterable {
Match3Kit.Pattern(indices: [])
}
}

final class Match3KitTests: XCTestCase {

func testExample() {
}

static var allTests = [
("testExample", testExample),
]
}
61 changes: 61 additions & 0 deletions Tests/Match3KitTests/MatcherTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// File.swift
//
//
// Created by Alexey Oleynik on 27.06.23.
//

import XCTest

@testable import Match3Kit

final class MatcherTests: XCTestCase {
enum Filling: GridFilling {
case empty
case x
case y

var pattern: Pattern {
.init(indices: [])
}
}

func testFindMatches() {
let matcher = Matcher(fillings: [Filling.x], minSeries: 3)

// Initialize grid with empty cells
var columns = [[Grid<Filling>.Cell]]()
for _ in 0..<5 {
var column = [Grid<Filling>.Cell]()
for _ in 0..<5 {
let cell = Grid<Filling>.Cell(id: UUID(), filling: .empty)
column.append(cell)
}
columns.append(column)
}

var grid = Grid<Filling>(size: Size(columns: 5, rows: 5),
columns: columns)
grid[.init(column: 0, row: 0)] = .init(id: UUID(), filling: .x)
grid[.init(column: 0, row: 1)] = .init(id: UUID(), filling: .x)
grid[.init(column: 0, row: 2)] = .init(id: UUID(), filling: .x)
let matches = matcher.findMatches(on: grid, at: Index(column: 0, row: 0))
XCTAssertEqual(matches.count, 3, "Should find 3 matches")
}

func testMatchCell() {
let matcher = Matcher(fillings: [Filling.x], minSeries: 3)
let cell1 = Grid<Filling>.Cell(id: UUID(), filling: .x)
let cell2 = Grid<Filling>.Cell(id: UUID(), filling: .x)
let isMatch = matcher.match(cell: cell1, with: cell2)
XCTAssertTrue(isMatch, "Should match cells with the same filling")
}

func testMatchCellFails() {
let matcher = Matcher(fillings: [Filling.x, .y], minSeries: 3)
let cell1 = Grid<Filling>.Cell(id: UUID(), filling: .x)
let cell2 = Grid<Filling>.Cell(id: UUID(), filling: .y)
let isMatch = matcher.match(cell: cell1, with: cell2)
XCTAssertFalse(isMatch, "Should not match cells with different fillings")
}
}

0 comments on commit 1a19b46

Please sign in to comment.