Skip to content

Commit

Permalink
added obstacles
Browse files Browse the repository at this point in the history
  • Loading branch information
Acepie committed May 10, 2024
1 parent b32f60c commit 5f3d8a6
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 10 deletions.
12 changes: 11 additions & 1 deletion src/bullet_heck_gleam.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import dungeon
import gleam/bool
import gleam/int
import gleam/list
import obstacle
import p5js_gleam.{type Assets, type P5}
import p5js_gleam/bindings as p5
import player
Expand Down Expand Up @@ -297,14 +298,20 @@ fn on_tick(state: WorldState) -> WorldState {

let player = player.update_velocity(player)
let player = player.apply_gravity(player)
let player =
list.fold(dungeon.obstacles, player, fn(player, o) {
case obstacle.collides_with(o, player.position, player.player_size) {
True -> player.apply_damage(player, obstacle.damage)
False -> player
}
})

let bullets =
bullets
|> list.filter(bullet.is_still_alive)

let #(bullets, player) =
list.fold(bullets, #([], player), fn(acc, b) {
// If the bullet hits a wall then remove it
use <- bool.guard(
!dungeon.can_move(
dungeon,
Expand All @@ -313,6 +320,7 @@ fn on_tick(state: WorldState) -> WorldState {
),
acc,
)
// If the bullet hits a wall then remove it

// Check if the bullet collides with something it can hit
let #(bullets, player) = acc
Expand All @@ -327,6 +335,8 @@ fn on_tick(state: WorldState) -> WorldState {
#([bullet.advance_bullet(b), ..bullets], player)
})

use <- bool.guard(player.is_player_dead(player), GameOver(False, score))

GameRunning(
dungeon: dungeon,
player: player,
Expand Down
71 changes: 67 additions & 4 deletions src/dungeon.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import gleam/int
import gleam/iterator
import gleam/list
import gleam/result
import obstacle.{type Obstacle}
import p5js_gleam.{type P5}
import pit.{type Pit}
import prng/random
Expand Down Expand Up @@ -37,6 +38,10 @@ const pit_size = 20.0

const minimum_pit_distance = 80.0

const obstacle_rate = 0.5

const obstacle_limit = 3

type Coordinate =
#(Int, Int)

Expand All @@ -50,6 +55,8 @@ pub type Dungeon {
rooms: Rooms,
/// The pits in the dungeon.
pits: List(Pit),
/// The obstacles in the dungeon.
obstacles: List(Obstacle),
)
}

Expand All @@ -62,7 +69,8 @@ pub fn generate_dungeon() -> Dungeon {
|> break_walls
|> compute_corner_walls
let pits = generate_pits(rooms)
Dungeon(rooms, pits)
let obstacles = generate_obstacles(rooms, pits)
Dungeon(rooms, pits, obstacles)
}

// Recursively generate rooms by randomly deciding to traverse each direction.
Expand All @@ -88,8 +96,8 @@ fn generate_rooms(
use <- bool.guard(recursion_depth > max_depth, rooms)

let advance_direction = fn(rooms: Rooms, direction: room.Direction) -> Rooms {
// Do not try to go back where you came from
use <- bool.guard(
// Do not try to go back where you came from
direction == previous_direction && recursion_depth != 0,
rooms,
)
Expand Down Expand Up @@ -164,8 +172,8 @@ fn break_walls(rooms: Rooms) -> Rooms {
use rooms, #(column, row), _ <- dict.fold(rooms, rooms)

let break_in_direction = fn(rooms: Rooms, dir: room.Direction) -> Rooms {
// Check if there is a room to the right
let #(next_column, next_row) = next_room_indices(column, row, dir)
// Check if there is a room to the right
use <- bool.guard(coordinate_out_of_bounds(next_column, next_row), rooms)

let next_room = dict.get(rooms, #(next_column, next_row))
Expand Down Expand Up @@ -242,12 +250,61 @@ fn compute_corner_walls(rooms: Rooms) -> Rooms {
fn generate_pits(rooms: Rooms) -> List(Pit) {
list.range(1, pit_count)
|> list.fold([], fn(pits, _) {
// Get a location to place the pit
let position = get_location_to_place_pit(rooms, pits)
// Get a location to place the pit
[pit.Pit(position, pit_size), ..pits]
})
}

// Creates obstacles in random rooms in the dungeon.
fn generate_obstacles(rooms: Rooms, pits: List(Pit)) -> List(Obstacle) {
use obstacles, coordinate, _ <- dict.fold(rooms, [])

// Roll if room should have obstacles
let obstacle_roll: Float =
random.float(0.0, 1.0)
|> random.random_sample
use <- bool.guard(obstacle_roll >. obstacle_rate, obstacles)

let point = coordinate_to_point(coordinate)

// Roll number of obstacles to place
let obstacle_count: Int =
random.int(0, obstacle_limit)
|> random.random_sample

use obstacles, _ <- list.fold(list.range(1, obstacle_count), obstacles)

case attempt_place_obstacle(point, pits, 0) {
Error(_) -> obstacles
Ok(obstacle) -> [obstacle, ..obstacles]
}
}

// Attempts to place an obstacle in a room. Returns an error if it fails.
fn attempt_place_obstacle(
point: vector.Vector,
pits: List(Pit),
tries: Int,
) -> Result(Obstacle, Nil) {
use <- bool.guard(tries > 3, Error(Nil))
// Get a random offset from the point
let max_offset = { int.to_float(room_size) -. obstacle.size } /. 2.0
let offset_x =
random.float(max_offset *. -1.0, max_offset)
|> random.random_sample
let offset_y =
random.float(max_offset *. -1.0, max_offset)
|> random.random_sample
let position = vector.add(point, vector.Vector(offset_x, offset_y, 0.0))

// Check if the obstacle is too close to a pit
case too_close_to_existing_pit(pits, position) {
True -> attempt_place_obstacle(point, pits, tries + 1)
False -> Ok(obstacle.Obstacle(position))
}
}

// Check if a position is too close to an existing pit.
fn too_close_to_existing_pit(pits: List(Pit), position: vector.Vector) -> Bool {
use pit <- list.any(pits)
Expand Down Expand Up @@ -327,6 +384,12 @@ pub fn draw(p: P5, dungeon: Dungeon) {
use pit <- list.each(dungeon.pits)
pit.draw(p, pit)
}

// Draw obstacles
{
use obstacle <- list.each(dungeon.obstacles)
obstacle.draw(p, obstacle)
}
}

/// Get the coordinate that the given point is in.
Expand Down
50 changes: 50 additions & 0 deletions src/obstacle.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import p5js_gleam.{type P5}
import p5js_gleam/bindings as p5
import vector.{type Vector}

pub const size = 6.0

pub const damage = 10

/// Represents an obstacle that the player must avoid.
pub type Obstacle {
/// Represents an obstacle that the player must avoid.
Obstacle(
/// The position of the obstacle.
position: Vector,
)
}

/// Checks if the object at the given position with given radius collides with the bullet.
pub fn collides_with(
obstacle: Obstacle,
position: Vector,
object_size: Float,
) -> Bool {
vector.distance(obstacle.position, position)
<. size /. 2.0 +. object_size /. 2.0
}

/// Renders the obstacle to the screen
pub fn draw(p: P5, obstacle: Obstacle) {
let position = obstacle.position
let spike1 = 4.0
let spike2 = 5.0
p5.stroke(p, "#f4424b")
|> p5.fill("#f4424b")
|> p5.circle(position.x, position.y, size)
|> p5.line(
position.x -. spike1,
position.y -. spike1,
position.x +. spike1,
position.y +. spike1,
)
|> p5.line(
position.x -. spike1,
position.y +. spike1,
position.x +. spike1,
position.y -. spike1,
)
|> p5.line(position.x, position.y -. spike2, position.x, position.y +. spike2)
|> p5.line(position.x -. spike2, position.y, position.x +. spike2, position.y)
}
26 changes: 21 additions & 5 deletions test/dungeon_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -91,15 +91,29 @@ fn validate_pits(d: dungeon.Dungeon) {
d
}

fn validate_obstacles(d: dungeon.Dungeon) {
let pits = d.pits
let obstacles = d.obstacles

{
use pit <- list.each(pits)
use obstacle <- list.each(obstacles)

expect.to_be_true(vector.distance(pit.position, obstacle.position) >. 80.0)
}

d
}

pub fn dungeon_tests() {
describe("dungeon", [
it("generate_dungeon", fn() {
{
use <- repeatedly

dungeon.generate_dungeon()
|> validate_room_is_traversable
|> validate_pits
|> validate_obstacles
}
|> take(30)

Expand All @@ -120,7 +134,7 @@ pub fn dungeon_tests() {
room.initialize_unbounded_room()
|> room.set_navigable(room.Top, True),
)
let dungeon = dungeon.Dungeon(rooms: rooms, pits: [])
let dungeon = dungeon.Dungeon(rooms: rooms, pits: [], obstacles: [])

// to out of bounds
expect.to_be_false(dungeon.can_move(
Expand Down Expand Up @@ -181,9 +195,11 @@ pub fn dungeon_tests() {
|> room.set_navigable(room.Top, True),
)
let dungeon =
dungeon.Dungeon(rooms: rooms, pits: [
pit.Pit(position: vector.Vector(0.0, 0.0, 0.0), size: 30.0),
])
dungeon.Dungeon(
rooms: rooms,
pits: [pit.Pit(position: vector.Vector(0.0, 0.0, 0.0), size: 30.0)],
obstacles: [],
)

expect.to_be_true(dungeon.is_over_pit(
dungeon,
Expand Down
27 changes: 27 additions & 0 deletions test/obstacle_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import obstacle.{type Obstacle, Obstacle}
import startest.{describe, it}
import startest/expect
import vector.{Vector}

pub fn obstacle_tests() {
describe("obstacle", [
it("collides_with", fn() {
expect.to_equal(
obstacle.collides_with(
Obstacle(Vector(0.0, 0.0, 0.0)),
Vector(10.0, 0.0, 0.0),
1.0,
),
False,
)
expect.to_equal(
obstacle.collides_with(
Obstacle(Vector(0.0, 0.0, 0.0)),
Vector(0.0, 0.0, 0.0),
1.0,
),
True,
)
}),
])
}

0 comments on commit 5f3d8a6

Please sign in to comment.