Модель игры

Одно время в App Store была доступна версия Color Lines от Piston Games. У них подсмотрел идею что можно использовать несколько пачек M&Ms и попробовать игру в оффлайне.


Запускаем Xcode и нажимаем Get started with a playground 

Выбираем шаблон Blank

Даем название Lines и сохраняем

Документация языка программирования Swift на русском языке
The Swift Programming Language

Зададим массив cells и посчитаем сколько единиц справа от cells[3]. Обратите внимание, что индексация в массивах начинается с нуля

посчитаем на пальцах:

Ответ 3 единицы справа от cells[3].

Используем цикл для решения этой задачи:

Посчитаем теперь единицы слева от cells[3]

в первом случае мы прибавляли (n+1), а во втором случае отнимали. Перепишем наше решение следующим образом:

если мы изменим c на -1, то мы посчитаем слева от cells[column]

изменим значение cells[0] на единицу

Как же так? А почему раньше этой ошибки не было? Потому что выход за пределы массива преграждало число ноль, а сейчас крайней оказалась единица. А что если добавить в массив по числу в начале и в конце, которое наверняка не будет удовлетворять условию цикла? Зададим массив тест, который получился из массива cells добавлением -1 в начале и в конце:

Теперь можно смело считать и влево и вправо.

Перейдем к двумерным массивам. cells из 9 строк и 9 столбцов состоит из единиц, а test 11 на 11 из -1

Затем мы в цикле присвоим test[i+1][j+1] = cells[i][j], т.е. мы добавили к cells столбец слева и с права, а так же строку снизу и сверху.

Мы, как и прежде можем считать сколько единиц слева и справа в отдельно взятой строке. Как теперь нам посчитать единицы сверху и снизу и по диагонали?

Возьмем cells[4][4] и условно пронумеруем «направления подсчета». r сокращение от row (ряд) c сокращение от column (колонка) принимают значения -1, 0, 1

0: r = -1 c = -1
1: r = -1 c = 0
2: r = -1 c = 1
3: r = 0 c = 1
4: r = 1 c = 1
5: r = 1 c = 0
6: r = 1 c = -1
7: r = 0 c = -1

Копируем код и вставляем в Lines.swift:

struct Lines {
    var cells = Array(repeating: Array(repeating: 0, count:9), count: 9)
    var test = Array(repeating: Array(repeating: -1, count:11), count: 11)

    mutating func dropBall(row: Int, column: Int, color: Int) -> Int {
        let c = [-1,  0,  1, 1, 1, 0, -1, -1]
        let r = [-1, -1, -1, 0, 1, 1,  1,  0]
        var n = [0, 0, 0, 0, 0, 0, 0, 0]
        var total = 0
        for i in 0..<9 {
            for j in 0..<9 {
                test[i+1][j+1] = cells[i][j]
            }
        }
        for i in 0..<8 {
            while test[(row + 1) + r[i] * (n[i] + 1)][(column + 1) + c[i] * (n[i] + 1)] == color {
                n[i] += 1
            }
        }
        for i in 0..<4 {
            if n[i] + n[i+4] < 4 {
                n[i] = 0
                n[i+4] = 0
            } else {
                total += n[i] + n[i+4]
            }
        }
        if total > 0 {
            total += 1
            for i in 0..<8 {
                for j in 0..<n[i] {
                    cells[row + (1 + j) * r[i]][column + (1 + j) * c[i]] = 0
                }
            }
        } else {
            cells[row][column] = color
        }
        return total
    }
}

Допишем GameFieldView:

class GameFieldView: UIView {
    var cells = Array(repeating: Array(repeating: 0, count:9), count: 9)
    let images = [UIImage(named: "0"), UIImage(named: "1"), UIImage(named: "2"), UIImage(named: "3"), UIImage(named: "4"), UIImage(named: "5"), UIImage(named: "6"), UIImage(named: "7")]
    
