This commit is contained in:
Miroslav Vasilev 2025-06-24 16:37:26 +03:00
parent fff70cc8b3
commit 87f5c2f842
14 changed files with 153 additions and 54 deletions

View file

@ -7,6 +7,7 @@ import (
"os/signal" "os/signal"
"sync" "sync"
"syscall" "syscall"
"time"
"code.haedhutner.dev/mvv/LastMUD/internal/server" "code.haedhutner.dev/mvv/LastMUD/internal/server"
@ -57,5 +58,7 @@ func processInput() {
if buf[0] == 'q' { if buf[0] == 'q' {
return return
} }
time.Sleep(50 * time.Millisecond)
} }
} }

BIN
internal/game/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,17 @@
package db
import "github.com/google/uuid"
type Identifier = uuid.UUID
type Entity interface {
Id() Identifier
}
type Repository[T Entity] interface {
Create(entity T) (rowsAffected int, err error)
Delete(entity T) (rowsAffected int, err error)
Update(entity T) (rowsAffected int, err error)
FetchOne(id Identifier) (entity T, err error)
FetchAll() (entities []T, err error)
}

View file

@ -0,0 +1,31 @@
package ecs
type PlayerState = byte
const (
PlayerStateJoining PlayerState = iota
PlayerStateLoggingIn
PlayerStateRegistering
PlayerStatePlaying
PlayerStateLeaving
)
type PlayerStateComponent struct {
State PlayerState
}
type NameComponent struct {
Name string
}
type DescriptionComponent struct {
Description string
}
type InRoomComponent struct {
InRoom Entity
}
type NeighboringRoomsComponent struct {
North, South, East, West Entity
}

View file

@ -0,0 +1,19 @@
package ecs
import "github.com/google/uuid"
type Entity uuid.UUID
type Room struct {
Entity
NameComponent
DescriptionComponent
NeighboringRoomsComponent
}
type Player struct {
Entity
PlayerStateComponent
NameComponent
InRoomComponent
}

View file

@ -0,0 +1 @@
package ecs

View file

@ -0,0 +1,15 @@
package ecs
type World struct {
Players []*Player
Rooms []*Room
DefaultRoom *Room
}
func CreateWorld() *World {
world := &World{
Players: []*Player{},
Rooms: []*Room{},
}
}

View file

@ -45,9 +45,9 @@ type EventBus struct {
events chan GameEvent events chan GameEvent
} }
func CreateEventBus() *EventBus { func CreateEventBus(capacity int) *EventBus {
return &EventBus{ return &EventBus{
events: make(chan GameEvent, 10), events: make(chan GameEvent, capacity),
} }
} }

View file

