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"
"sync"
"syscall"
"time"
"code.haedhutner.dev/mvv/LastMUD/internal/server"
@ -57,5 +58,7 @@ func processInput() {
if buf[0] == 'q' {
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
}
func CreateEventBus() *EventBus {
func CreateEventBus(capacity int) *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) {
game.world.AddPlayerToDefaultRoom(CreatePlayer(pje.connectionId, nil))
game.enqeueOutput(game.CreateOutput(pje.connectionId, []byte("Welcome to LastMUD!")))
p := CreateJoiningPlayer(pje.connectionId)
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 {
@ -51,7 +53,7 @@ type PlayerCommandEvent struct {
}
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 {
return nil, err
@ -77,17 +79,23 @@ func (pce *PlayerCommandEvent) Handle(game *LastMUDGame, delta time.Duration) {
return
}
event := pce.parseCommandIntoEvent(game, player)
}
func (pce *PlayerCommandEvent) parseCommandIntoEvent(game *LastMUDGame, player *Player) GameEvent {
switch pce.command.Command().Definition().Name() {
case SayCommand:
speech, err := pce.command.Command().Parameters()[0].AsString()
if err != nil {
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 {

View file

@ -12,6 +12,9 @@ import (
const TickRate = time.Duration(50 * time.Millisecond)
const MaxEnqueuedOutputPerTick = 100
const MaxEnqueuedGameEventsPerTick = 100
type GameOutput struct {
connId uuid.UUID
contents []byte
@ -36,7 +39,8 @@ type LastMUDGame struct {
ctx context.Context
wg *sync.WaitGroup
commandRegistry *command.CommandRegistry
cmdRegistry *command.CommandRegistry
world *World
eventBus *EventBus
@ -48,12 +52,12 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
game = &LastMUDGame{
wg: wg,
ctx: ctx,
eventBus: CreateEventBus(),
output: make(chan GameOutput, 10),
eventBus: CreateEventBus(MaxEnqueuedGameEventsPerTick),
output: make(chan GameOutput, MaxEnqueuedOutputPerTick),
world: CreateWorld(),
}
game.commandRegistry = game.CreateGameCommandRegistry()
game.cmdRegistry = game.CreateGameCommandRegistry()
wg.Add(1)
go game.start()
@ -61,6 +65,23 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
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() {
defer game.wg.Done()
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) {
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) {
for {
event := g.eventBus.Pop()

View file

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

View file

@ -1 +0,0 @@
package game

View file

@ -11,7 +11,11 @@ import (
"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 {
ctx context.Context
@ -53,6 +57,13 @@ func (c *Connection) Id() uuid.UUID {
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() {
defer c.wg.Done()
@ -100,6 +111,8 @@ func (c *Connection) checkAlive() {
if err != nil {
break
}
time.Sleep(CheckAlivePeriod)
}
}
@ -119,10 +132,3 @@ func (c *Connection) closeConnection() {
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
}