Skip to content

Commit

Permalink
Merge pull request #6 from silverbucket/heatmap-view
Browse files Browse the repository at this point in the history
Heatmap view
  • Loading branch information
silverbucket authored Jan 10, 2025
2 parents 18f2675 + ebd4cc9 commit 245a4e2
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 85 deletions.
55 changes: 46 additions & 9 deletions cmd/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import (
)

const (
LogFile = "pirate-wars.log"
WorldWidth = 600 // Y
WorldHeight = 600 // X
TotalTowns = 20
ViewWidth = 75
ViewHeight = 50
MiniMapFactor = 11
TotalNpcs = 50
MaxMovementCost = 9999
LogFile = "pirate-wars.log"
WorldWidth = 600 // Y
WorldHeight = 600 // X
TotalTowns = 20
ViewWidth = 75
ViewHeight = 50
MiniMapFactor = 11
TotalNpcs = 50
)

type ViewPort struct {
Expand Down Expand Up @@ -50,3 +49,41 @@ func GenID(pos Coordinates) string {
func Inbounds(c Coordinates) bool {
return c.X >= 0 && c.X < WorldHeight && c.Y >= 0 && c.Y < WorldWidth
}

func IsPositionAdjacent(p Coordinates, t Coordinates) bool {
for _, dir := range Directions {
n := AddDirection(p, dir)
if t.X == n.X && t.Y == n.Y {
return true
}
}
return false
}

func RandomPosition() Coordinates {
return Coordinates{X: rand.Intn(WorldWidth - 1), Y: rand.Intn(WorldHeight - 1)}
}

func AddDirection(p Coordinates, d Coordinates) Coordinates {
return Coordinates{p.X + d.X, p.Y + d.Y}
}

func ClosestTo(d Coordinates, p []Coordinates) Coordinates {
closest := Coordinates{}
val := 99999
for _, o := range p {
v := diff(d.X, o.X) + diff(d.Y, o.Y)
if v < val {
val = v
closest = o
}
}
return closest
}

func diff(a, b int) int {
if a < b {
return b - a
}
return a - b
}
123 changes: 99 additions & 24 deletions cmd/terrain/heatmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,38 @@ package terrain

import (
"fmt"
"github.com/charmbracelet/lipgloss/table"
"pirate-wars/cmd/common"
)

const HeatmapUnprocessed = -1
const HeatmapQueued = -2
const MaxMovementCost = HeatMapCost(999999)
const LandMovementBase = HeatMapCost(5000)

type DirectionCost struct {
pos common.Coordinates
cost HeatMapCost
}

type HeatMapCost int

type HeatMap struct {
grid [][]HeatMapCost
}

func (h *HeatMap) SetCost(c common.Coordinates, v HeatMapCost) {
h.grid[c.X][c.Y] = v
}

func (h *HeatMap) GetCost(c common.Coordinates) HeatMapCost {
return h.grid[c.X][c.Y]
}

func (t *Terrain) GenerateTownHeatMap(town *Town) bool {
// Define the starting point
startX, startY := town.GetX(), town.GetY()

// Queue contains points to visit, cost
queue := [][]int{{startX, startY, 0}}
queue := []DirectionCost{{town.GetPos(), 0}}
count := 0
// Perform Breadth-First Search
for len(queue) > 0 {
Expand All @@ -19,48 +42,46 @@ func (t *Terrain) GenerateTownHeatMap(town *Town) bool {
j := queue[0]
queue = queue[1:]

c := common.Coordinates{j[0], j[1]}
cost := j[2]
c := j.pos
cost := j.cost

//t.Logger.Infof("[towm %v] Processing %v, %v", t`own, x, y)
if t.World.IsPassableByBoat(c) {
//t.Logger.Debug(fmt.Sprintf("[town %v] Assigning cost %v, %v = %v [%v]", town, x, y, cost, t.Towns[town].heatMap[x][y]))
//t.Logger.Debug(fmt.Sprintf("[town %v] Assigning cost %v, %v = %v [%v]", town, x, y, cost, t.Towns[town].HeatMap[x][y]))
if t.World.GetPositionType(c) == TypeShallowWater {
// shallow water costs more (dangerous)
town.SetHeatmapCost(c, cost+3)
cost = cost + 4
cost = cost + 10
town.HeatMap.SetCost(c, cost)
} else if t.World.GetPositionType(c) == TypeOpenWater {
// open water faster than shallow, but not as fast as deep
town.SetHeatmapCost(c, cost+1)
cost = cost + 2
cost = cost + 5
town.HeatMap.SetCost(c, cost)
} else {
town.SetHeatmapCost(c, cost)
cost = cost + 1
town.HeatMap.SetCost(c, cost)
}
cost = cost + 1
} else {
if cost == 0 && t.World.GetPositionType(c) == TypeTown {
// starting town is the cheapest
town.SetHeatmapCost(c, cost)
town.HeatMap.SetCost(c, cost)
} else {
// land is impassible
town.SetHeatmapCost(c, common.MaxMovementCost)
// land currently impassible
town.HeatMap.SetCost(c, MaxMovementCost)
}
}

// Explore neighbors
for _, dir := range common.Directions {
n := common.Coordinates{c.X + dir.X, c.Y + dir.Y}

//t.Logger.Debug(fmt.Sprintf("[town %v] Prepping direction %v, %v cost:%v", town, newX, newY, cost))

// Check if the new point is within bounds and not visited
if common.Inbounds(n) && town.GetHeatmapCost(n) == -1 {
if !t.World.IsPassableByBoat(n) && t.World.GetPositionType(n) != TypeTown {
town.SetHeatmapCost(n, common.MaxMovementCost)
// Check if the new point is within bounds of the map and not visited
if common.Inbounds(n) && town.HeatMap.GetCost(n) == HeatmapUnprocessed {
if t.World.IsLand(n) {
town.HeatMap.SetCost(n, MaxMovementCost)
} else {
//t.Logger.Debug(fmt.Sprintf("[town %v] (%v, %v) Adding direction %v, %v -- heatmap:%v", town, x, y, newX, newY, t.Towns[town].heatMap[newX][newY]))
town.SetHeatmapCost(n, -2)
queue = append(queue, []int{n.X, n.Y, cost})
//t.Logger.Debug(fmt.Sprintf("[town %v] (%v, %v) Adding direction %v, %v -- heatmap:%v", town, x, y, newX, newY, t.Towns[town].HeatMap[newX][newY]))
town.HeatMap.SetCost(n, HeatmapQueued)
queue = append(queue, DirectionCost{n, cost})
}
}
}
Expand All @@ -80,3 +101,57 @@ func (t *Terrain) GenerateTownHeatMap(town *Town) bool {
// t.GenerateTownHeatMap(&town)
// }
//}

func (h *HeatMap) Paint(avatar AvatarReadOnly, npcs []AvatarReadOnly) string {
left := 0
top := 0
worldHeight := len(h.grid)
worldWidth := len(h.grid[0])
viewHeight := worldHeight
viewWidth := worldWidth
rowWidth := worldWidth

viewport := table.New().BorderBottom(false).BorderTop(false).BorderLeft(false).BorderRight(false)

// overlay map of all avatars
overlay := make(map[string]AvatarReadOnly)

// center viewport on avatar
left = avatar.GetX() - (common.ViewWidth / 3)
top = avatar.GetY() - (common.ViewHeight / 3)
if left < 0 {
left = 0
}
if top < 0 {
top = 0
}
viewHeight = common.ViewHeight + top
viewWidth = common.ViewWidth + left
rowWidth = common.ViewWidth

overlay[fmt.Sprintf("%03d%03d", avatar.GetX(), avatar.GetY())] = avatar
// on the world map we draw the NPCs
for _, n := range npcs {
overlay[fmt.Sprintf("%03d%03d", n.GetX(), n.GetY())] = n
}

//world.logger.Debug(fmt.Sprintf("avatar position: X:%v Y:%v", avs[0].GetX, avs[0].GetY()))
for y := top; y < worldHeight && y < viewHeight; y++ {
var row = make([]string, rowWidth)
for x := left; x < worldWidth && x < viewWidth; x++ {
item, ok := overlay[fmt.Sprintf("%03d%03d", x, y)]
if ok {
row[x-left] = item.Render()
} else {
row[x-left] = h.grid[x][y].Render()
}
}
viewport.Row(row...).BorderColumn(false)
}

return fmt.Sprintln(viewport)
}

func (hc *HeatMapCost) Render() string {
return fmt.Sprintf(createTerrainItem("0").PaddingLeft(1).PaddingRight(1).Render("%v"), *hc)
}
61 changes: 39 additions & 22 deletions cmd/terrain/npc.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,28 @@ var ColorPossibilities = []ColorScheme{

func (t *Terrain) CreateNpc() {
pos := t.RandomPositionDeepWater()
firstTown := t.GetRandomTown()
secondTown := t.GetRandomTown()
tradeTowns := []Town{}
for {
town := t.GetRandomTown()
// ensure towns are unique
if secondTown.GetY() == firstTown.GetY() && secondTown.GetY() == firstTown.GetY() {
secondTown = t.GetRandomTown()
} else {
if len(tradeTowns) > 2 {
break
} else if len(tradeTowns) == 2 {
if (town.GetY() == tradeTowns[0].GetY() && town.GetY() == tradeTowns[0].GetY()) || !town.AccessibleFrom(pos) {
// either same town, or inaccessible from position, try again
continue
}
}
tradeTowns = append(tradeTowns, town)
}

npc := Npc{
id: common.GenID(pos),
avatar: CreateAvatar(pos, '⏏', ColorPossibilities[rand.Intn(len(ColorPossibilities)-1)]),
agenda: Agenda{
goal: GoalTypeTrade,
tradeTarget: 0,
tadeRoute: []Town{firstTown, secondTown},
tadeRoute: tradeTowns,
},
}
t.Logger.Infof("[%v] NPC created at %d, %d", npc.id, pos.X, pos.Y)
Expand All @@ -77,41 +82,39 @@ func (t *Terrain) CalcNpcMovements() {
}

npc := &t.Npcs[i]
target := npc.avatar.GetPos()
town := &npc.agenda.tadeRoute[npc.agenda.tradeTarget]

// if we're already at our destination, flip our trade route
if town.GetHeatmapCost(target) < 3 {
if town.HeatMap.GetCost(npc.avatar.GetPos()) < 3 {
oldTown := npc.agenda.tadeRoute[npc.agenda.tradeTarget]
npc.agenda.tradeTarget = npc.agenda.tradeTarget ^ 1
town = &npc.agenda.tadeRoute[npc.agenda.tradeTarget]
t.Logger.Info(fmt.Sprintf("[%v] NPC movement trade switch from town %v to town %v", npc.id, oldTown.GetPos(), town.GetPos()))
t.Logger.Info(fmt.Sprintf("[%v] NPC movement trade route switch town %v to town %v", npc.id, oldTown.GetPos(), town.GetPos()))
}

// find next move by cost on heatmap
var lowestCost = common.MaxMovementCost
opts := []DirectionCost{}
for _, dir := range common.Directions {
n := common.Coordinates{npc.avatar.GetX() + dir.X, npc.avatar.GetY() + dir.Y}
n := common.AddDirection(npc.avatar.GetPos(), dir)
if !common.Inbounds(n) {
// don't check out of bounds
continue
}

//t.Logger.Debug(fmt.Sprintf("New heatmap coordinates check [%v][%v]", newX, newY))
//t.Logger.Debug(fmt.Sprintf("Npc at %v, %v - checking square %v, %v cost:%v [lowest cost: %v]", newPosition.X, newPosition.Y, newX, newY, town.heatMap[newX][newY], lowestCost)
cost := town.GetHeatmapCost(n)
if cost >= 0 && cost < lowestCost {
lowestCost = cost
target = n
}
//t.Logger.Debug(fmt.Sprintf("Npc at %v, %v - checking square %v, %v cost:%v [lowest cost: %v]", newPosition.X, newPosition.Y, newX, newY, town.HeatMap[newX][newY], lowestCost)
opts = append(opts, DirectionCost{n, town.HeatMap.GetCost(n)})
}

pick := decideDirection(opts, town.GetPos())
target := pick.pos
cost := pick.cost
npcpos := npc.avatar.GetPos()

if target.X == npc.avatar.GetX() && target.Y == npc.avatar.GetY() {
t.Logger.Debug(fmt.Sprintf("[%v] NPC stuck! Travelling to town at %v (cost %v)", npc.id, town.GetPos(), town.GetHeatmapCost(target)))
t.Logger.Debug(fmt.Sprintf("[%v] NPC stuck at %v! Travelling to town at %v (cost %v)", npc.id, npcpos, town.GetPos(), cost))
} else {
pos := npc.avatar.GetPos()
t.Logger.Info(fmt.Sprintf("[%v] NPC moving from %v to %v", npc.id, pos, target))
if !t.isPositionAdjacent(pos, target) {
t.Logger.Info(fmt.Sprintf("[%v] NPC moving from %v to %v (cost %v)", npc.id, npcpos, target, cost))
if !common.IsPositionAdjacent(npcpos, target) {
t.Logger.Warn(fmt.Sprintf("[%v] NPC warp!", npc.id))
}
npc.avatar.SetPos(target)
Expand All @@ -126,3 +129,17 @@ func (t *Terrain) GetNpcAvatars() []AvatarReadOnly {
}
return avs
}

func decideDirection(o []DirectionCost, dest common.Coordinates) DirectionCost {
lowestCost := MaxMovementCost
choice := DirectionCost{}
for _, e := range o {
if e.cost <= lowestCost && e.cost >= 0 {
lowestCost = e.cost
//possibilities = append(possibilities, e.pos)
choice = e
}
}
//return common.ClosestTo(dest, possibilities)
return choice
}
14 changes: 0 additions & 14 deletions cmd/terrain/terrain.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,17 +144,3 @@ func (t *Terrain) RandomPositionDeepWater() common.Coordinates {
}
}
}

func (t *Terrain) RandomPosition() common.Coordinates {
return common.Coordinates{X: rand.Intn(common.WorldWidth - 1), Y: rand.Intn(common.WorldHeight - 1)}
}

func (t *Terrain) isPositionAdjacent(pos common.Coordinates, target common.Coordinates) bool {
for _, dir := range common.Directions {
newX, newY := pos.X+dir.X, pos.Y+dir.Y
if target.X == newX && target.Y == newY {
return true
}
}
return false
}
Loading

0 comments on commit 245a4e2

Please sign in to comment.