    override func draw(_ rect: CGRect) {
        let context = UIGraphicsGetCurrentContext()
        context?.draw(images[0]!.cgImage!, in: rect)
        
        let cellHeight=rect.height/9
        let cellWidth=rect.width/9
        var imageRect = CGRect(x: 0.0, y:0.0, width: cellWidth, height: cellHeight)
        
        for i in 0..<9 {
            for j in 0..<9 {
                if cells[i][j] > 0 {
                    imageRect.origin.x=cellHeight*CGFloat(j)
                    imageRect.origin.y=cellWidth*CGFloat(i)
                    context?.draw(images[cells[i][j]]!.cgImage!, in: imageRect)
                }
            }
        }
    }
}

изменим tapGameField

    @IBAction func tapGameFiled(_ sender: UITapGestureRecognizer) {
        let point = sender.location(in: gameFieldView)
        
        let column = Int(point.x/(gameFieldView.frame.width/9))
        let row = Int(point.y/(gameFieldView.frame.height/9))
        let score = lines.dropBall(row: row, column: column, color: 1)
        if score > 4 {
            print(score)
        }
        gameFieldView.cells = lines.cells
        gameFieldView.setNeedsDisplay()
    }

Запускаем приложение

Функция dropBall обнуляет элементы массива, которые образуют ряд из пяти и более шариков, для того чтоб наглядно увидеть, линии для удаления, заменим ноль на 2 (это желтый шарик)

запустим приложение и расставим шарики. К примеру у меня получилось так, что если я поставлю шарик в указанную клетку, то завершатся несколько линий

не все шарики удалились, по вертикали и горизонтали шариков меньше пяти

Добавим Lines структуру

struct Balls {
    var row: Int
    var column: Int
    var color: Int
}

и несколько переменных

    var colorsInGame = 1
    var nextBalls = [Balls]()
    var ballsInGame = 7
    var selectedBall = Balls(row: 0, column: 0, color: 0)

и

    mutating func makeNextBalls(count: Int) {
        var emptyCells = [Balls]()
        for i in 0..<9 {
            for j in 0..<9 {
                if cells[i][j]==0 {
                    emptyCells.append(Balls(row: i, column: j, color: Int(arc4random()) % colorsInGame + 1))
                }
            }
        }
        if !nextBalls.isEmpty {
            nextBalls.removeAll()
        }
        for _ in 0..<count {
            if !emptyCells.isEmpty {
                nextBalls.append(emptyCells.remove(at: Int(arc4random()) % emptyCells.count))
            }
        }
    }

и

    mutating func newGame() {
        for i in 0..<9 {
            for j in 0..<9 {
                cells[i][j] = 0
            }
        }
        makeNextBalls(count: ballsInGame)
        for _ in 0..<ballsInGame {
            
            if !nextBalls.isEmpty {
                _ = dropBall(row: nextBalls[0].row, column: nextBalls[0].column, color: nextBalls[0].color)
                nextBalls.remove(at: 0)
            }
        }
        makeNextBalls(count: 3)
    }

запускаем

и

    mutating func dropThreeBalls() {
        var c = 0
        for _ in 0..<3 {
            if !nextBalls.isEmpty {
                if cells[nextBalls[0].row][nextBalls[0].column] > 0 {
                    c = nextBalls[0].color
                } else {
                    _ = dropBall(row: nextBalls[0].row, column: nextBalls[0].column, color: nextBalls[0].color)
                }
                nextBalls.remove(at: 0)
            } else {
                makeNextBalls(count: 3)
                if !nextBalls.isEmpty {
                    _ = dropBall(row: nextBalls[0].row, column: nextBalls[0].column, color: nextBalls[0].color)
                    nextBalls.remove(at: 0)
                }
            }
        }
        makeNextBalls(count: 3)
        if (c > 0) && (!nextBalls.isEmpty) {
            _ = dropBall(row: nextBalls[0].row, column: nextBalls[0].column, color: c)
            nextBalls.remove(at: 0)
            makeNextBalls(count: 3)
        }
    }

перепишем

