Basics

This is a moderately-sized example to show many of the features of Ki. Most features are there, notably missing are:

  • asynchronous programming
  • major memory management
  • error handling
import math

const LAVA_SPOT_COUNT:  6
const PLAYER_COUNT:     2
const ENEMY_COUNT:      4
const MAX_AMMO:        10

flag Direction {
    None
    North
    NorthEast
    East
    SouthEast
    South
    SouthWest
    West
    NorthWest
}

range AmmoCount [0..MAX_AMMO]

struct PointStruct {
    var x: uint(0)
    var y: uint(0)
}

type Point (PointStruct) {
    staticfn random(min: &Point, max: &Point): Point {
        return (Point(
            math.rand_num_range(@type: uint, min.x, max.x)
            math.rand_num_range(@type: uint, min.y, max.y)
        ))
    }

    fn :eq(other: &Point): bool {
        return (self.x == other.x && self.y == other.y)
    }

    fn :str(): str {
        return ("Point({{self.x}}, {{self.y}})")
    }

    fn randomize!(min: &Point, max: &Point) {
        self.x = math.rand_num_range(@type: uint, min.x, max.x)
        self.y = math.rand_num_range(@type: uint, min.y, max.y)
    }

    fn move!(d: Direction) {
        switch (d) {
            case Direction.North {
                self.y--
            }
            case Direction.South {
                self.y++
            }
            case Direction.East {
                self.x++
            }
            case Direction.West {
                self.x--
            }
            case Direction.NorthWest {
                self.x--
                self.y--
            }
            case Direction.NorthEast {
                self.x++
                self.y--
            }
            case Direction.SouthEast {
                self.x++
                self.y++
            }
            case Direction.SouthWest {
                self.x--
                self.y++
            }
        }
    }

    fn can_move(d: Direction): bool {
        switch(d) {
            case Direction.North {
                if (self.y <= 0) {
                    return (false)
                }
            }
            case Direction.West {
                if (self.x <= 0) {
                    return (false)
                }
            }
            case Direction.NorthWest {
                if (self.x <= 0) {
                    return (false)
                }
                if (self.y <= 0) {
                    return (false)
                }
            }
            case Direction.NorthEast {
                if (self.y <= 0) {
                    return (false)
                }
            }
            case Direction.SouthWest {
                if (self.x <= 0) {
                    return (false)
                }
            }
        }
    }

    get possible_moves: Direction {
        var possible_directions: Direction.None

        for (d in Direction) {
            if (self.can_move(d)) {
                possible_directions |= d
            }
        }

        return (possible_directions)
    }
}

struct OptionalPoint {
    variant DoesNotExist {}
    variant Exists {
        var point: Point
    }
}

component GameObject {
    :requirements {
        var position: Point
        var game: Game
    }

    pred is_in_lava {
        return (self.game.map.is_lava(&self.position))
    }

    get possible_moves: Direction {
        var position_possible_directions: self.position.possible_moves

        if (position_possible_directions == PossibleDirection.None) {
            return (PossibleDirections.None)
        }

        var game_possible_directions: PossibleDirection.None

        for (d in Direction) {
            var p: self.point.move(d)

            if (self.game.map.is_position_valid(&p) &&
                !self.game.is_position_occupied(&p)) {
                game_possible_directions |= d
            }
        }

        return (position_possible_directions & game_possible_directions)
    }

    fn can_move(d: Direction): bool {
        return (self.possible_moves & d != 0)
    }
}

type Enemy {
    :fields {
        var position: Point
        var game:     Game
    }

    :components {
        GameObject
    }
}

type Player {
    :fields {
        var position: Point
        var game:     Game
        var ammo:     AmmoCount(MAX_AMMO)
    }

    :components {
        GameObject
    }

    staticfn at_random_position(): Player {
        return (Player(position: self.game.get_random_unoccupied_position()))
    }

    fn try_move!() {
        var possible_moves: self.possible_moves

        if (possible_moves != Direction.None) {
            self.move(math.rand_flag_range(possible_moves))
        }
    }

    fn shoot!() {
        self.ammo--
    }
}

array LavaSpotArray(@type: Point)

dynarray EnemyArray(@type: *Enemy)

dynarray PlayerArray(@type: *Player)

struct SizeStruct {
    var width:  uint(0)
    var height: uint(0)
}

type Size(SizeStruct) {
    fn :str(): str {
        return ("{{self.width}} x {{self.height}}")
    }
}

struct MapStruct {
    var size:       Size
    var lava_spots: LavaSpotArray
}

