Skip to content

Commit

Permalink
Add simulation actor and game connection actors
Browse files Browse the repository at this point in the history
  • Loading branch information
nicklanng committed Mar 20, 2024
1 parent e86291c commit d63f74e
Show file tree
Hide file tree
Showing 15 changed files with 412 additions and 97 deletions.
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ gleam_stdlib = "~> 0.34 or ~> 1.0"
glisten = { path = "../glisten" }
gleam_otp = "~> 0.10"
gleam_erlang = "~> 0.24"
repeatedly = "~> 2.1"

[dev-dependencies]
gleeunit = "~> 1.0"
2 changes: 2 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ packages = [
{ name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
{ name = "glisten", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], source = "local", path = "../glisten" },
{ name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" },
]

[requirements]
Expand All @@ -15,3 +16,4 @@ gleam_otp = { version = "~> 0.10" }
gleam_stdlib = { version = "~> 0.34 or ~> 1.0" }
gleeunit = { version = "~> 1.0" }
glisten = { path = "../glisten" }
repeatedly = { version = "~> 2.1"}
55 changes: 55 additions & 0 deletions src/data/world.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import gleam/dict.{type Dict}
import gleam/otp/task
import model/core

pub type DataError {
DataError
}

pub type RoomTemplate {
RoomTemplate(
name: String,
description: String,
exits: Dict(core.Direction, core.Location),
)
}

pub type RegionTemplate {
RegionTemplate(name: String, rooms: Dict(String, RoomTemplate))
}

fn add_room(region: RegionTemplate, id: String, room: RoomTemplate) {
RegionTemplate(..region, rooms: dict.insert(region.rooms, id, room))
}

pub type WorldTemplate {
WorldTemplate(regions: Dict(String, RegionTemplate))
}

fn add_region(world: WorldTemplate, id: String, region: RegionTemplate) {
WorldTemplate(regions: dict.insert(world.regions, id, region))
}

pub fn load_world() -> Result(WorldTemplate, DataError) {
let handle =
task.async(fn() {
// this will eventually call out to the file system
Ok(
WorldTemplate(regions: dict.new())
|> add_region(
"testregion",
RegionTemplate(name: "Test Region", rooms: dict.new())
|> add_room(
"testroom",
RoomTemplate(
name: "Test Room",
description: "An empty test room",
exits: dict.new(),
),
),
),
)
})

task.await(handle, 60_000)
}
10 changes: 9 additions & 1 deletion src/gleamud.gleam
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import gleam/erlang/process
import gleam/io
import telnet/server
import model/simulation
import repeatedly

pub fn main() {
let assert Ok(_) = server.start(3000)
let assert Ok(sim_subject) = simulation.start()
let assert Ok(_) = server.start(3000, sim_subject)

let _ =
repeatedly.call(100, Nil, fn(_state, _i) {
process.send(sim_subject, simulation.Tick)
})

io.println("Connect with:")
io.println(" telnet localhost 3000")
Expand Down
16 changes: 16 additions & 0 deletions src/model/core.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
pub type Direction {
North
East
South
West
NorthEast
SouthEast
SouthWest
NorthWest
Up
Down
}

pub type Location {
Location(region: String, room: String)
}
File renamed without changes.
17 changes: 17 additions & 0 deletions src/model/prefabs.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import gleam/int
import gleam/option.{None}
import model/entity

pub fn create_guest_player() {
entity.new([
entity.Named(name: "Guest" <> int.to_string(int.random(99_999))),
entity.Physical(hp: 10, size: 0),
entity.PaperDollHead(entity: None),
entity.PaperDollBack(entity: None),
entity.PaperDollChest(entity: None),
entity.PaperDollPrimaryHand(entity: None),
entity.PaperDollOffHand(entity: None),
entity.PaperDollLegs(entity: None),
entity.PaperDollFeet(entity: None),
])
}
53 changes: 53 additions & 0 deletions src/model/simulation.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import gleam/erlang/process.{type Subject}
import gleam/otp/actor
import data/world
import gleam/io

type State {
State(world_template: world.WorldTemplate)
}

/// Control message are sent by top level actors to control the sim directly
pub type Control {
JoinAsGuest(Subject(Update))
Tick
Shutdown
}

/// Commands are sent from game connections to entities
pub type Command {
Look
}

/// Updates are sent from entities to game connections
pub type Update {
CommandSubject(Subject(Command))
// RoomDescription
}

pub fn start() -> Result(Subject(Control), actor.StartError) {
// data loading
let assert Ok(world) = world.load_world()

actor.start(State(world), handle_message)
}

pub fn stop(subject: Subject(Control)) {
process.send(subject, Shutdown)
}

fn handle_message(message: Control, state: State) -> actor.Next(Control, State) {
case message {
Tick -> {
// io.println("tick")
actor.continue(state)
}

JoinAsGuest(subject) -> {
io.debug("continue as guest")
actor.continue(state)
}

Shutdown -> actor.Stop(process.Normal)
}
}
40 changes: 40 additions & 0 deletions src/model/types.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// import gleam/dict.{type Dict}
// import model/entity.{type Entity}

// pub type Direction {
// North
// East
// South
// West
// NorthEast
// SouthEast
// SouthWest
// NorthWest
// Up
// Down
// }

// pub type Location {
// Location(region: String, room: String)
// }

// pub type RoomTemplate {
// RoomTemplate(name: String, description: String, exits: Dict(Direction, Exit))
// }

// pub type RegionTemplate {
// RegionTemplate(name: String, rooms: Dict(String, RoomTemplate))
// }

// pub type Room {
// Room(location: Location, entities: List(Entity))
// }

// pub type Region {
// Region(name: String, rooms: Dict(String, Room))
// RegionInstance(name: String, rooms: Dict(String, Room))
// }

// pub type World {
// World(templates: Dict(String, RegionTemplate), regions: Dict(String, Region))
// }
44 changes: 0 additions & 44 deletions src/simulation/types.gleam

This file was deleted.

121 changes: 121 additions & 0 deletions src/telnet/game_connection.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import gleam/erlang/process.{type Selector, type Subject}
import gleam/option.{None, Some}
import gleam/otp/actor
import telnet/states/states
import telnet/states/menu
import model/simulation
import gleam/function
import glisten

pub type Message {
Dimensions(Int, Int)
Data(String)
Update(simulation.Update)
}

pub fn start(
parent_subject: Subject(Subject(Message)),
sim_subject: Subject(simulation.Control),
conn: glisten.Connection(BitArray),
) -> Result(Subject(Message), actor.StartError) {
actor.start_spec(actor.Spec(
init: fn() {
let tcp_subject = process.new_subject()
process.send(parent_subject, tcp_subject)

let selector =
process.new_selector()
|> process.selecting(tcp_subject, function.identity)

actor.Ready(
#(
tcp_subject,
selector,
states.FirstIAC(
conn: conn,
dimensions: states.ClientDimensions(80, 24),
directory: states.Directory(
sim_subject: sim_subject,
command_subject: None,
),
),
),
selector,
)
},
init_timeout: 1000,
loop: handle_message,
))
}

fn handle_message(
message: Message,
state: #(Subject(Message), Selector(Message), states.State),
) -> actor.Next(Message, #(Subject(Message), Selector(Message), states.State)) {
case message {
Dimensions(width, height) ->
handle_dimensions(state, width, height)
|> actor.continue()
Data(str) ->
handle_data(state, str)
|> actor.continue()
Update(update) ->
handle_update(state, update)
|> actor.continue()
}
}

fn handle_dimensions(
state: #(Subject(Message), Selector(Message), states.State),
width: Int,
height: Int,
) -> #(Subject(Message), Selector(Message), states.State) {
#(state.0, state.1, case state.2 {
states.FirstIAC(conn, _, directory) ->
states.Menu(conn, states.ClientDimensions(width, height), directory)
|> menu.on_enter()
states.Menu(conn, _, directory) ->
states.Menu(conn, states.ClientDimensions(width, height), directory)
states.InWorld(conn, _, directory) ->
states.InWorld(conn, states.ClientDimensions(width, height), directory)
})
}

fn handle_data(
state: #(Subject(Message), Selector(Message), states.State),
str: String,
) -> #(Subject(Message), Selector(Message), states.State) {
case state.2 {
states.FirstIAC(_, _, _) -> state
states.Menu(_, _, _) -> {
let #(new_state, command_subject) =
state.2
|> menu.handle_input(str)
case command_subject {
Some(subject) -> #(
state.0,
process.new_selector()
|> process.selecting(state.0, function.identity)
|> process.selecting(subject, fn(update) { Update(update) }),
new_state,
)
None -> #(state.0, state.1, new_state)
}
}
states.InWorld(_, _, _) -> state
}
}

fn handle_update(
state: #(Subject(Message), Selector(Message), states.State),
update: simulation.Update,
) -> #(Subject(Message), Selector(Message), states.State) {
case update {
simulation.CommandSubject(subject) -> #(
state.0,
state.1,
state.2
|> states.with_command_subject(subject),
)
}
}
Loading

0 comments on commit d63f74e

Please sign in to comment.