@ -23,8 +23,10 @@ func (pje *PlayerJoinEvent) Type() EventType {
} }
func (pje *PlayerJoinEvent) Handle(game *LastMUDGame, delta time.Duration) { func (pje *PlayerJoinEvent) Handle(game *LastMUDGame, delta time.Duration) {
game.world.AddPlayerToDefaultRoom(CreatePlayer(pje.connectionId, nil)) p := CreateJoiningPlayer(pje.connectionId)
game.enqeueOutput(game.CreateOutput(pje.connectionId, []byte("Welcome to LastMUD!"))) game.world.AddPlayerToDefaultRoom(p)
game.enqeueOutput(game.CreateOutput(p.Identity(), []byte("Welcome to LastMUD!")))
game.enqeueOutput(game.CreateOutput(p.Identity(), []byte("Please enter your name:")))
} }
type PlayerLeaveEvent struct { type PlayerLeaveEvent struct {
@ -51,7 +53,7 @@ type PlayerCommandEvent struct {
} }
func (game *LastMUDGame) CreatePlayerCommandEvent(connId uuid.UUID, cmdString string) (event *PlayerCommandEvent, err error) { func (game *LastMUDGame) CreatePlayerCommandEvent(connId uuid.UUID, cmdString string) (event *PlayerCommandEvent, err error) {
cmdCtx, err := command.CreateCommandContext(game.CommandRegistry(), cmdString) cmdCtx, err := command.CreateCommandContext(game.commandRegistry(), cmdString)
if err != nil { if err != nil {
return nil, err return nil, err
@ -77,17 +79,23 @@ func (pce *PlayerCommandEvent) Handle(game *LastMUDGame, delta time.Duration) {
return return
} }
event := pce.parseCommandIntoEvent(game, player)
}
func (pce *PlayerCommandEvent) parseCommandIntoEvent(game *LastMUDGame, player *Player) GameEvent {
switch pce.command.Command().Definition().Name() { switch pce.command.Command().Definition().Name() {
case SayCommand: case SayCommand:
speech, err := pce.command.Command().Parameters()[0].AsString() speech, err := pce.command.Command().Parameters()[0].AsString()
if err != nil { if err != nil {
logging.Error("Unable to handle player speech from player with id", pce.connectionId, ": Speech could not be parsed: ", err.Error()) logging.Error("Unable to handle player speech from player with id", pce.connectionId, ": Speech could not be parsed: ", err.Error())
return return nil
} }
game.EnqueueEvent(game.CreatePlayerSayEvent(player, speech)) return game.CreatePlayerSayEvent(player, speech)
} }
return nil
} }
type PlayerSayEvent struct { type PlayerSayEvent struct {

View file

@ -12,6 +12,9 @@ import (
const TickRate = time.Duration(50 * time.Millisecond) const TickRate = time.Duration(50 * time.Millisecond)
const MaxEnqueuedOutputPerTick = 100
const MaxEnqueuedGameEventsPerTick = 100
type GameOutput struct { type GameOutput struct {
connId uuid.UUID connId uuid.UUID
contents []byte contents []byte
@ -36,7 +39,8 @@ type LastMUDGame struct {
ctx context.Context ctx context.Context
wg *sync.WaitGroup wg *sync.WaitGroup
commandRegistry *command.CommandRegistry cmdRegistry *command.CommandRegistry
world *World world *World
eventBus *EventBus eventBus *EventBus
@ -48,12 +52,12 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
game = &LastMUDGame{ game = &LastMUDGame{
wg: wg, wg: wg,
ctx: ctx, ctx: ctx,
eventBus: CreateEventBus(), eventBus: CreateEventBus(MaxEnqueuedGameEventsPerTick),
output: make(chan GameOutput, 10), output: make(chan GameOutput, MaxEnqueuedOutputPerTick),
world: CreateWorld(), world: CreateWorld(),
} }
game.commandRegistry = game.CreateGameCommandRegistry() game.cmdRegistry = game.CreateGameCommandRegistry()
wg.Add(1) wg.Add(1)
go game.start() go game.start()
@ -61,6 +65,23 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
return return
} }
func (game *LastMUDGame) EnqueueEvent(event GameEvent) {
game.eventBus.Push(event)
}
func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
select {
case output := <-game.output:
return &output
default:
return nil
}
}
func (game *LastMUDGame) commandRegistry() *command.CommandRegistry {
return game.cmdRegistry
}
func (game *LastMUDGame) start() { func (game *LastMUDGame) start() {
defer game.wg.Done() defer game.wg.Done()
defer game.shutdown() defer game.shutdown()
@ -102,27 +123,10 @@ func (game *LastMUDGame) shouldStop() bool {
} }
} }
func (game *LastMUDGame) EnqueueEvent(event GameEvent) {
game.eventBus.Push(event)
}
func (game *LastMUDGame) enqeueOutput(output GameOutput) { func (game *LastMUDGame) enqeueOutput(output GameOutput) {
game.output <- output game.output <- output
} }
func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
select {
case output := <-game.output:
return &output
default:
return nil
}
}
func (game *LastMUDGame) CommandRegistry() *command.CommandRegistry {
return game.commandRegistry
}
func (g *LastMUDGame) tick(delta time.Duration) { func (g *LastMUDGame) tick(delta time.Duration) {
for { for {
event := g.eventBus.Pop() event := g.eventBus.Pop()

View file

@ -5,12 +5,23 @@ import "github.com/google/uuid"
type Player struct { type Player struct {
id uuid.UUID id uuid.UUID
state PlayerState
currentRoom *Room currentRoom *Room
} }
func CreatePlayer(identity uuid.UUID, room *Room) *Player { func CreateJoiningPlayer(identity uuid.UUID) *Player {
return &Player{ return &Player{
id: identity, id: identity,
state: PlayerStateJoining,
currentRoom: nil,
}
}
func CreatePlayer(identity uuid.UUID, state PlayerState, room *Room) *Player {
return &Player{
id: identity,
state: state,
currentRoom: room, currentRoom: room,
} }
} }

View file

@ -1 +0,0 @@
package game

View file

@ -11,7 +11,11 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
const MaxLastSeenTime = 120 * time.Second const MaxLastSeenTime = 90 * time.Second
const CheckAlivePeriod = 50 * time.Millisecond
const DeleteBeforeAndMoveToStartOfLine = "\033[1K\r"
type Connection struct { type Connection struct {
ctx context.Context ctx context.Context
@ -53,6 +57,13 @@ func (c *Connection) Id() uuid.UUID {
return c.identity return c.identity
} }
func (c *Connection) Write(output []byte) (err error) {
output = append([]byte(DeleteBeforeAndMoveToStartOfLine+"< "), output...)
output = append(output, []byte("\n> ")...)
_, err = c.conn.Write(output)
return
}
func (c *Connection) listen() { func (c *Connection) listen() {
defer c.wg.Done() defer c.wg.Done()
@ -100,6 +111,8 @@ func (c *Connection) checkAlive() {
if err != nil { if err != nil {
break break
} }
time.Sleep(CheckAlivePeriod)
} }
} }
@ -119,10 +132,3 @@ func (c *Connection) closeConnection() {
logging.Info("Disconnected: ", c.conn.RemoteAddr()) logging.Info("Disconnected: ", c.conn.RemoteAddr())
} }
func (c *Connection) Write(output []byte) (err error) {
output = append([]byte("< "), output...)
output = append(output, []byte("\n> ")...)
_, err = c.conn.Write(output)
return
}

View file

@ -1,15 +0,0 @@
package server
type inputEmptyError struct {
msg string
}
func newInputEmptyError() *inputEmptyError {
return &inputEmptyError{
msg: "No input available at this moment",
}
}
func (err *inputEmptyError) Error() string {
return err.msg
}