2
\$\begingroup\$

This is a simple program where the goal is to visualize a virtual game to be played by an AI. For the purposes of this post, the playGameManually() function was added as a way of testing the rest of the code. The program was built around an open ended framework that allows each element to be modified independently outside of regular gameplay while also making it simple when using manually.

import Foundation

// -- Constants -- //

let MAPWIDTH  = 50
let MAPHEIGHT = 20

// -- End Constants -- //

struct Coord {
    var x: Int
    var y: Int

    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

enum Direction: Int {
    case up    = 1
    case down  = 2
    case left  = 3
    case right = 4
}

class Game {

    var snake = Array<Coord>(arrayLiteral: Coord(x: 0, y: 2), Coord(x: 0, y: 1), Coord(x: 0, y: 0)) // A queue would be far better here but since swift dosen't have one build in we use an array for simplicity.
    var food = Coord(x: 10, y: 10)
    var direction = Direction.down

    func move() {
        func calculateNextLocation() -> Coord {
            // This function calculates where the snake should move and makes sure that the next movement is not off the screen.  It does NOT check to see if a part has crossed or if it is touching food.

            if direction == .down && snake[0].y < MAPHEIGHT - 1 {
                return Coord(x: snake[0].x, y: snake[0].y + 1)
            }
            else if direction == .up && snake[0].y > 0 {
                return Coord(x: snake[0].x, y: snake[0].y - 1)
            }
            else if direction == .left && snake[0].x > 0 {
                return Coord(x: snake[0].x - 1, y: snake[0].y)
            }
            else if direction == .right && snake[0].x < MAPWIDTH - 1 {
                return Coord(x: snake[0].x + 1, y: snake[0].y)
            }
            else {
                print("Death: You went off the map!")
                death()
                return Coord(x: 0, y: 0)//compiler doesn't know that death() will self destruct
            }
        }

        func isCrossed(coord: Coord) -> Bool {
            for node in snake {
                if node.x == coord.x && node.y == coord.y {
                    return true
                }
            }
            return false
        }

        let nextCoord = calculateNextLocation()

        if !isCrossed(coord: nextCoord) {
            if nextCoord.x == food.x && nextCoord.y == food.y {//delete last node if not touching food
                food = generateNewFoodCoords()
            }
            else {
                snake.removeLast()
            }
            snake.insert(nextCoord, at: 0)
        }
        else {
            print("Death: You crossed your path!")
            death()
        }

    }

    func generateNewFoodCoords() -> Coord {
        return Coord(x: Int(arc4random_uniform(UInt32(MAPWIDTH))), y: Int(arc4random_uniform(UInt32(MAPHEIGHT))))
    }

    func moveUp() {
        if direction != .down {
            direction = .up
            move();
        }
        else {
            print("Death: You looped back over your tail!")
            death()
        }
    }

    func moveDown() {
        if direction != .up {
            direction = .down
            move();
        }
        else {
            print("Death: You looped back over your tail!")
            death()
        }
    }

    func moveLeft() {
        if direction != .right {
            direction = .left
            move();
        }
        else {
            print("Death: You looped back over your tail!")
            death()
        }
    }

    func moveRight() {
        if direction != .left {
            direction = .right
            move();
        }
        else {
            print("Death: You looped back over your tail!")
            death()
        }
    }

    func death() {
        exit(1)
    }

