aboutsummaryrefslogtreecommitdiff
path: root/internal/world/grid.go
blob: ac80476eb2aae4e2f88ba242fe2473fa6c8435c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package world

// GridConflict describes a layout conflict found while embedding rooms onto a
// 3D grid via their exit deltas.
//
//   - "twist":   one room is forced onto two different grid cells (it was already
//     placed at Existing but a later exit wants it at Want).
//   - "overlap": two distinct rooms want the same grid cell (Occupier already
//     holds Want).
type GridConflict struct {
	Kind     string  // "twist" or "overlap"
	From     int     // room whose exit produced the conflict
	Dir      ExitDir // exit direction taken from From
	Target   int     // room being placed
	Want     [3]int  // grid cell Target was assigned
	Existing [3]int  // where Target was already placed (twist only)
	Occupier int     // room already at Want (overlap only)
}

// RoomGrid is the embedding of one connected component onto a 3D grid.
type RoomGrid struct {
	Coord  map[int][3]int // room -> grid coordinate
	RoomAt map[[3]int]int // grid coordinate -> room
	Dist   map[int]int    // room -> BFS hop distance from the seed
}

// BuildGrid lays out the rooms reachable from seed onto a 3D grid, assigning
// each room a coordinate by accumulating per-exit DirectionDeltas3D offsets in
// BFS order. The seed sits at the origin (0,0,0).
//
//   - load returns a room and false to skip one that cannot be read; its exits
//     are then not followed, but any coordinate already assigned to it stands.
//   - include, when non-nil, restricts placement to targets for which it returns
//     true (pass nil to place every reachable target).
//   - edgeInclude, when non-nil, restricts traversal to exits for which it
//     returns true (pass nil to follow every exit whose target passes include).
//     It receives (fromRoomID, dir, targetRoomID) and is checked before include.
//   - onConflict, when non-nil, is called for each twist/overlap encountered.
//
// The first assignment to a room or cell always wins; conflicting placements are
// skipped. In a world that passes grid validation there are no conflicts, so the
// layout is unambiguous.
func BuildGrid(seed int, load func(int) (*Room, bool), include func(int) bool, edgeInclude func(int, ExitDir, int) bool, onConflict func(GridConflict)) RoomGrid {
	g := RoomGrid{
		Coord:  map[int][3]int{seed: {0, 0, 0}},
		RoomAt: map[[3]int]int{{0, 0, 0}: seed},
		Dist:   map[int]int{seed: 0},
	}
	queue := []int{seed}

	for len(queue) > 0 {
		rid := queue[0]
		queue = queue[1:]
		room, ok := load(rid)
		if !ok {
			continue
		}
		c := g.Coord[rid]
		for _, dir := range ExitOrder {
			exit, ok := room.Exits[dir]
			if !ok || exit.Room <= 0 {
				continue
			}
			target := exit.Room
			if include != nil && !include(target) {
				continue
			}
			d := DirectionDeltas3D[dir]
			want := [3]int{c[0] + d[0], c[1] + d[1], c[2] + d[2]}

			if existing, seen := g.Coord[target]; seen {
				if existing != want && onConflict != nil {
					onConflict(GridConflict{
						Kind: "twist", From: rid, Dir: dir,
						Target: target, Want: want, Existing: existing,
					})
				}
				continue
			}
			if occupier, used := g.RoomAt[want]; used && occupier != target {
				if onConflict != nil {
					onConflict(GridConflict{
						Kind: "overlap", From: rid, Dir: dir,
						Target: target, Want: want, Occupier: occupier,
					})
				}
				continue
			}

			g.Coord[target] = want
			g.RoomAt[want] = target
			g.Dist[target] = g.Dist[rid] + 1
			queue = append(queue, target)
		}
	}

	return g
}

// BuildGridConflicts lays out the rooms reachable from seed exactly like
// BuildGrid but returns every twist/overlap conflict encountered instead of
// silently skipping them. Callers pass a load closure that may return *edited
// copies* of rooms to model a hypothetical edit (e.g. an insert or a remove)
// without writing anything to disk; a non-empty result means the modeled world
// cannot be embedded on the grid. include and edgeInclude default to "place
// and follow everything" (geometry is independent of gating), matching the
// startup validator's validateRoomGrid.
func BuildGridConflicts(seed int, load func(int) (*Room, bool)) []GridConflict {
	var conflicts []GridConflict
	BuildGrid(seed, load, nil, nil, func(gc GridConflict) {
		conflicts = append(conflicts, gc)
	})
	return conflicts
}