geom.gno
2.93 Kb · 101 lines
1package hex
2
3import "math"
4
5// Axial step of each direction on the flat-top grid (q east along the
6// columns, r down a column), matching scripts/gen_board_data.py.
7var dirStep = [6][2]int{{0, -1}, {1, -1}, {1, 0}, {0, 1}, {-1, 1}, {-1, 0}}
8
9// Step returns the axial (q, r) offset of one hex in direction d.
10func (d Dir) Step() (q, r int) { return dirStep[d][0], dirStep[d][1] }
11
12// Coords returns the axial coordinates of a hex.
13func (b *Board) Coords(id string) (q, r int, ok bool) {
14 i := b.Index(id)
15 if i < 0 {
16 return 0, 0, false
17 }
18 return b.qr[i][0], b.qr[i][1], true
19}
20
21// Distance is the range between two hexes in hexes along the shortest route
22// over the board (rule 13.51), or -1 when either hex is unknown.
23func (b *Board) Distance(from, to string) int {
24 src, dst := b.Index(from), b.Index(to)
25 if src < 0 || dst < 0 {
26 return -1
27 }
28 if src == dst {
29 return 0
30 }
31 dist := make([]int, len(b.ids))
32 for i := range dist {
33 dist[i] = -1
34 }
35 dist[src] = 0
36 queue := []int{src}
37 for len(queue) > 0 {
38 i := queue[0]
39 queue = queue[1:]
40 for _, j := range b.nbr[i] {
41 if j < 0 || dist[j] >= 0 {
42 continue
43 }
44 dist[j] = dist[i] + 1
45 if j == dst {
46 return dist[j]
47 }
48 queue = append(queue, j)
49 }
50 }
51 return -1
52}
53
54// InAimZone reports whether target lies in the aim zone of a character at
55// from facing f (rule 8.32): its own hex and every hex in or between its
56// ahead-left and ahead-right rows, i.e. the 120° cone spanned by those two
57// directions.
58func (b *Board) InAimZone(from string, f Dir, target string) bool {
59 q0, r0, ok := b.Coords(from)
60 q1, r1, ok2 := b.Coords(target)
61 if !ok || !ok2 || !f.Valid() {
62 return false
63 }
64 dq, dr := q1-q0, r1-r0
65 uq, ur := Dir((int(f) + 5) % 6).Step() // ahead left
66 wq, wr := Dir((int(f) + 1) % 6).Step() // ahead right
67 // Solve a*u + b*w = d; the step vectors 120° apart have determinant ±1.
68 det := uq*wr - ur*wq
69 a := (dq*wr - dr*wq) / det
70 c := (uq*dr - ur*dq) / det
71 return a >= 0 && c >= 0
72}
73
74// LongDir is the direction from target that continues the line drawn from
75// shooter through target (rule 14.32): the "Long" hex of a direction card is
76// the target's neighbour that way. When the line runs between two hexes the
77// one closest to it wins, lowest direction first; in a shared hex it is the
78// shooter's facing.
79func (b *Board) LongDir(shooter, target string, shooterFacing Dir) Dir {
80 q0, r0, ok := b.Coords(shooter)
81 q1, r1, ok2 := b.Coords(target)
82 if !ok || !ok2 || (q0 == q1 && r0 == r1) {
83 return shooterFacing
84 }
85 x, y := planar(q1-q0, r1-r0)
86 best, bestCos := Dir(0), -2.0
87 for d := 0; d < 6; d++ {
88 sq, sr := Dir(d).Step()
89 sx, sy := planar(sq, sr)
90 cos := (x*sx + y*sy) / (math.Sqrt(x*x+y*y) * math.Sqrt(sx*sx+sy*sy))
91 if cos > bestCos+1e-9 {
92 best, bestCos = Dir(d), cos
93 }
94 }
95 return best
96}
97
98// planar maps an axial offset to flat-top pixel-like coordinates (x east, y south).
99func planar(q, r int) (x, y float64) {
100 return 1.5 * float64(q), math.Sqrt(3) * (float64(r) + float64(q)/2)
101}