    func drawMap() {
        print("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n")
        print("+--------------------------------------------------+")
        for y in 0 ..< MAPHEIGHT {
            print("|", terminator:"")
            for x in 0 ..< MAPWIDTH {
                var hasPrinted = false
                for node in snake {
                    if x == node.x && y == node.y {
                        print("+", terminator:"")
                        hasPrinted = true
                        break
                    }
                }
                if !hasPrinted {
                    if x == food.x && y == food.y {
                        print("X", terminator:"")
                    }
                    else {
                        print(" ", terminator:"")
                    }
                }
            }
            print("|\n", terminator:"")
        }
        print("+--------------------------------------------------+")
    }
}

func playGameManually() {
    let g = Game()
    g.drawMap()
    var l = ""
    while l != "q" {
        l = readLine() ?? ""
        if l == "w" {
            g.moveUp()
        }
        else if l == "a" {
            g.moveLeft()
        }
        else if l == "s" {
            g.moveDown()
        }
        else if l == "d" {
            g.moveRight()
        }

        g.drawMap()
    }
}
\$\endgroup\$
3
\$\begingroup\$

The raw values of enum Direction are not used at all, you can simplify the definition to

enum Direction {
    case up
    case down
    case left
    case right
}

The x/y values of struct Coord are never mutated, so you can declare them as constants with let. The init method is not needed because there is a default member-wise initializer:

struct Coord {
    let x: Int
    let y: Int
}

Your code can be simplified at many places if you make the Coord type Equatable:

struct Coord: Equatable {
    let x: Int
    let y: Int

    static func ==(lhs: Coord, rhs: Coord) -> Bool {
        return lhs.x == rhs.x && lhs.y == rhs.y
    }
}

For example,

if nextCoord.x == food.x && nextCoord.y == food.y

becomes

if nextCoord == food

and

for node in snake {
    if node.x == coord.x && node.y == coord.y {
        return true
    }
}
return false

becomes

return snake.contains(node)

so that func isCrossed() is not really needed anymore:

let nextCoord = calculateNextLocation()
if !snake.contains(nextCoord) {
    if nextCoord == food {
        food = generateNewFoodCoords()
    } else {
        snake.removeLast()
    }
    snake.insert(nextCoord, at: 0)
} else  ...

This also simplifies the code in func drawMap(). In addition, String(repeating:count:) can be used here to draw the horizontal lines (so that it works for other map widths as well):

func drawMap() {
    print(String(repeating: "\n", count: 22))
    print("+" + String(repeating: "-", count: MAPWIDTH) + "+")
    for y in 0 ..< MAPHEIGHT {
        print("|", terminator:"")
        for x in 0 ..< MAPWIDTH {
            let coord = Coord(x: x, y: y)
            if snake.contains(coord) {
                print("+", terminator:"")
            } else if coord == food {
                print("X", terminator:"")
            } else {
                print(" ", terminator:"")
            }
        }
        print("|\n", terminator:"")
    }
    print("+" + String(repeating: "-", count: MAPWIDTH) + "+")
}

Next,

var snake = Array<Coord>(arrayLiteral: Coord(x: 0, y: 2), Coord(x: 0, y: 1), Coord(x: 0, y: 0))

can be written shorter as

var snake = [ Coord(x: 0, y: 2), Coord(x: 0, y: 1), Coord(x: 0, y: 0) ]

The dummy return in

print("Death: You went off the map!")
death()
return Coord(x: 0, y: 0)//compiler doesn't know that death() will self destruct

is not needed because you can tell the compiler that death() will never return:

func death() -> Never {
    exit(1)
}

I would pass the reason as a parameter:

func death(_ reason: String) -> Never {
    print("Death: \(reason)")
    exit(1)
}

so that you can call

death("You went off the map!")

In func playGameManually() I would use a switch statement to handle the possible inputs, and a boolean flag for the "running" state:

func playGameManually() {
    let g = Game()
    var running = true
    while running {
        g.drawMap()
        switch readLine() ?? "" {
        case "w":
            g.moveUp()
        case "a":
            g.moveLeft()
        case "s":
            g.moveDown()
        case "d":
            g.moveRight()
        case "q":
            running = false
        default:
            break // ignore all other input
        }
    }
}

Further suggestions:

  • func calculateNextLocation() operates on a coordinate, not on the game. This could be made a

    func next(direction: Direction) -> Coord { ... }
    

    method of struct Coord.

  • Make MAPWIDTH / MAPHEIGHT properties of the game instead of global variables.

\$\endgroup\$
0
\$\begingroup\$

the moveUp / moveDown etc can be more generic

func move(Direction nextDirection, Direction deathDirection){
    if direction != deathDirection {
        direction = nextDirection
        move();
    }
    else {
        print("Death: You looped back over your tail!")
        death()
    }
}

and then instead of moveUp() and others you give the direction as parameter:

move(.up,.down)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.