@IBAction func tapGameFiled(_ sender: UITapGestureRecognizer) {
        let point = sender.location(in: gameFieldView)
        
        let column = Int(point.x/(gameFieldView.frame.width/9))
        let row = Int(point.y/(gameFieldView.frame.height/9))
        let score = lines.dropBall(row: row, column: column, color: 1)
        if score > 4 {
            print(score)
        } else {
            lines.dropThreeBalls()
            if lines.nextBalls.isEmpty {
                print("Game Over")
            }
        }
        gameFieldView.cells = lines.cells
        gameFieldView.setNeedsDisplay()
    }

А теперь про зайцев….

За один ход игрок может передвинуть один шарик, выделив его и указав его новое местоположение. Для совершения хода необходимо, чтобы между начальной и конечной клетками существовал путь из свободных клеток.

Предположим, что нам захотелось переместить зайца

Заяц, как ладья на шахматной доске, может двигаться либо по вертикали, либо по горизонтали. За один шаг он может переместиться на соседнюю клетку. Предположим, что у нас есть фишки, с числами. Разложим фишки с цифрой 1 на соседние с зайцем клетки, получим все клетки, на которые можно переместить зайца за один шаг. Разложим фишки с цифрой 2 на оставшиеся пустые клетки (не занятые ни фишками, ни фигурками) на которые можно переместиться с клетки с фишкой 1 за один шаг. Продолжая раскладывать фишки таким образом, мы закроем все пустые клетки в которые существует путь из клетки с зайцем. В оставшиеся свободные клетки переместиться нельзя, так путь к ним прегражден фигурками. В итоге мы получили алгоритм, по которому на этапе выбора фигурки для перемещения, мы уже сможем определить на какую клетку можно переместиться и за сколько ходов, а на какую нельзя.

mutating func makeWays(row: Int, column: Int) {
        selectedBall.row = row
        selectedBall.column = column
        selectedBall.color = cells[row][column]
        for i in 0..<9 {
            for j in 0..<9 {
                test[i+1][j+1] = (cells[i][j] == 0) ? -2 : -1
            }
        }
        test[row+1][column+1] = 0
        var k = 0
        var flag = false
        repeat {
            flag = false
            for i in 1..<10 {
                for j in 1..<10 {
                    if test[i][j] == -2 {
                        if  (test[i-1][j] == k) ||
                            (test[i][j-1] == k) ||
                            (test[i+1][j] == k) ||
                            (test[i][j+1] == k) {
                            
                            test[i][j] = k + 1
                            flag = true
                        }
                    }
                }
            }
            if flag {
                k += 1
            }
        } while flag
    }

перепишем

    var ballSelected = false
    var score = 0
    
    //MARK: Actions
    @IBAction func tapGameFiled(_ sender: UITapGestureRecognizer) {
        let point = sender.location(in: gameFieldView)
        let column = Int(point.x/(gameFieldView.frame.width/9))
        let row = Int(point.y/(gameFieldView.frame.height/9))
        var line = 0
        if ballSelected {
            switch lines.test[row + 1][column + 1] {
            case 0:
                //Передумал
                ballSelected = false
            case -1:
                //Выбрал другой шарик
                lines.makeWays(row: row, column: column)
            case -2:
                //Хода нет
                print("Хода нет")
            default:
                //Перейти на новую клетку
                ballSelected = false
                lines.cells[lines.selectedBall.row][lines.selectedBall.column] = 0
                line = lines.dropBall(row: row, column: column, color: lines.selectedBall.color)
                if line > 4 {
                    score += 2 * line
                    print(score)
                } else {
                    lines.dropThreeBalls()
                    if lines.nextBalls.isEmpty {
                        print("Game Over")
                    }
                }
                gameFieldView.cells = lines.cells
                gameFieldView.setNeedsDisplay()
            }
        } else {
            if lines.cells[row][column] > 0 {
                ballSelected = true
                print(row, column)
                lines.makeWays(row: row, column: column)
            } else {
                //пустая клетка
            }
        }
    }

 


GameFieldView.swift
Lines.swift
ViewController.swift
Если заинтересовались, то пишите на developer@macservice.kz. Будем вместе дописывать приложение.

Старт Начало