diff --git a/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/.gitignore b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Package.mmp b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Package.mmp new file mode 100644 index 0000000..102d0e2 --- /dev/null +++ b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Package.mmp @@ -0,0 +1,17 @@ +# This is a MadMachine project file in TOML format +# This file holds those parameters that could not be managed by SwiftPM +# Edit this file would change the behavior of the building/downloading procedure +# Those project files in the dependent libraries would be IGNORED + +# Specify the board name below +# There are "SwiftIOBoard" and "SwiftIOMicro" now +board = "SwiftIOMicro" + +# Specifiy the target triple below +# There are "thumbv7em-unknown-none-eabi" and "thumbv7em-unknown-none-eabihf" now +# If your code use significant floating-point calculation, +# plz set it to "thumbv7em-unknown-none-eabihf" +triple = "thumbv7em-unknown-none-eabi" + +# Reserved for future use +version = 1 diff --git a/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Package.swift b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Package.swift new file mode 100644 index 0000000..dfd7774 --- /dev/null +++ b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "MazeGame", + dependencies: [ + // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/madmachineio/SwiftIO.git", branch: "main"), + .package(url: "https://github.com/madmachineio/MadBoards.git", branch: "main"), + .package(url: "https://github.com/madmachineio/MadDrivers.git", branch: "main"), + .package(url: "https://github.com/madmachineio/MadGraphics.git", branch: "main") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "MazeGame", + dependencies: [ + "SwiftIO", + "MadBoards", + // Use specific library name rather than "MadDrivers" would speed up the build procedure. + .product(name: "ST7789", package: "MadDrivers"), + .product(name: "LIS3DH", package: "MadDrivers"), + "MadGraphics" + ]), + ] +) diff --git a/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Resources/Fonts/Roboto-Regular.ttf b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Resources/Fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..ddf4bfa Binary files /dev/null and b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Resources/Fonts/Roboto-Regular.ttf differ diff --git a/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Sources/Ball.swift b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Sources/Ball.swift new file mode 100644 index 0000000..8f31d63 --- /dev/null +++ b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Sources/Ball.swift @@ -0,0 +1,20 @@ +import MadGraphics + +struct Ball { + var x1: Int + var y1: Int + let size: Int + + var x2: Int { + x1 + size + } + var y2: Int { + y1 + size + } + + init(at point: Point, size: Int) { + x1 = point.x + y1 = point.y + self.size = size + } +} \ No newline at end of file diff --git a/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Sources/Game.swift b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Sources/Game.swift new file mode 100644 index 0000000..3c5d85f --- /dev/null +++ b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Sources/Game.swift @@ -0,0 +1,266 @@ +import MadGraphics +import ST7789 +import SwiftIO + +// Place a ball at the upper left corner of the maze and move it based on acceleration. +// If the ball reaches the destination (bottom right), the game ends. +// Press the D1 button to restart the game. +struct Game { + var maze: Maze + var ball: Ball + let ballColor = Color(UInt32(0xEFE891)) + + let width = 20 + var speed = 2 + + let screen: ST7789 + let canvas: Canvas + var frameBuffer: [UInt16] + + init(screen: ST7789, canvas: Canvas) { + ball = Ball(at: Point(x: 1, y: 1), size: 7) + maze = Maze(width: width, canvas: canvas) + frameBuffer = [UInt16](repeating: 0, count: canvas.width * canvas.height) + self.screen = screen + self.canvas = canvas + + maze.generate() + + canvas.fillRectangle(at: Point(ball.x1, ball.y1), width: ball.size, height: ball.size, color: ballColor) + updateDisplay(canvas: canvas, frameBuffer: &frameBuffer, screen: screen) + } + + // Create a new maze and place the ball at the starting point. + mutating func reset() { + maze.reset() + maze.generate() + + canvas.fillRectangle(at: Point(ball.x1, ball.y1), width: ball.size, height: ball.size, color: maze.bgColor) + ball = Ball(at: Point(x: 1, y: 1), size: 7) + canvas.fillRectangle(at: Point(ball.x1, ball.y1), width: ball.size, height: ball.size, color: ballColor) + + updateDisplay(canvas: canvas, frameBuffer: &frameBuffer, screen: screen) + } + + // Update the display to show that the game has finished. + mutating func finishGame() { + canvas.fillRectangle(at: Point(0, 0), width: canvas.width, height: canvas.height, color: Color.red) + + var fileLength = 0 + if let fontDataBuffer = openFile(path: "/lfs/Resources/Fonts/Roboto-Regular.ttf", length: &fileLength) { + let largeFont = Font(from: fontDataBuffer, length: fileLength, pointSize: 10, dpi: 220) + let largeText = largeFont.getMask("Good job!") + canvas.blend(from: largeText, foreground: Color.white, to: Point(x: (canvas.width - largeText.width) / 2, y: 60)) + + let font = Font(from: fontDataBuffer, length: fileLength, pointSize: 6, dpi: 220) + let text = font.getMask("Press D1 to continue") + canvas.blend(from: text, foreground: Color.white, to: Point(x: (canvas.width - text.width) / 2, y: 140)) + } + + updateDisplay(canvas: canvas, frameBuffer: &frameBuffer, screen: screen) + } + + // Verify if the ball has reached the bottom right corner of the maze. + func finished() -> Bool { + return ball.x1 / width == maze.column - 1 && ball.y1 / width == maze.row - 1 + } + + // Update the ball's position based on the acceleration. + mutating func update(_ acceleration: (x: Float, y: Float, z: Float)) { + guard !finished() else { + finishGame() + return + } + + let lastBallPos = Point(ball.x1, ball.y1) + + // Move to the left. + if acceleration.x > 0.25 { + ball.x1 -= speed + + let gridXmin = max(ball.x1 / width, 0) + let gridXmax = min(ball.x2 / width, maze.column - 1) + let gridYmin = max(ball.y1 / width, 0) + let gridYmax = min(ball.y2 / width, maze.row - 1) + + // Check if the ball collides with any walls. + // If it does, reposition it close to the wall. + for y in gridYmin...gridYmax { + for x in gridXmin...gridXmax { + let result = checkGridWalls(ballPos: Point(ball.x1, ball.y1), gridPos: Point(x, y)) + + if result.top || result.bottom || result.right { + ball.x1 = (x + 1) * width + 1 + } + + if result.left { + ball.x1 = x * width + 1 + } + } + } + } + + // Move to the right + if acceleration.x < -0.25 { + ball.x1 += speed + + let gridXmin = max(ball.x1 / width, 0) + let gridXmax = min(ball.x2 / width, maze.column - 1) + let gridYmin = max(ball.y1 / width, 0) + let gridYmax = min(ball.y2 / width, maze.row - 1) + + for y in gridYmin...gridYmax { + for x in gridXmin...gridXmax { + let result = checkGridWalls(ballPos: Point(ball.x1, ball.y1), gridPos: Point(x, y)) + + if result.top || result.bottom || result.left { + ball.x1 = x * width - ball.size - 1 + } + + if result.right { + ball.x1 = (x + 1) * width - ball.size - 1 + } + } + } + } + + // Move downwards. + if acceleration.y > 0.25 { + ball.y1 += speed + + let gridXmin = max(ball.x1 / width, 0) + let gridXmax = min(ball.x2 / width, maze.column - 1) + let gridYmin = max(ball.y1 / width, 0) + let gridYmax = min(ball.y2 / width, maze.row - 1) + + for y in gridYmin...gridYmax { + for x in gridXmin...gridXmax { + let result = checkGridWalls(ballPos: Point(ball.x1, ball.y1), gridPos: Point(x, y)) + + if result.bottom { + ball.y1 = (y + 1) * width - 1 - ball.size + } + + if result.top || result.right || result.left { + ball.y1 = y * width - 1 - ball.size + } + } + } + } + + // Move upwards. + if acceleration.y < -0.25 { + ball.y1 -= speed + + let gridXmin = max(ball.x1 / width, 0) + let gridXmax = min(ball.x2 / width, maze.column - 1) + let gridYmin = max(ball.y1 / width, 0) + let gridYmax = min(ball.y2 / width, maze.row - 1) + + for y in gridYmin...gridYmax { + for x in gridXmin...gridXmax { + let result = checkGridWalls(ballPos: Point(ball.x1, ball.y1), gridPos: Point(x, y)) + + if result.top { + ball.y1 = y * width + 1 + } + + if result.bottom || result.right || result.left { + ball.y1 = (y + 1) * width + 1 + } + } + } + } + + // If the ball's position has changed, update the display. + if lastBallPos.x != ball.x1 || lastBallPos.y != ball.y1 { + canvas.fillRectangle(at: lastBallPos, width: ball.size, height: ball.size, color: maze.bgColor) + canvas.fillRectangle(at: Point(ball.x1, ball.y1), width: ball.size, height: ball.size, color: ballColor) + updateDisplay(canvas: canvas, frameBuffer: &frameBuffer, screen: screen) + } + } + + // Check if the ball collides with a wall. + func checkCollision(ballPos: Point, wallP1: Point, wallP2: Point) -> Bool { + return ball.x1 <= wallP2.x && ball.x2 >= wallP1.x && ball.y1 <= wallP2.y && ball.y2 >= wallP1.y + } + + // Check if the ball collides with any wall of a cell in the maze grid. + func checkGridWalls(ballPos: Point, gridPos: Point) -> Wall { + let walls = maze.grids[maze.getIndex(gridPos)].walls + var result = Wall(top: false, right: false, bottom: false, left: false) + + if walls.top && + checkCollision(ballPos: ballPos, wallP1: Point(gridPos.x * width, gridPos.y * width), wallP2: Point((gridPos.x + 1) * width, gridPos.y * width)) { + result.top = true + } + + if walls.right && + checkCollision(ballPos: ballPos, wallP1: Point((gridPos.x + 1) * width, gridPos.y * width), wallP2: Point((gridPos.x + 1) * width, (gridPos.y + 1) * width)) { + result.right = true + + } + + if walls.bottom && + checkCollision(ballPos: ballPos, wallP1: Point(gridPos.x * width, (gridPos.y + 1) * width), wallP2: Point((gridPos.x + 1) * width, (gridPos.y + 1) * width)) { + result.bottom = true + } + + if walls.left && + checkCollision(ballPos: ballPos, wallP1: Point(gridPos.x * width, gridPos.y * width), wallP2: Point(gridPos.x * width, (gridPos.y + 1) * width)) { + result.left = true + } + + return result + } + + // Open a font file. + func openFile(path: String, length: inout Int) -> UnsafeMutableRawBufferPointer? { + var fontDataBuffer: UnsafeMutableRawBufferPointer? = nil + + print("open file:") + do { + let file = try FileDescriptor.open(path, .readOnly) + + try file.seek(offset: 0, from: FileDescriptor.SeekOrigin.end) + let bytes = try file.tell() + fontDataBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: bytes, alignment: 8) + length = bytes + + try file.seek(offset: 0, from: FileDescriptor.SeekOrigin.start) + try file.read(into: fontDataBuffer!, count: bytes) + try file.close() + } catch { + print("Error, file handle error") + if let buffer = fontDataBuffer { + buffer.deallocate() + } + return nil + } + + print("open file success") + return fontDataBuffer + } + + // Get the region that needs to be updated and send data to the screen. + func updateDisplay(canvas: Canvas, frameBuffer: inout [UInt16], screen: ST7789) { + guard let dirty = canvas.getDirtyRect() else { + return + } + + var index = 0 + let stride = canvas.width + let canvasBuffer = canvas.buffer + for y in dirty.y0.. Bool { + return grids.filter { !$0.visited }.count == 0 + } + + // Calculate the index of a cell in the array. + func getIndex(_ point: Point) -> Int { + return point.x + point.y * column + } + + // Remove the wall between two cells. + mutating func removeWall(_ current: Point, _ next: Point) { + let x = current.x - next.x + + if x == 1 { + grids[getIndex(current)].walls.left = false + grids[getIndex(next)].walls.right = false + } else if x == -1 { + grids[getIndex(current)].walls.right = false + grids[getIndex(next)].walls.left = false + } + + let y = current.y - next.y + + if y == 1 { + grids[getIndex(current)].walls.top = false + grids[getIndex(next)].walls.bottom = false + } else if y == -1 { + grids[getIndex(current)].walls.bottom = false + grids[getIndex(next)].walls.top = false + } + } + + // Find nearby cells that haven't been visited yet, and select one randomly. + func getNext() -> Point? { + var neighbors: [Point] = [] + + if current.y > 0 { + let top = Point(current.x, current.y - 1) + + if !grids[getIndex(top)].visited { + neighbors.append(top) + } + } + if current.x < column - 1 { + let right = Point(current.x + 1, current.y) + + if !grids[getIndex(right)].visited { + neighbors.append(right) + } + } + + if current.y < row - 1 { + let bottom = Point(current.x, current.y + 1) + if !grids[getIndex(bottom)].visited { + neighbors.append(bottom) + } + } + + if current.x > 0 { + let left = Point(current.x - 1, current.y) + if !grids[getIndex(left)].visited { + neighbors.append(left) + } + } + + return neighbors.randomElement() + } +} \ No newline at end of file diff --git a/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Sources/MazeGame.swift b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Sources/MazeGame.swift new file mode 100644 index 0000000..3955922 --- /dev/null +++ b/Examples/SwiftIOPlayground/12MoreProjects/MazeGame/Sources/MazeGame.swift @@ -0,0 +1,53 @@ +import SwiftIO +import MadBoard +import ST7789 +import MadGraphics +import LIS3DH + +@main +public struct MazeGame { + public static func main() { + // Initialize the SPI pin and the digital pins for the LCD. + let bl = DigitalOut(Id.D2) + let rst = DigitalOut(Id.D12) + let dc = DigitalOut(Id.D13) + let cs = DigitalOut(Id.D5) + let spi = SPI(Id.SPI0, speed: 30_000_000) + + // Initialize the LCD using the pins above. Rotate the screen to keep the original at the upper left. + let screen = ST7789(spi: spi, cs: cs, dc: dc, rst: rst, bl: bl, rotation: .angle90) + + let i2c = I2C(Id.I2C0) + let accelerometer = LIS3DH(i2c) + + let canvas = Canvas(width: screen.width, height: screen.height) + var mazeGame = Game(screen: screen, canvas: canvas) + + let resetButton = DigitalIn(Id.D1) + + var reset = false + resetButton.setInterrupt(.falling) { + reset = true + } + + var sleepTime: Float = 0 + let maxTime: Float = 20 + let minTime: Float = 5 + + while true { + // If the reset button is pressed, restart the game. + if reset { + mazeGame.reset() + reset = false + } + + // Update ball's position based on the acceleration. + let acceleration = accelerometer.readXYZ() + mazeGame.update(acceleration) + + // Map the acceleration into a sleep time in order to control the speed of the ball. + sleepTime = min(max(abs(acceleration.x), abs(acceleration.y)), 1) * (minTime - maxTime) + maxTime + sleep(ms: Int(sleepTime)) + } + } +} \ No newline at end of file