type Map(MapStruct) {
    fn is_valid_position(p: &Point): bool {
        return (p.x < self.size.width && p.y < self.size.height)
    }

    fn is_lava(p: &Point): bool {
        for (&lava_spot in self.map.lava_spots) {
            if (p == lava_spot) {
                return (true)
            }
        }
    }
}

struct GameStruct {
    var map:     Map
    var players: PlayerArray
    var enemies: EnemyArray
}

type Game(GameStruct) {
    fn add_player(p: *Player) {
        self.players.append!(*p)
    }

    fn add_enemy(e: *Enemy) {
        self.enemies.append!(*e)
    }

    fn is_position_occupied(p: &Point) (bool) {
        for (&player in self.players) {
            if (player.position == p) {
                return (true)
            }
        }

        for (&enemy in self.enemies) {
            if (enemy.position == p) {
                return (true)
            }
        }

        return (false)
    }

    get random_unoccupied_position: OptionalPoint {
        var point_indices: [0..(self.map.size.width * self.map.size.height))
        var points: array([Point() for x in point_indices])
        var unoccupied_point_count: point_indices.min

        ###
        # We could do:
        #
        # for (x in [0..self.map.size.width)) {
        #     for (y in [0..self.map.size.height)) {
        #         points[x * y] = Point(x, y)
        #     }
        # }
        #
        # But then the compiler can't be sure that`x * y` is a valid index into
        # `points` and will error.
        ###
        for (n in point_indices) {
            # This is guaranteed to succeed because the length of `points` is
            # defined using the range `point_indices`, and
            # `unoccupied_point_count` is a member of that range.  Therefore no
            # `with` block is necessary.
            var p: &points[unoccupied_point_count]!

            p.x = n % self.map.size.width
            p.y = n / self.map.size.height

            if (!(self.is_position_occupied(&p) || self.map.is_lava(&p))) {
                # This is safe because the default overflow policy is `limit`.
                unoccupied_point_count++
            }
        }

        if (unoccupied_point_count > point_indices.min) {
            return (OptionalPoint->Exists(math.rand_num_range(
                type: @point_indices,
                point_indices.min,
                unoccupied_point_count
            )))
        }

        return (OptionalPoint->DoesNotExist)
    }

    get live_player_count: uint {
        var live_player_count: 0

        for (&player in self.players) {
            if (player.health != 0) {
                live_player_count++
            }
        }

        return (live_player_count)
    }

    get live_enemy_count: uint {
        var live_enemy_count: 0

        for (&enemy in self.enemies) {
            if (enemy.health != 0) {
                live_enemy_count++
            }
        }

        return (live_enemy_count)
    }

    pred players_are_alive {
        return (self.live_player_count > 0)
    }

    pred enemies_are_alive {
        return (self.live_enemy_count > 0)
    }

    fn tick() {
        for (i, &player! in self.players) {
            if (player.health == 0) {
                continue
            }
            player.move()
            if (player.in_lava()) {
                player.health = 0
                continue
            }
            player.shoot()

            for (j, &enemy in self.enemies) {
                if (enemy.health == 0) {
                    continue
                }
                if (enemy.position.x == player.position.x) {
                    echo("Player {{i + 1}} shot enemy {{j + 1}}!")
                    enemy.health = 0
                    continue
                }
            }
        }
    }
}

fn main() {
    var map_size: Size(20, 1, 20)
    var zero: Point()
    var max: Point(
        map_size.width - 1,
        map_size.height - 1,
    )
    var game: Game(
        map: Map(
            size: map_size
            lava_spots: PointArray(LAVA_SPOT_COUNT)
        )
    )

    for (&point! in lava_spots) {
        point.randomize(zero, max)
    }

    loop (PLAYER_COUNT) {
        var unoccupied_point = game.random_unoccupied_position

        if (unoccupied_point->Exists) {
            game.add_player!(*Player(
                game: game
                position: unoccupied_point->Exists.point
            ))
        }
    }

    loop (ENEMY_COUNT) {
        var unoccupied_point = game.random_unoccupied_position

        if (unoccupied_point->Exists) {
            game.add_enemy!(*Enemy(
                game: game,
                position: unoccupied_point->Exists.point
            ))
        }
    }

    loop {
        if (game.live_player_count == 0) {
            echo("All players died, enemies win :(")
            break
        }

        if (game.live_enemy_count == 0) {
            echo("All enemies died, players win :)")
            break
        }
    }
}

# vi: set et sw=4 ts=4 tw=79: