diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bed04b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +c.out +main +**/*.log \ No newline at end of file diff --git a/cmd/common/common.go b/cmd/common/common.go index c63d1f5..5cfe099 100644 --- a/cmd/common/common.go +++ b/cmd/common/common.go @@ -6,6 +6,7 @@ type Coordinates struct { } const ( + LogFile = "pirate-wars.log" WorldWidth = 600 WorldHeight = 600 TotalTowns = 30 @@ -28,3 +29,47 @@ type ViewPort struct { height int topLeft int } + +//func CreateNewLogger(filename string, prefix string) *log.Logger { +// f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0664) +// if err != nil { +// log.Fatalf("error opening file: %v", err) +// } +// defer f.Close() +// +// logger := log.New(f, filename, log.LstdFlags) +// logger.SetPrefix(fmt.Sprintf("%v: ", prefix)) +// logger.Println("TEST 1234") +// return logger +//} + +//func CreateLogger(filename string, namespace string) func(string) { +// f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) +// if err != nil { +// log.Fatal(err) +// } +// defer f.Close() +// +// logger := log.New(f, namespace, log.LstdFlags) +// return func(msg string) { +// d := godump.Dumper{} +// logger.Println(d.Sprint(msg)) +// } +//} +// +//func InitLogger(name string) { +// CreateLogger(LogFile, name) +//} + +//func Logger(name string) log { +// var logger *os.File +// if _, ok := os.LookupEnv("DEBUG"); ok { +// var err error +// logger, err = os.OpenFile(LogFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) +// if err != nil { +// os.Exit(1) +// } +// } +// spew.Fdump(m.dump, msg) +// return logger +//} diff --git a/cmd/terrain/terrain.go b/cmd/terrain/terrain.go index d887be7..d32a633 100644 --- a/cmd/terrain/terrain.go +++ b/cmd/terrain/terrain.go @@ -6,8 +6,8 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/table" "github.com/ojrac/opensimplex-go" + "go.uber.org/zap" "math/rand" - "pirate-wars/cmd/avatar" "pirate-wars/cmd/common" "pirate-wars/cmd/town" ) @@ -17,7 +17,7 @@ import ( // Boats: ⏅ ⏏ ⏚ ⏛ ⏡ ⪮ ⩯ ⩠ ⩟ ⅏ // People: 옷 -type Terrain struct { +type Props struct { width int height int scale float64 @@ -26,7 +26,14 @@ type Terrain struct { octaves int } -type Type int +type Terrain struct { + logger *zap.SugaredLogger + props Props + World MapView + MiniMap MapView +} + +type TerrainType int type TypeQualities struct { symbol rune @@ -35,7 +42,7 @@ type TypeQualities struct { RequiresBoat bool } -var TypeLookup = map[Type]TypeQualities{ +var TypeLookup = map[TerrainType]TypeQualities{ common.TypeDeepWater: {symbol: '⏖', style: createTerrainItem("18"), Passable: true, RequiresBoat: true}, common.TypeOpenWater: {symbol: '⏝', style: createTerrainItem("20"), Passable: true, RequiresBoat: true}, common.TypeShallowWater: {symbol: '⏑', style: createTerrainItem("26"), Passable: true, RequiresBoat: true}, @@ -47,68 +54,113 @@ var TypeLookup = map[Type]TypeQualities{ common.TypeTown: {symbol: '⩎', style: createTerrainItem("0"), Passable: true, RequiresBoat: false}, } -type World [][]Type +type MapView struct { + grid [][]TerrainType + logger *zap.SugaredLogger + isMiniMap bool +} func createTerrainItem(color lipgloss.Color) lipgloss.Style { return lipgloss.NewStyle().Background(color).Padding(0).Margin(0) } -func Init() *Terrain { +func Init(logger *zap.SugaredLogger) *Terrain { + logger.Info("Initializing terrain") //default values for terrain map generation - t := Terrain{ - width: common.WorldWidth, - height: common.WorldHeight, - scale: 60, - lacunarity: 2.0, - persistence: 0.5, - octaves: 5, + worldGrid := make([][]TerrainType, common.WorldHeight) + for i := range worldGrid { + worldGrid[i] = make([]TerrainType, common.WorldHeight) + } + + // Calculate MiniMap dimensions + height := len(worldGrid) / common.MiniMapFactor + width := len(worldGrid[0]) / common.MiniMapFactor + + // Create new 2D slice + miniMap := make([][]TerrainType, height+1) + for i := range miniMap { + miniMap[i] = make([]TerrainType, width+1) + } + + return &Terrain{ + logger: logger, + props: Props{ + width: common.WorldWidth, + height: common.WorldHeight, + scale: 60, + lacunarity: 2.0, + persistence: 0.5, + octaves: 5, + }, + World: MapView{ + isMiniMap: false, + logger: logger, + grid: worldGrid, + }, + MiniMap: MapView{ + isMiniMap: true, + logger: logger, + grid: miniMap, + }, } - return &t } -func (tt Type) Render() string { +func (tt TerrainType) Render() string { return fmt.Sprintf(TypeLookup[tt].style.PaddingLeft(1).PaddingRight(1).Render("%c"), TypeLookup[tt].symbol) } -func (tt Type) IsPassableByBoat() bool { - if TypeLookup[tt].RequiresBoat { - return true - } - return false +func (t Terrain) genTownCoords() common.Coordinates { + coords := common.Coordinates{X: rand.Intn(common.WorldWidth), Y: rand.Intn(common.WorldHeight)} + t.logger.Debug(fmt.Sprintf("Generating random town coordinates: %v", coords)) + return coords } -func GenerateTowns(world World, count int) { - for i := 0; i <= count; i++ { +func (t *Terrain) generateTowns(fn func() common.Coordinates) { + t.logger.Info("Initializing %v towns", common.TotalTowns) + for i := 0; i <= common.TotalTowns; i++ { for { - coords := common.Coordinates{X: min(rand.Intn(common.WorldWidth-5), 5), Y: min(rand.Intn(common.WorldHeight-5), 5)} + coords := fn() + adjacentCoords := t.World.GetAdjacentCoords(coords) if coords.X > 1 && coords.Y > 1 && coords.X < common.WorldWidth-1 && coords.Y < common.WorldHeight && - world[coords.X][coords.Y] == common.TypeBeach && - (world[coords.X+1][coords.Y] == common.TypeShallowWater || - world[coords.X][coords.Y+1] == common.TypeShallowWater || - world[coords.X-1][coords.Y] == common.TypeShallowWater || - world[coords.X][coords.Y-1] == common.TypeShallowWater) { - town.Create(coords, '⩎') - break + t.World.grid[coords.X][coords.Y] == common.TypeBeach { + + isAdjacentWater := false + for _, a := range adjacentCoords { + if t.World.grid[a.X][a.Y] == common.TypeShallowWater { + isAdjacentWater = true + break + } + } + if isAdjacentWater { + town.Create(coords, '⩎') + t.World.grid[coords.X][coords.Y] = common.TypeTown + // grow towns + for _, a := range adjacentCoords { + if t.World.grid[a.X][a.Y] == common.TypeLowland || t.World.grid[a.X][a.Y] == common.TypeBeach { + t.World.grid[a.X][a.Y] = common.TypeTown + } + } + break + } } } } } -func (t *Terrain) Generate() World { - //var world [WorldWidth][WorldHeight]Type - world := make([][]Type, common.WorldHeight) - for i := range world { - world[i] = make([]Type, common.WorldHeight) - } +func (t *Terrain) GenerateTowns() { + t.generateTowns(t.genTownCoords) +} +func (t *Terrain) GenerateWorld() { + t.logger.Info("Initializing world") noise := opensimplex.New(rand.Int63()) - for x := 0; x < t.width; x++ { - for y := 0; y < t.height; y++ { + for x := 0; x < t.props.width; x++ { + for y := 0; y < t.props.height; y++ { //sample x and y and apply scale - xFloat := float64(x) / t.scale - yFloat := float64(y) / t.scale + xFloat := float64(x) / t.props.scale + yFloat := float64(y) / t.props.scale //init values for octave calculation frequency := 1.0 @@ -117,45 +169,39 @@ func (t *Terrain) Generate() World { total := 0.0 //octave calculation - for i := 0; i < t.octaves; i++ { + for i := 0; i < t.props.octaves; i++ { total += noise.Eval2(xFloat*frequency, yFloat*frequency) * amplitude normalizeOctaves += amplitude - amplitude *= t.persistence - frequency *= t.lacunarity + amplitude *= t.props.persistence + frequency *= t.props.lacunarity } //normalize to -1 to 1, and then from 0 to 1 (this is for the ability to use grayscale, if using colors could keep from -1 to 1) var s = (total/normalizeOctaves + 1) / 2 if s > 0.60 { - world[x][y] = common.TypeDeepWater + t.World.grid[x][y] = common.TypeDeepWater } else if s > 0.46 { - world[x][y] = common.TypeOpenWater + t.World.grid[x][y] = common.TypeOpenWater } else if s > 0.42 { - world[x][y] = common.TypeShallowWater + t.World.grid[x][y] = common.TypeShallowWater } else if s > 0.40 { - world[x][y] = common.TypeBeach + t.World.grid[x][y] = common.TypeBeach } else if s > 0.31 { - world[x][y] = common.TypeLowland + t.World.grid[x][y] = common.TypeLowland } else if s > 0.26 { - world[x][y] = common.TypeHighland + t.World.grid[x][y] = common.TypeHighland } else if s > 0.21 { - world[x][y] = common.TypeRock + t.World.grid[x][y] = common.TypeRock } else { - world[x][y] = common.TypePeak + t.World.grid[x][y] = common.TypePeak } } } - - GenerateTowns(world, common.TotalTowns) - for _, o := range town.List { - world[o.GetX()][o.GetY()] = common.TypeTown - } - - return world + t.GenerateMiniMap() } -func GetType(i int) (Type, error) { - for k, _ := range TypeLookup { +func GetType(i int) (TerrainType, error) { + for k := range TypeLookup { if int(k) == i { return k, nil } @@ -163,53 +209,94 @@ func GetType(i int) (Type, error) { return 0, errors.New("invalid type") } -func (world World) RenderMiniMap() World { - // Calculate new dimensions - height := len(world) / common.MiniMapFactor - width := len(world[0]) / common.MiniMapFactor - - // Create new 2D slice - newArr := make([][]Type, height+1) - for i := range newArr { - newArr[i] = make([]Type, width+1) - } - +func (t *Terrain) GenerateMiniMap() { // Down-sample - for i, row := range world { + for i, row := range t.World.grid { for j, val := range row { // Calculate corresponding index in new slice newI := i / common.MiniMapFactor newJ := j / common.MiniMapFactor // Assign original value - newArr[newI][newJ] = val + t.MiniMap.grid[newI][newJ] = val } } +} + +func (t *Terrain) RandomPositionDeepWater() common.Coordinates { + for { + coords := common.Coordinates{X: rand.Intn(common.WorldWidth), Y: rand.Intn(common.WorldHeight)} + if t.World.grid[coords.X][coords.Y] == common.TypeDeepWater { + return coords + } + } +} + +type AvatarReadOnly interface { + GetX() int + GetY() int + GetMiniMapX() int + GetMiniMapY() int + Render() string +} - return newArr +func (world MapView) GetTerrainType(x int, y int) TerrainType { + return world.grid[x][y] +} + +func (world MapView) GetAdjacentCoords(coords common.Coordinates) []common.Coordinates { + var adjacentCoords []common.Coordinates + for i := -1; i <= 1; i++ { + for j := -1; j <= 1; j++ { + if i == 0 && j == 0 { + continue + } + adjX := coords.X + i + adjY := coords.Y + j + if adjX < 0 || adjX >= world.GetWidth() || adjY < 0 || adjY >= world.GetHeight() { + continue + } + adjacentCoords = append(adjacentCoords, common.Coordinates{X: adjX, Y: adjY}) + } + } + return adjacentCoords } -func (world World) Paint(avatar avatar.Type, isMiniMap bool) string { +func (world MapView) GetWidth() int { + return len(world.grid[0]) +} + +func (world MapView) GetHeight() int { + return len(world.grid) +} + +func (world MapView) Paint(avatar AvatarReadOnly) string { left := 0 top := 0 - worldHeight := len(world) - worldWidth := len(world[0]) + worldHeight := len(world.grid) + worldWidth := len(world.grid[0]) viewHeight := worldHeight viewWidth := worldWidth rowWidth := worldWidth avatarX := avatar.GetX() avatarY := avatar.GetY() - if isMiniMap { + if world.isMiniMap { avatarX = avatar.GetMiniMapX() avatarY = avatar.GetMiniMapY() for _, o := range town.List { - world[o.GetMiniMapX()][o.GetMiniMapY()] = common.TypeTown + world.grid[o.GetMiniMapX()][o.GetMiniMapY()] = common.TypeTown } } else { // center viewport on avatar - left = avatar.GetX() - (common.ViewWidth / 2) - top = avatar.GetY() - (common.ViewHeight / 2) + left = avatarX - (common.ViewWidth / 2) + top = avatarY - (common.ViewHeight / 2) + if left < 0 { + left = 0 + } + if top < 0 { + top = 0 + } viewHeight = common.ViewHeight + top viewWidth = common.ViewWidth + left rowWidth = common.ViewWidth @@ -217,14 +304,16 @@ func (world World) Paint(avatar avatar.Type, isMiniMap bool) string { viewport := table.New().BorderBottom(false).BorderTop(false).BorderLeft(false).BorderRight(false) + world.logger.Debug(fmt.Sprintf("avatar position: X:%v Y:%v", avatarX, avatarY)) + world.logger.Debug(fmt.Sprintf("viewport: top:%v left:%v", top, left)) + world.logger.Debug(fmt.Sprintf("world: height:%v width:%v", worldHeight, worldWidth)) for y := top; y < worldHeight && y < viewHeight; y++ { var row = make([]string, rowWidth) for x := left; x < worldWidth && x < viewWidth; x++ { if x == avatarX && y == avatarY { row[x-left] = avatar.Render() } else { - //fmt.Printf("[%v , %v , %v ]\n", x, y, x-left) - row[x-left] = world[x][y].Render() + row[x-left] = world.grid[x][y].Render() } } viewport.Row(row...).BorderColumn(false) @@ -232,3 +321,11 @@ func (world World) Paint(avatar avatar.Type, isMiniMap bool) string { return fmt.Sprintln(viewport) } + +func (world MapView) IsPassableByBoat(coordinates common.Coordinates) bool { + tt := world.grid[coordinates.X][coordinates.Y] + if TypeLookup[tt].RequiresBoat { + return true + } + return false +} diff --git a/cmd/terrain/terrain_test.go b/cmd/terrain/terrain_test.go new file mode 100644 index 0000000..4b5fafc --- /dev/null +++ b/cmd/terrain/terrain_test.go @@ -0,0 +1,42 @@ +package terrain + +import ( + "go.uber.org/zap" + "testing" +) + +func initTestLogger() *zap.SugaredLogger { + logger, _ := zap.NewProduction() + return logger.Sugar() +} + +func cleanup() { +} + +type CoordinatesMock struct { + X int + Y int +} + +type AvatarMock struct { + pos CoordinatesMock + char rune +} + +func (av AvatarMock) Render() string { + return "" +} +func (av AvatarMock) GetX() int { return av.pos.X } +func (av AvatarMock) GetMiniMapX() int { return av.pos.X } +func (av AvatarMock) GetY() int { return av.pos.X } +func (av AvatarMock) GetMiniMapY() int { return av.pos.X } + +func TestPaint(t *testing.T) { + t.Cleanup(cleanup) + avatar := AvatarMock{pos: CoordinatesMock{X: 100, Y: 100}, char: '@'} + logger := initTestLogger() + tr := Init(logger) + tr.GenerateWorld() + tr.GenerateTowns() + tr.World.Paint(avatar) +} diff --git a/go.mod b/go.mod index aceab92..a610130 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.23.1 require ( github.com/charmbracelet/bubbletea v1.2.4 github.com/charmbracelet/lipgloss v1.0.0 - github.com/davecgh/go-spew v1.1.1 github.com/ojrac/opensimplex-go v1.0.2 + go.uber.org/zap v1.27.0 ) require ( @@ -22,6 +22,7 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/text v0.3.8 // indirect diff --git a/go.sum b/go.sum index fbdee61..8639c69 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,15 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= github.com/charmbracelet/bubbletea v1.2.4/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= +github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -28,9 +32,19 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/ojrac/opensimplex-go v1.0.2 h1:l4vs0D+JCakcu5OV0kJ99oEaWJfggSc9jiLpxaWvSzs= github.com/ojrac/opensimplex-go v1.0.2/go.mod h1:NwbXFFbXcdGgIFdiA7/REME+7n/lOf1TuEbLiZYOWnM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -39,3 +53,5 @@ golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 3a601cc..35dfa08 100644 --- a/main.go +++ b/main.go @@ -3,21 +3,22 @@ package main import ( "fmt" "github.com/charmbracelet/bubbletea" - "github.com/davecgh/go-spew/spew" - "io" - "math/rand" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" "os" "pirate-wars/cmd/avatar" "pirate-wars/cmd/common" "pirate-wars/cmd/terrain" ) +const BASE_LOG_LEVEL = zap.DebugLevel +const DEV_MODE = true + type model struct { - world terrain.World + logger *zap.SugaredLogger + terrain terrain.Terrain avatar avatar.Type - miniMap terrain.World printMiniMap bool - dump io.Writer } func (m model) Init() tea.Cmd { @@ -27,9 +28,9 @@ func (m model) Init() tea.Cmd { func (m model) View() string { if m.printMiniMap { - return m.miniMap.Paint(m.avatar, true) + return m.terrain.MiniMap.Paint(&m.avatar) } else { - return m.world.Paint(m.avatar, false) + return m.terrain.World.Paint(&m.avatar) } } @@ -39,8 +40,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { // Is it a key press? case tea.KeyMsg: - - spew.Fdump(m.dump, msg) // Cool, what was the actual key pressed? switch msg.String() { // These keys should exit the program. @@ -50,15 +49,21 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "left", "h": if m.avatar.GetX() > 0 { target := m.avatar.GetX() - 1 - if m.world[target][m.avatar.GetY()].IsPassableByBoat() { + if m.terrain.World.IsPassableByBoat(common.Coordinates{ + X: target, + Y: m.avatar.GetY(), + }) { m.avatar.SetX(target) } } case "right", "l": - if m.avatar.GetX() < len(m.world[m.avatar.GetY()])-1 { + if m.avatar.GetX() < m.terrain.World.GetWidth()-1 { target := m.avatar.GetX() + 1 - if m.world[target][m.avatar.GetY()].IsPassableByBoat() { + if m.terrain.World.IsPassableByBoat(common.Coordinates{ + X: target, + Y: m.avatar.GetY(), + }) { m.avatar.SetX(target) } } @@ -67,16 +72,22 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "up", "k": if m.avatar.GetY() > 0 { target := m.avatar.GetY() - 1 - if m.world[m.avatar.GetX()][target].IsPassableByBoat() { + if m.terrain.World.IsPassableByBoat(common.Coordinates{ + X: m.avatar.GetX(), + Y: target, + }) { m.avatar.SetY(target) } } // The "down" and "j" keys move the cursor down case "down", "j": - if m.avatar.GetY() < len(m.world)-1 { + if m.avatar.GetY() < m.terrain.World.GetHeight()-1 { target := m.avatar.GetY() + 1 - if m.world[m.avatar.GetX()][target].IsPassableByBoat() { + if m.terrain.World.IsPassableByBoat(common.Coordinates{ + X: m.avatar.GetX(), + Y: target, + }) { m.avatar.SetY(target) } } @@ -86,37 +97,49 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.avatar.GetY() > 0 && m.avatar.GetX() > 0 { targetY := m.avatar.GetY() - 1 targetX := m.avatar.GetX() - 1 - if m.world[targetX][targetY].IsPassableByBoat() { + if m.terrain.World.IsPassableByBoat(common.Coordinates{ + X: targetX, + Y: targetY, + }) { m.avatar.SetXY(common.Coordinates{X: targetX, Y: targetY}) } } // The "down+left" and "b" keys move the cursor diagonal down+left case "down+left", "b": - if m.avatar.GetY() < len(m.world)-1 && m.avatar.GetX() > 0 { + if m.avatar.GetY() < m.terrain.World.GetHeight()-1 && m.avatar.GetX() > 0 { targetY := m.avatar.GetY() + 1 targetX := m.avatar.GetX() - 1 - if m.world[targetX][targetY].IsPassableByBoat() { + if m.terrain.World.IsPassableByBoat(common.Coordinates{ + X: targetX, + Y: targetY, + }) { m.avatar.SetXY(common.Coordinates{X: targetX, Y: targetY}) } } // The "upright" and "u" keys move the cursor diagonal up+left case "up+right", "u": - if m.avatar.GetY() > 0 && m.avatar.GetX() < len(m.world[m.avatar.GetY()])-1 { + if m.avatar.GetY() > 0 && m.avatar.GetX() < m.terrain.World.GetWidth()-1 { targetY := m.avatar.GetY() - 1 targetX := m.avatar.GetX() + 1 - if m.world[targetX][targetY].IsPassableByBoat() { + if m.terrain.World.IsPassableByBoat(common.Coordinates{ + X: targetX, + Y: targetY, + }) { m.avatar.SetXY(common.Coordinates{X: targetX, Y: targetY}) } } // The "downright" and "n" keys move the cursor diagonal down+left case "down+right", "n": - if m.avatar.GetY() < len(m.world)-1 && m.avatar.GetX() < len(m.world[m.avatar.GetY()])-1 { + if m.avatar.GetY() < m.terrain.World.GetHeight()-1 && m.avatar.GetX() < m.terrain.World.GetWidth()-1 { targetY := m.avatar.GetY() + 1 targetX := m.avatar.GetX() + 1 - if m.world[targetX][targetY].IsPassableByBoat() { + if m.terrain.World.IsPassableByBoat(common.Coordinates{ + X: targetX, + Y: targetY, + }) { m.avatar.SetXY(common.Coordinates{X: targetX, Y: targetY}) } } @@ -135,43 +158,56 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // m.selected[m.cursor] = struct{}{} //} } - //spew.Fdump(m.dump, fmt.Sprintf("x:%v y:%v", m.avatar.GetX(), m.avatar.GetY())) } + m.logger.Debug(fmt.Sprintf("moving to x:%v y:%v", m.avatar.GetX(), m.avatar.GetY())) + // Return the updated model to the Bubble Tea runtime for processing. // Note that we're not returning a command. return m, nil } -func findStartingPosition(world terrain.World) common.Coordinates { - for { - coords := common.Coordinates{X: rand.Intn(common.WorldWidth), Y: rand.Intn(common.WorldHeight)} - if world[coords.X][coords.Y] == common.TypeDeepWater { - return coords - } +func createLogger() *zap.SugaredLogger { + // truncate file + configFile, err := os.OpenFile(common.LogFile, os.O_TRUNC, 0664) + if err != nil { + panic(err) } + if err = configFile.Close(); err != nil { + panic(err) + } + // create logger + cfg := zap.NewProductionConfig() + cfg.OutputPaths = []string{common.LogFile} + cfg.Level = zap.NewAtomicLevelAt(BASE_LOG_LEVEL) + cfg.Development = DEV_MODE + cfg.DisableCaller = false + cfg.DisableStacktrace = false + cfg.Encoding = "console" + encoderConfig := zap.NewProductionEncoderConfig() + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + cfg.EncoderConfig = encoderConfig + logger := zap.Must(cfg.Build()) + defer logger.Sync() + return logger.Sugar() } func main() { - var dump *os.File - if _, ok := os.LookupEnv("DEBUG"); ok { - var err error - dump, err = os.OpenFile("pirate-wars.log", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644) - if err != nil { - os.Exit(1) - } - } + logger := createLogger() + logger.Info("Starting...") + + t := terrain.Init(logger) + t.GenerateWorld() + t.GenerateTowns() - t := terrain.Init() - world := t.Generate() // ⏅ ⏏ ⏚ ⏛ ⏡ ⪮ ⩯ ⩠ ⩟ ⅏ if _, err := tea.NewProgram(model{ - world: world, - miniMap: world.RenderMiniMap(), - avatar: avatar.Create(findStartingPosition(world), '⏏'), - dump: dump, + logger: logger, + terrain: *t, + avatar: avatar.Create(t.RandomPositionDeepWater(), '⏏'), }, tea.WithAltScreen()).Run(); err != nil { - fmt.Printf("Uh oh, there was an error: %v\n", err) + fmt.Printf("Error: %v\n", err) os.Exit(1) } + logger.Info("Exiting...") }