From b2f87f1aebe5656a3aa7782b4778c9a3d8970524 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Sat, 11 Jul 2026 08:54:05 +0000 Subject: [PATCH 01/38] feat: add point to hex algorithms --- viewer/coordinate.odin | 2 +- viewer/cube.odin | 53 ++++++++++++++++++++++++++++++++++++++++++ viewer/hex.odin | 25 ++++++++++++++++---- viewer/sector.odin | 4 ++-- 4 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 viewer/cube.odin diff --git a/viewer/coordinate.odin b/viewer/coordinate.odin index 93238f6..c622a28 100644 --- a/viewer/coordinate.odin +++ b/viewer/coordinate.odin @@ -17,7 +17,7 @@ new_coordinate :: proc(col, row: i32) -> Coordinate { return {col, row} } -oddq_to_axial :: proc(coord: Coordinate) -> Hex { +oddq_to_axial :: proc(coord: Coordinate) -> Hex(i32) { parity := coord.col & 1 q := coord.col r := coord.row - (coord.col - parity) / 2 diff --git a/viewer/cube.odin b/viewer/cube.odin new file mode 100644 index 0000000..890e9fd --- /dev/null +++ b/viewer/cube.odin @@ -0,0 +1,53 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package viewer + +import "base:intrinsics" +import "core:math" + +Cube :: struct($T: typeid) where intrinsics.type_is_numeric(T) { + q, r, s: T, +} + +new_cube :: proc(q, r, s: $T) -> Cube(T) { + return {q, r, s} +} + +axial_to_cube :: proc(hex: Hex($T)) -> Cube(T) { + q := hex.q + r := hex.r + s := -q - r + + return new_cube(q, r, s) +} + +cube_to_axial :: proc(cube: Cube(i32)) -> Hex(i32) { + q := cube.q + r := cube.r + + return new_hex(q, r) +} + +cube_round :: proc(frac: Cube(f32)) -> Cube(i32) { + q := math.round(frac.q) + r := math.round(frac.r) + s := math.round(frac.s) + + q_diff := math.abs(q - frac.q) + r_diff := math.abs(r - frac.r) + s_diff := math.abs(s - frac.s) + + if q_diff > r_diff && q_diff > s_diff { + q = -r - s + } else if r_diff > s_diff { + r = -q - s + } else { + s = -q - r + } + + return new_cube(i32(q), i32(r), i32(s)) +} diff --git a/viewer/hex.odin b/viewer/hex.odin index 1e91781..ac1f865 100644 --- a/viewer/hex.odin +++ b/viewer/hex.odin @@ -6,25 +6,40 @@ package viewer +import "base:intrinsics" +import "core:math" import rl "vendor:raylib" -Hex :: struct { - q, r: i32, +Hex :: struct($T: typeid) where intrinsics.type_is_numeric(T) { + q, r: T, } -new_hex :: proc(q, r: i32) -> Hex { +new_hex :: proc(q, r: $T) -> Hex(T) { return {q, r} } -draw_hex :: proc(hex: Hex) { +draw_hex :: proc(hex: Hex(i32)) { center := oddq_offset_to_pixel(axial_to_oddq(hex)) rl.DrawPolyLines(center, 6, HEX_SIZE, 0, rl.DARKGRAY) } -axial_to_oddq :: proc(hex: Hex) -> Coordinate { +axial_to_oddq :: proc(hex: Hex(i32)) -> Coordinate { parity := hex.q & 1 col := hex.q row := hex.r + (hex.q - parity) / 2 return new_coordinate(col, row) } + +pixel_to_flat_hex :: proc(point: rl.Vector2) -> Hex(i32) { + x := point.x / HEX_SIZE + y := point.y / HEX_SIZE + q := ((2.0 / 3) * x) + r := ((-1.0 / 3) * x + (math.SQRT_THREE / 3) * y) + + return axial_round(new_hex(q, r)) +} + +axial_round :: proc(frac: Hex(f32)) -> Hex(i32) { + return cube_to_axial(cube_round(axial_to_cube(frac))) +} diff --git a/viewer/sector.odin b/viewer/sector.odin index 28bcc2e..a420d37 100644 --- a/viewer/sector.odin +++ b/viewer/sector.odin @@ -10,12 +10,12 @@ import rl "vendor:raylib" Sector :: struct { name: cstring, - hexes: [SECTOR_ROWS][SECTOR_COLUMNS]Hex, + hexes: [SECTOR_ROWS][SECTOR_COLUMNS]Hex(i32), origin: Coordinate, } new_sector :: proc(name: cstring, origin: Coordinate) -> Sector { - hexes: [SECTOR_ROWS][SECTOR_COLUMNS]Hex + hexes: [SECTOR_ROWS][SECTOR_COLUMNS]Hex(i32) for &row, y in hexes { for &hex, x in row { From 3265925abcc7e309af2951a09a82f0e3ea9f6fc0 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Sat, 11 Jul 2026 13:51:34 +0000 Subject: [PATCH 02/38] fix: run tests for all packages --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7e55692..751e346 100644 --- a/Makefile +++ b/Makefile @@ -29,4 +29,4 @@ run: odin run . test: - odin test . + odin test . --all-packages From d6bbdf2e7fb85b38d8baec53dbe0fcc648154cea Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Sun, 12 Jul 2026 04:03:57 +0000 Subject: [PATCH 03/38] feat: start to rewrite hex grid logic --- viewer/coordinate.odin | 35 --------------------------- viewer/cube.odin | 53 ----------------------------------------- viewer/hex.odin | 52 ++++++++++++++++++++++------------------ viewer/layout.odin | 17 +++++++++++++ viewer/orientation.odin | 27 +++++++++++++++++++++ viewer/point.odin | 32 +++++++++++++++++++++++++ viewer/renderer.odin | 2 +- viewer/sector.odin | 28 +++++++--------------- 8 files changed, 114 insertions(+), 132 deletions(-) delete mode 100644 viewer/coordinate.odin delete mode 100644 viewer/cube.odin create mode 100644 viewer/layout.odin create mode 100644 viewer/orientation.odin create mode 100644 viewer/point.odin diff --git a/viewer/coordinate.odin b/viewer/coordinate.odin deleted file mode 100644 index c622a28..0000000 --- a/viewer/coordinate.odin +++ /dev/null @@ -1,35 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package viewer - -import "core:math" -import rl "vendor:raylib" - -Coordinate :: struct { - col, row: i32, -} - -new_coordinate :: proc(col, row: i32) -> Coordinate { - return {col, row} -} - -oddq_to_axial :: proc(coord: Coordinate) -> Hex(i32) { - parity := coord.col & 1 - q := coord.col - r := coord.row - (coord.col - parity) / 2 - - return new_hex(q, r) -} - -oddq_offset_to_pixel :: proc(coord: Coordinate) -> rl.Vector2 { - x := 1.5 * f32(coord.col) - y := math.SQRT_THREE * (f32(coord.row) + 0.5 * f32(coord.col & 1)) - x *= HEX_SIZE - y *= HEX_SIZE - - return {x, y} -} diff --git a/viewer/cube.odin b/viewer/cube.odin deleted file mode 100644 index 890e9fd..0000000 --- a/viewer/cube.odin +++ /dev/null @@ -1,53 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -package viewer - -import "base:intrinsics" -import "core:math" - -Cube :: struct($T: typeid) where intrinsics.type_is_numeric(T) { - q, r, s: T, -} - -new_cube :: proc(q, r, s: $T) -> Cube(T) { - return {q, r, s} -} - -axial_to_cube :: proc(hex: Hex($T)) -> Cube(T) { - q := hex.q - r := hex.r - s := -q - r - - return new_cube(q, r, s) -} - -cube_to_axial :: proc(cube: Cube(i32)) -> Hex(i32) { - q := cube.q - r := cube.r - - return new_hex(q, r) -} - -cube_round :: proc(frac: Cube(f32)) -> Cube(i32) { - q := math.round(frac.q) - r := math.round(frac.r) - s := math.round(frac.s) - - q_diff := math.abs(q - frac.q) - r_diff := math.abs(r - frac.r) - s_diff := math.abs(s - frac.s) - - if q_diff > r_diff && q_diff > s_diff { - q = -r - s - } else if r_diff > s_diff { - r = -q - s - } else { - s = -q - r - } - - return new_cube(i32(q), i32(r), i32(s)) -} diff --git a/viewer/hex.odin b/viewer/hex.odin index ac1f865..27c021e 100644 --- a/viewer/hex.odin +++ b/viewer/hex.odin @@ -6,40 +6,46 @@ package viewer -import "base:intrinsics" import "core:math" import rl "vendor:raylib" -Hex :: struct($T: typeid) where intrinsics.type_is_numeric(T) { - q, r: T, -} +Hex :: distinct rl.Vector3 + +new_hex :: proc(q, r, s: f32) -> Hex { + assert(q + r + s == 0) -new_hex :: proc(q, r: $T) -> Hex(T) { - return {q, r} + return {q, r, s} } -draw_hex :: proc(hex: Hex(i32)) { - center := oddq_offset_to_pixel(axial_to_oddq(hex)) - rl.DrawPolyLines(center, 6, HEX_SIZE, 0, rl.DARKGRAY) +draw_hex :: proc(hex: Hex) { + rl.DrawPolyLines({0, 0}, 6, HEX_SIZE, 0, rl.DARKGRAY) } -axial_to_oddq :: proc(hex: Hex(i32)) -> Coordinate { - parity := hex.q & 1 - col := hex.q - row := hex.r + (hex.q - parity) / 2 +hex_to_pixel :: proc(layout: Layout, h: Hex) -> Point { + M := layout.orientation + + x := (M.f[0][0] * h.x + M.f[0][1] * h.y) * layout.size.x + y := (M.f[1][0] * h.x + M.f[1][1] * h.y) * layout.size.y - return new_coordinate(col, row) + return new_point(x + layout.origin.x, y + layout.origin.y) } -pixel_to_flat_hex :: proc(point: rl.Vector2) -> Hex(i32) { - x := point.x / HEX_SIZE - y := point.y / HEX_SIZE - q := ((2.0 / 3) * x) - r := ((-1.0 / 3) * x + (math.SQRT_THREE / 3) * y) +hex_round :: proc(h: Hex) -> Hex { + q := math.round(h.x) + r := math.round(h.y) + s := math.round(h.z) - return axial_round(new_hex(q, r)) -} + q_diff := math.abs(q - h.x) + r_diff := math.abs(r - h.y) + s_diff := math.abs(s - h.z) + + if q_diff > r_diff && q_diff > s_diff { + q = -r - s + } else if r_diff > s_diff { + r = -q - s + } else { + s = -q - r + } -axial_round :: proc(frac: Hex(f32)) -> Hex(i32) { - return cube_to_axial(cube_round(axial_to_cube(frac))) + return new_hex(q, r, s) } diff --git a/viewer/layout.odin b/viewer/layout.odin new file mode 100644 index 0000000..29db583 --- /dev/null +++ b/viewer/layout.odin @@ -0,0 +1,17 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package viewer + +Layout :: struct { + orientation: Orientation, + origin: Point, + size: Point, +} + +new_layout :: proc(orientation: Orientation, origin: Point, size: Point) -> Layout { + return {orientation, size, origin} +} diff --git a/viewer/orientation.odin b/viewer/orientation.odin new file mode 100644 index 0000000..34bba58 --- /dev/null +++ b/viewer/orientation.odin @@ -0,0 +1,27 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package viewer + +import "core:math" + +Orientation :: struct { + f: matrix[2, 2]f32, + b: matrix[2, 2]f32, + start_angle: f32, +} + +new_orientation :: proc(f: matrix[2, 2]f32, b: matrix[2, 2]f32, start_angle: f32) -> Orientation { + return {f, b, start_angle} +} + +flat_orientation :: proc() -> Orientation { + return { + {3.0 / 2.0, 0.0, math.SQRT_THREE / 2.0, math.SQRT_THREE}, + {2.0 / 3.0, 0.0, -1.0 / 3.0, math.SQRT_THREE / 3.0}, + 0.0, + } +} diff --git a/viewer/point.odin b/viewer/point.odin new file mode 100644 index 0000000..9313bdc --- /dev/null +++ b/viewer/point.odin @@ -0,0 +1,32 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package viewer + +import rl "vendor:raylib" + +Point :: distinct rl.Vector2 + +new_point :: proc(x, y: f32) -> Point { + return {x, y} +} + +pixel_to_hex_fractional :: proc(layout: Layout, p: Point) -> Hex { + M := layout.orientation + + pt := new_point( + (p.x - layout.origin.x) / layout.size.x, + (p.y - layout.origin.y) / layout.size.y, + ) + q := M.b[0, 0] * pt.x + M.b[0, 1] * pt.y + r := M.b[1, 0] * pt.x + M.b[1, 1] * pt.y + + return new_hex(q, r, -q - r) +} + +pixel_to_hex_rounded :: proc(layout: Layout, p: Point) -> Hex { + return hex_round(pixel_to_hex_fractional(layout, p)) +} diff --git a/viewer/renderer.odin b/viewer/renderer.odin index 93b0b5a..aa054e2 100644 --- a/viewer/renderer.odin +++ b/viewer/renderer.odin @@ -13,7 +13,7 @@ run :: proc() { defer rl.CloseWindow() camera := new_camera() - sector := new_sector("Spinward Marches", {38, 6}) + sector := new_sector("Spinward Marches") for !rl.WindowShouldClose() { rl.BeginDrawing() diff --git a/viewer/sector.odin b/viewer/sector.odin index a420d37..8253423 100644 --- a/viewer/sector.odin +++ b/viewer/sector.odin @@ -6,34 +6,22 @@ package viewer -import rl "vendor:raylib" - Sector :: struct { name: cstring, - hexes: [SECTOR_ROWS][SECTOR_COLUMNS]Hex(i32), - origin: Coordinate, + hexes: map[string]Hex, + layout: Layout, } -new_sector :: proc(name: cstring, origin: Coordinate) -> Sector { - hexes: [SECTOR_ROWS][SECTOR_COLUMNS]Hex(i32) +new_sector :: proc(name: cstring, origin: Point = {0, 0}) -> Sector { + hexes: map[string]Hex - for &row, y in hexes { - for &hex, x in row { - hex = oddq_to_axial({origin.col + i32(x), origin.row + i32(y)}) - } - } + layout := new_layout(flat_orientation(), origin, {SECTOR_ROWS, SECTOR_COLUMNS}) - return {name, hexes, origin} + return {name, hexes, layout} } draw_sector :: proc(sector: Sector) { - for row in sector.hexes { - for hex in row { - draw_hex(hex) - } + for _, hex in sector.hexes { + draw_hex(hex) } - - position := oddq_offset_to_pixel({sector.origin.col + 4, sector.origin.row + 18}) - - rl.DrawText(sector.name, i32(position.x - 8), i32(position.y - 8), 48, rl.WHITE) } From 45ec1e609a90830490a1c7ed49bb9a59b0932157 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Sun, 12 Jul 2026 04:39:33 +0000 Subject: [PATCH 04/38] feat: create hex indexer --- viewer/hex.odin | 7 ++++--- viewer/point.odin | 2 +- viewer/sector.odin | 24 +++++++++++++++++++++--- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/viewer/hex.odin b/viewer/hex.odin index 27c021e..b550fe5 100644 --- a/viewer/hex.odin +++ b/viewer/hex.odin @@ -6,10 +6,11 @@ package viewer +import "core:fmt" import "core:math" import rl "vendor:raylib" -Hex :: distinct rl.Vector3 +Hex :: rl.Vector3 new_hex :: proc(q, r, s: f32) -> Hex { assert(q + r + s == 0) @@ -17,8 +18,8 @@ new_hex :: proc(q, r, s: f32) -> Hex { return {q, r, s} } -draw_hex :: proc(hex: Hex) { - rl.DrawPolyLines({0, 0}, 6, HEX_SIZE, 0, rl.DARKGRAY) +hex_index :: proc(x, y: i32) -> string { + return fmt.aprintf("%02d%02d", x, y) } hex_to_pixel :: proc(layout: Layout, h: Hex) -> Point { diff --git a/viewer/point.odin b/viewer/point.odin index 9313bdc..8084e6b 100644 --- a/viewer/point.odin +++ b/viewer/point.odin @@ -8,7 +8,7 @@ package viewer import rl "vendor:raylib" -Point :: distinct rl.Vector2 +Point :: rl.Vector2 new_point :: proc(x, y: f32) -> Point { return {x, y} diff --git a/viewer/sector.odin b/viewer/sector.odin index 8253423..18dc604 100644 --- a/viewer/sector.odin +++ b/viewer/sector.odin @@ -6,22 +6,40 @@ package viewer +import rl "vendor:raylib" + Sector :: struct { name: cstring, hexes: map[string]Hex, layout: Layout, } -new_sector :: proc(name: cstring, origin: Point = {0, 0}) -> Sector { +new_sector :: proc( + name: cstring, + origin: Point = {0, 0}, + size: Point = {SECTOR_COLUMNS, SECTOR_ROWS}, +) -> Sector { hexes: map[string]Hex - layout := new_layout(flat_orientation(), origin, {SECTOR_ROWS, SECTOR_COLUMNS}) + for y in 0 ..< size.y { + for x in 0 ..< size.x { + index := hex_index(i32(x), i32(y)) + hexes[index] = new_hex(x, y, -x - y) + } + } + + layout := new_layout(flat_orientation(), origin, size) return {name, hexes, layout} } draw_sector :: proc(sector: Sector) { for _, hex in sector.hexes { - draw_hex(hex) + draw_hex(sector.layout, hex) } } + +draw_hex :: proc(layout: Layout, hex: Hex) { + center := hex_to_pixel(layout, hex) + rl.DrawPolyLines(center, 6, HEX_SIZE, 0, rl.DARKGRAY) +} From 3ebbc2cf7e8c250316f3f6a95cc85ebe731ca7ad Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:32:43 +0000 Subject: [PATCH 05/38] feat: add language server config --- ols.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 ols.json diff --git a/ols.json b/ols.json new file mode 100644 index 0000000..58aa3ea --- /dev/null +++ b/ols.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json", + "enable_document_symbols": true, + "enable_hover": true, + "enable_snippets": true +} From 1b8659d655d121543029107a9c16a1db953794ee Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:02:11 +0000 Subject: [PATCH 06/38] feat: add unit test for hex indexing --- viewer/hex.odin | 2 +- viewer/hex_test.odin | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 viewer/hex_test.odin diff --git a/viewer/hex.odin b/viewer/hex.odin index b550fe5..bb23f14 100644 --- a/viewer/hex.odin +++ b/viewer/hex.odin @@ -19,7 +19,7 @@ new_hex :: proc(q, r, s: f32) -> Hex { } hex_index :: proc(x, y: i32) -> string { - return fmt.aprintf("%02d%02d", x, y) + return fmt.tprintf("%02d%02d", x + 1, y + 1) } hex_to_pixel :: proc(layout: Layout, h: Hex) -> Point { diff --git a/viewer/hex_test.odin b/viewer/hex_test.odin new file mode 100644 index 0000000..c9fb766 --- /dev/null +++ b/viewer/hex_test.odin @@ -0,0 +1,17 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package viewer + +import "core:testing" + +@(test) +test_hex_index :: proc(t: ^testing.T) { + testing.expect_value(t, hex_index(0, 0), "0101") + testing.expect_value(t, hex_index(1, 0), "0201") + testing.expect_value(t, hex_index(0, 1), "0102") + testing.expect_value(t, hex_index(1, 1), "0202") +} From 06e58fed3e211bbeadff855a4aff6178b8c63eb7 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:36:53 +0000 Subject: [PATCH 07/38] feat: rework hex logic --- viewer/hex.odin | 36 +++++++++++++++++++++--------------- viewer/hex_test.odin | 25 +++++++++++++++++++++++++ viewer/layout_test.odin | 11 +++++++++++ viewer/orientation.odin | 26 +++++++++++++++++++------- viewer/point.odin | 18 ++++++++++++------ viewer/sector.odin | 14 +++++++------- 6 files changed, 95 insertions(+), 35 deletions(-) create mode 100644 viewer/layout_test.odin diff --git a/viewer/hex.odin b/viewer/hex.odin index bb23f14..654fc09 100644 --- a/viewer/hex.odin +++ b/viewer/hex.odin @@ -6,39 +6,45 @@ package viewer +import "base:intrinsics" import "core:fmt" import "core:math" -import rl "vendor:raylib" -Hex :: rl.Vector3 +Hex :: struct($T: typeid) where intrinsics.type_is_numeric(T) { + q, r, s: T, +} -new_hex :: proc(q, r, s: f32) -> Hex { - assert(q + r + s == 0) +new_hex :: proc(q, r, s: $T) -> Hex(T) { + assert(math.round(f64(q + r + s)) == 0) return {q, r, s} } -hex_index :: proc(x, y: i32) -> string { +hex_index :: proc(x, y: int) -> string { return fmt.tprintf("%02d%02d", x + 1, y + 1) } -hex_to_pixel :: proc(layout: Layout, h: Hex) -> Point { +hex_lerp :: proc(a: Hex(f64), b: Hex(f64), t: f64) -> Hex(f64) { + return new_hex(a.q * (1.0 - t) + b.q * t, a.r * (1.0 - t) + b.r * t, a.s * (1.0 - t) + b.s * t) +} + +hex_to_pixel :: proc(layout: Layout, h: Hex(int)) -> Point { M := layout.orientation - x := (M.f[0][0] * h.x + M.f[0][1] * h.y) * layout.size.x - y := (M.f[1][0] * h.x + M.f[1][1] * h.y) * layout.size.y + x := (M.f0 * f64(h.q) + M.f1 * f64(h.r)) * layout.size.x + y := (M.f2 * f64(h.q) + M.f3 * f64(h.r)) * layout.size.y return new_point(x + layout.origin.x, y + layout.origin.y) } -hex_round :: proc(h: Hex) -> Hex { - q := math.round(h.x) - r := math.round(h.y) - s := math.round(h.z) +hex_round :: proc(h: Hex(f64)) -> Hex(int) { + q := int(math.round(h.q)) + r := int(math.round(h.r)) + s := int(math.round(h.s)) - q_diff := math.abs(q - h.x) - r_diff := math.abs(r - h.y) - s_diff := math.abs(s - h.z) + q_diff := math.abs(f64(q) - h.q) + r_diff := math.abs(f64(r) - h.r) + s_diff := math.abs(f64(s) - h.s) if q_diff > r_diff && q_diff > s_diff { q = -r - s diff --git a/viewer/hex_test.odin b/viewer/hex_test.odin index c9fb766..779cf45 100644 --- a/viewer/hex_test.odin +++ b/viewer/hex_test.odin @@ -15,3 +15,28 @@ test_hex_index :: proc(t: ^testing.T) { testing.expect_value(t, hex_index(0, 1), "0102") testing.expect_value(t, hex_index(1, 1), "0202") } + +@(test) +test_hex_round :: proc(t: ^testing.T) { + a := new_hex(0.0, 0.0, 0.0) + b := new_hex(1.0, -1.0, 0.0) + c := new_hex(0.0, -1.0, 1.0) + d := new_hex(10.0, -20.0, 10.0) + e := new_hex(5, -10, 5) + f := new_hex( + a.q * 0.4 + b.q * 0.3 + c.q * 0.3, + a.r * 0.4 + b.r * 0.3 + c.r * 0.3, + a.s * 0.4 + b.s * 0.3 + c.s * 0.3, + ) + g := new_hex( + a.q * 0.3 + b.q * 0.3 + c.q * 0.4, + a.r * 0.3 + b.r * 0.3 + c.r * 0.4, + a.s * 0.3 + b.s * 0.3 + c.s * 0.4, + ) + + testing.expect_value(t, hex_round(hex_lerp(a, d, 0.5)), e) + testing.expect_value(t, hex_round(a), hex_round(hex_lerp(a, b, 0.499))) + testing.expect_value(t, hex_round(b), hex_round(hex_lerp(a, b, 0.501))) + testing.expect_value(t, hex_round(a), hex_round(f)) + testing.expect_value(t, hex_round(c), hex_round(g)) +} diff --git a/viewer/layout_test.odin b/viewer/layout_test.odin new file mode 100644 index 0000000..b7ae6c6 --- /dev/null +++ b/viewer/layout_test.odin @@ -0,0 +1,11 @@ +package viewer + +import "core:testing" + +@(test) +test_layout :: proc(t: ^testing.T) { + h := new_hex(3, 4, -7) + flat := new_layout(flat_orientation(), new_point(10.0, 15.0), new_point(35.0, 71.0)) + + testing.expect_value(t, pixel_to_hex_rounded(flat, hex_to_pixel(flat, h)), h) +} diff --git a/viewer/orientation.odin b/viewer/orientation.odin index 34bba58..421a608 100644 --- a/viewer/orientation.odin +++ b/viewer/orientation.odin @@ -9,19 +9,31 @@ package viewer import "core:math" Orientation :: struct { - f: matrix[2, 2]f32, - b: matrix[2, 2]f32, - start_angle: f32, + f0: f64, + f1: f64, + f2: f64, + f3: f64, + b0: f64, + b1: f64, + b2: f64, + b3: f64, + start_angle: f64, } -new_orientation :: proc(f: matrix[2, 2]f32, b: matrix[2, 2]f32, start_angle: f32) -> Orientation { - return {f, b, start_angle} +new_orientation :: proc(f0, f1, f2, f3, b0, b1, b2, b3, start_angle: f64) -> Orientation { + return {f0, f1, f2, f3, b0, b1, b2, b3, start_angle} } flat_orientation :: proc() -> Orientation { return { - {3.0 / 2.0, 0.0, math.SQRT_THREE / 2.0, math.SQRT_THREE}, - {2.0 / 3.0, 0.0, -1.0 / 3.0, math.SQRT_THREE / 3.0}, + 3.0 / 2.0, + 0.0, + math.SQRT_THREE / 2.0, + math.SQRT_THREE, + 2.0 / 3.0, + 0.0, + -1.0 / 3.0, + math.SQRT_THREE / 3.0, 0.0, } } diff --git a/viewer/point.odin b/viewer/point.odin index 8084e6b..e4bed93 100644 --- a/viewer/point.odin +++ b/viewer/point.odin @@ -8,25 +8,31 @@ package viewer import rl "vendor:raylib" -Point :: rl.Vector2 +Point :: struct { + x, y: f64, +} -new_point :: proc(x, y: f32) -> Point { +new_point :: proc(x, y: f64) -> Point { return {x, y} } -pixel_to_hex_fractional :: proc(layout: Layout, p: Point) -> Hex { +point_to_vector :: proc(p: Point) -> rl.Vector2 { + return {f32(p.x), f32(p.y)} +} + +pixel_to_hex_fractional :: proc(layout: Layout, p: Point) -> Hex(f64) { M := layout.orientation pt := new_point( (p.x - layout.origin.x) / layout.size.x, (p.y - layout.origin.y) / layout.size.y, ) - q := M.b[0, 0] * pt.x + M.b[0, 1] * pt.y - r := M.b[1, 0] * pt.x + M.b[1, 1] * pt.y + q := M.b0 * pt.x + M.b1 * pt.y + r := M.b2 * pt.x + M.b3 * pt.y return new_hex(q, r, -q - r) } -pixel_to_hex_rounded :: proc(layout: Layout, p: Point) -> Hex { +pixel_to_hex_rounded :: proc(layout: Layout, p: Point) -> Hex(int) { return hex_round(pixel_to_hex_fractional(layout, p)) } diff --git a/viewer/sector.odin b/viewer/sector.odin index 18dc604..decd9bd 100644 --- a/viewer/sector.odin +++ b/viewer/sector.odin @@ -10,7 +10,7 @@ import rl "vendor:raylib" Sector :: struct { name: cstring, - hexes: map[string]Hex, + hexes: map[string]Hex(int), layout: Layout, } @@ -19,11 +19,11 @@ new_sector :: proc( origin: Point = {0, 0}, size: Point = {SECTOR_COLUMNS, SECTOR_ROWS}, ) -> Sector { - hexes: map[string]Hex + hexes: map[string]Hex(int) - for y in 0 ..< size.y { - for x in 0 ..< size.x { - index := hex_index(i32(x), i32(y)) + for y in 0 ..< int(size.y) { + for x in 0 ..< int(size.x) { + index := hex_index(x, y) hexes[index] = new_hex(x, y, -x - y) } } @@ -39,7 +39,7 @@ draw_sector :: proc(sector: Sector) { } } -draw_hex :: proc(layout: Layout, hex: Hex) { +draw_hex :: proc(layout: Layout, hex: Hex(int)) { center := hex_to_pixel(layout, hex) - rl.DrawPolyLines(center, 6, HEX_SIZE, 0, rl.DARKGRAY) + rl.DrawPolyLines(point_to_vector(center), 6, HEX_SIZE, 0, rl.DARKGRAY) } From 258a39fb5f51079ecb2af37d571efb33384857d1 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:37:53 +0000 Subject: [PATCH 08/38] fix: add missing license header --- viewer/layout_test.odin | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/viewer/layout_test.odin b/viewer/layout_test.odin index b7ae6c6..4ba0df3 100644 --- a/viewer/layout_test.odin +++ b/viewer/layout_test.odin @@ -1,3 +1,9 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + package viewer import "core:testing" From 9c09b2515fc5e656b8246e2e531e469fd77d4b4c Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:51:11 +0000 Subject: [PATCH 09/38] fix: shorten procedure parameters --- viewer/hex.odin | 2 +- viewer/layout.odin | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/viewer/hex.odin b/viewer/hex.odin index 654fc09..96e5ce7 100644 --- a/viewer/hex.odin +++ b/viewer/hex.odin @@ -24,7 +24,7 @@ hex_index :: proc(x, y: int) -> string { return fmt.tprintf("%02d%02d", x + 1, y + 1) } -hex_lerp :: proc(a: Hex(f64), b: Hex(f64), t: f64) -> Hex(f64) { +hex_lerp :: proc(a, b: Hex(f64), t: f64) -> Hex(f64) { return new_hex(a.q * (1.0 - t) + b.q * t, a.r * (1.0 - t) + b.r * t, a.s * (1.0 - t) + b.s * t) } diff --git a/viewer/layout.odin b/viewer/layout.odin index 29db583..c56decf 100644 --- a/viewer/layout.odin +++ b/viewer/layout.odin @@ -12,6 +12,6 @@ Layout :: struct { size: Point, } -new_layout :: proc(orientation: Orientation, origin: Point, size: Point) -> Layout { +new_layout :: proc(orientation: Orientation, origin, size: Point) -> Layout { return {orientation, size, origin} } From ab9a63af85c055d36434e1f0a8e12a8f2d43fba1 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:54:25 +0000 Subject: [PATCH 10/38] fix: shorten layout fields --- viewer/layout.odin | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/viewer/layout.odin b/viewer/layout.odin index c56decf..9332616 100644 --- a/viewer/layout.odin +++ b/viewer/layout.odin @@ -7,9 +7,8 @@ package viewer Layout :: struct { - orientation: Orientation, - origin: Point, - size: Point, + orientation: Orientation, + origin, size: Point, } new_layout :: proc(orientation: Orientation, origin, size: Point) -> Layout { From d094893748d4835782fa83a9b50c321a40fc7f0e Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:21:04 +0000 Subject: [PATCH 11/38] feat: various fixes and refactors --- viewer/camera.odin | 4 +++- viewer/hex.odin | 14 ++++++++------ viewer/hex_test.odin | 14 ++++++++++---- viewer/point.odin | 8 ++++---- viewer/sector.odin | 19 +++++++++++++------ 5 files changed, 38 insertions(+), 21 deletions(-) diff --git a/viewer/camera.odin b/viewer/camera.odin index 05e2220..ce197b5 100644 --- a/viewer/camera.odin +++ b/viewer/camera.odin @@ -31,6 +31,8 @@ zoom_camera :: proc(camera: ^rl.Camera2D) { camera.target = world scale := 0.2 * wheel - camera.zoom = clamp(math.exp(math.log(camera.zoom, math.E) + scale), 0.125, 64.0) + value := math.exp(math.log(camera.zoom, math.E) + scale) + + camera.zoom = clamp(value, 0.125, 64.0) } } diff --git a/viewer/hex.odin b/viewer/hex.odin index 96e5ce7..96ab317 100644 --- a/viewer/hex.odin +++ b/viewer/hex.odin @@ -20,21 +20,23 @@ new_hex :: proc(q, r, s: $T) -> Hex(T) { return {q, r, s} } -hex_index :: proc(x, y: int) -> string { - return fmt.tprintf("%02d%02d", x + 1, y + 1) +hex_index :: proc(h: Hex(int)) -> string { + return fmt.tprintf("%02d%02d", h.q + 1, h.r + 1) } hex_lerp :: proc(a, b: Hex(f64), t: f64) -> Hex(f64) { - return new_hex(a.q * (1.0 - t) + b.q * t, a.r * (1.0 - t) + b.r * t, a.s * (1.0 - t) + b.s * t) + return new_hex(math.lerp(a.q, b.q, t), math.lerp(a.r, b.r, t), math.lerp(a.s, b.s, t)) } hex_to_pixel :: proc(layout: Layout, h: Hex(int)) -> Point { M := layout.orientation + size := layout.size + origin := layout.origin - x := (M.f0 * f64(h.q) + M.f1 * f64(h.r)) * layout.size.x - y := (M.f2 * f64(h.q) + M.f3 * f64(h.r)) * layout.size.y + x := (M.f0 * f64(h.q) + M.f1 * f64(h.r)) * size.x + y := (M.f2 * f64(h.q) + M.f3 * f64(h.r)) * size.y - return new_point(x + layout.origin.x, y + layout.origin.y) + return new_point(x + origin.x, y + origin.y) } hex_round :: proc(h: Hex(f64)) -> Hex(int) { diff --git a/viewer/hex_test.odin b/viewer/hex_test.odin index 779cf45..8efedc7 100644 --- a/viewer/hex_test.odin +++ b/viewer/hex_test.odin @@ -8,12 +8,18 @@ package viewer import "core:testing" +@(test) +test_new_hex :: proc(t: ^testing.T) { + testing.expect_value(t, new_hex(1, 1, -2), Hex(int){1, 1, -2}) + testing.expect_value(t, new_hex(1.0, 1.0, -2.0), Hex(f64){1.0, 1.0, -2.0}) +} + @(test) test_hex_index :: proc(t: ^testing.T) { - testing.expect_value(t, hex_index(0, 0), "0101") - testing.expect_value(t, hex_index(1, 0), "0201") - testing.expect_value(t, hex_index(0, 1), "0102") - testing.expect_value(t, hex_index(1, 1), "0202") + testing.expect_value(t, hex_index(new_hex(0, 0, 0)), "0101") + testing.expect_value(t, hex_index(new_hex(1, 0, -1)), "0201") + testing.expect_value(t, hex_index(new_hex(0, 1, -1)), "0102") + testing.expect_value(t, hex_index(new_hex(1, 1, -2)), "0202") } @(test) diff --git a/viewer/point.odin b/viewer/point.odin index e4bed93..f122944 100644 --- a/viewer/point.odin +++ b/viewer/point.odin @@ -22,11 +22,11 @@ point_to_vector :: proc(p: Point) -> rl.Vector2 { pixel_to_hex_fractional :: proc(layout: Layout, p: Point) -> Hex(f64) { M := layout.orientation + origin := layout.origin + size := layout.size + + pt := new_point((p.x - origin.x) / size.x, (p.y - origin.y) / size.y) - pt := new_point( - (p.x - layout.origin.x) / layout.size.x, - (p.y - layout.origin.y) / layout.size.y, - ) q := M.b0 * pt.x + M.b1 * pt.y r := M.b2 * pt.x + M.b3 * pt.y diff --git a/viewer/sector.odin b/viewer/sector.odin index decd9bd..f946a6c 100644 --- a/viewer/sector.odin +++ b/viewer/sector.odin @@ -21,10 +21,17 @@ new_sector :: proc( ) -> Sector { hexes: map[string]Hex(int) - for y in 0 ..< int(size.y) { - for x in 0 ..< int(size.x) { - index := hex_index(x, y) - hexes[index] = new_hex(x, y, -x - y) + left := int(origin.x) + right := int(size.x) + top := int(origin.y) + bottom := int(size.y) + + for q := left; q < right; q += 1 { + q_offset := q >> 1 + for r := top - q_offset; r <= bottom - q_offset; r += 1 { + hex := new_hex(q, r, -q - r) + index := hex_index(hex) + hexes[index] = hex } } @@ -40,6 +47,6 @@ draw_sector :: proc(sector: Sector) { } draw_hex :: proc(layout: Layout, hex: Hex(int)) { - center := hex_to_pixel(layout, hex) - rl.DrawPolyLines(point_to_vector(center), 6, HEX_SIZE, 0, rl.DARKGRAY) + center := point_to_vector(hex_to_pixel(layout, hex)) + rl.DrawPolyLines(center, 6, HEX_SIZE, 0, rl.DARKGRAY) } From 080a771f2af18c132557e32576c3897731196547 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:26:08 +0000 Subject: [PATCH 12/38] feat: flatten project structure --- Makefile | 2 +- viewer/camera.odin => camera.odin | 2 +- viewer/config.odin => config.odin | 2 +- viewer/hex.odin => hex.odin | 2 +- viewer/hex_test.odin => hex_test.odin | 2 +- viewer/layout.odin => layout.odin | 2 +- viewer/layout_test.odin => layout_test.odin | 2 +- main.odin | 4 +--- viewer/orientation.odin => orientation.odin | 2 +- viewer/point.odin => point.odin | 2 +- viewer/renderer.odin => renderer.odin | 2 +- viewer/sector.odin => sector.odin | 2 +- 12 files changed, 12 insertions(+), 14 deletions(-) rename viewer/camera.odin => camera.odin (98%) rename viewer/config.odin => config.odin (95%) rename viewer/hex.odin => hex.odin (98%) rename viewer/hex_test.odin => hex_test.odin (99%) rename viewer/layout.odin => layout.odin (96%) rename viewer/layout_test.odin => layout_test.odin (96%) rename viewer/orientation.odin => orientation.odin (98%) rename viewer/point.odin => point.odin (98%) rename viewer/renderer.odin => renderer.odin (97%) rename viewer/sector.odin => sector.odin (98%) diff --git a/Makefile b/Makefile index 751e346..7e55692 100644 --- a/Makefile +++ b/Makefile @@ -29,4 +29,4 @@ run: odin run . test: - odin test . --all-packages + odin test . diff --git a/viewer/camera.odin b/camera.odin similarity index 98% rename from viewer/camera.odin rename to camera.odin index ce197b5..550ff52 100644 --- a/viewer/camera.odin +++ b/camera.odin @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package viewer +package main import "core:math" import rl "vendor:raylib" diff --git a/viewer/config.odin b/config.odin similarity index 95% rename from viewer/config.odin rename to config.odin index 180b351..9f42072 100644 --- a/viewer/config.odin +++ b/config.odin @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package viewer +package main HEX_SIZE :: 12 diff --git a/viewer/hex.odin b/hex.odin similarity index 98% rename from viewer/hex.odin rename to hex.odin index 96ab317..c1c6d1c 100644 --- a/viewer/hex.odin +++ b/hex.odin @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package viewer +package main import "base:intrinsics" import "core:fmt" diff --git a/viewer/hex_test.odin b/hex_test.odin similarity index 99% rename from viewer/hex_test.odin rename to hex_test.odin index 8efedc7..c94083e 100644 --- a/viewer/hex_test.odin +++ b/hex_test.odin @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package viewer +package main import "core:testing" diff --git a/viewer/layout.odin b/layout.odin similarity index 96% rename from viewer/layout.odin rename to layout.odin index 9332616..1c4ea37 100644 --- a/viewer/layout.odin +++ b/layout.odin @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package viewer +package main Layout :: struct { orientation: Orientation, diff --git a/viewer/layout_test.odin b/layout_test.odin similarity index 96% rename from viewer/layout_test.odin rename to layout_test.odin index 4ba0df3..b7e55e0 100644 --- a/viewer/layout_test.odin +++ b/layout_test.odin @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package viewer +package main import "core:testing" diff --git a/main.odin b/main.odin index 14297dc..2d1e13e 100644 --- a/main.odin +++ b/main.odin @@ -6,8 +6,6 @@ package main -import "viewer" - main :: proc() { - viewer.run() + run() } diff --git a/viewer/orientation.odin b/orientation.odin similarity index 98% rename from viewer/orientation.odin rename to orientation.odin index 421a608..1fbeae7 100644 --- a/viewer/orientation.odin +++ b/orientation.odin @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package viewer +package main import "core:math" diff --git a/viewer/point.odin b/point.odin similarity index 98% rename from viewer/point.odin rename to point.odin index f122944..a9fe355 100644 --- a/viewer/point.odin +++ b/point.odin @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package viewer +package main import rl "vendor:raylib" diff --git a/viewer/renderer.odin b/renderer.odin similarity index 97% rename from viewer/renderer.odin rename to renderer.odin index aa054e2..6c5109e 100644 --- a/viewer/renderer.odin +++ b/renderer.odin @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package viewer +package main import rl "vendor:raylib" diff --git a/viewer/sector.odin b/sector.odin similarity index 98% rename from viewer/sector.odin rename to sector.odin index f946a6c..a9fc49a 100644 --- a/viewer/sector.odin +++ b/sector.odin @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -package viewer +package main import rl "vendor:raylib" From e9bdd26bae591ca5190e025c5d2dc5fcfd2098a5 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:33:40 +0000 Subject: [PATCH 13/38] feat: add strict style checking --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 7e55692..a3e95ff 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ format: lint: hadolint .devcontainer/Dockerfile - odin check . -vet + odin check . -strict-style -vet yamllint .github/workflows/linux.yml run: From fbfd85ef42514563020ef88dd3bf79cabc00282d Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:57:27 +0000 Subject: [PATCH 14/38] feat: add basic error handler --- renderer.odin => app.odin | 12 +++++++++++- main.odin | 6 +++++- 2 files changed, 16 insertions(+), 2 deletions(-) rename renderer.odin => app.odin (82%) diff --git a/renderer.odin b/app.odin similarity index 82% rename from renderer.odin rename to app.odin index 6c5109e..d34e5e7 100644 --- a/renderer.odin +++ b/app.odin @@ -8,10 +8,18 @@ package main import rl "vendor:raylib" -run :: proc() { +Error :: enum { + Initialization_Failed, +} + +run :: proc() -> Error { rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE) defer rl.CloseWindow() + if rl.IsWindowReady() { + return .Initialization_Failed + } + camera := new_camera() sector := new_sector("Spinward Marches") @@ -30,4 +38,6 @@ run :: proc() { rl.EndDrawing() } + + return nil } diff --git a/main.odin b/main.odin index 2d1e13e..dbd4c47 100644 --- a/main.odin +++ b/main.odin @@ -6,6 +6,10 @@ package main +import "core:os" + main :: proc() { - run() + if err := run(); err != nil { + os.exit(1) + } } From 306999831010b2483ebb527f49a524a4d3308679 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 05:58:14 +0000 Subject: [PATCH 15/38] fix: address error conditional --- app.odin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.odin b/app.odin index d34e5e7..770468a 100644 --- a/app.odin +++ b/app.odin @@ -16,7 +16,7 @@ run :: proc() -> Error { rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE) defer rl.CloseWindow() - if rl.IsWindowReady() { + if !rl.IsWindowReady() { return .Initialization_Failed } From df1e1ee4fdec923edd2fc453f6822ff83f91a308 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:11:25 +0000 Subject: [PATCH 16/38] fix: remove unused constructor --- orientation.odin | 4 ---- 1 file changed, 4 deletions(-) diff --git a/orientation.odin b/orientation.odin index 1fbeae7..907ae20 100644 --- a/orientation.odin +++ b/orientation.odin @@ -20,10 +20,6 @@ Orientation :: struct { start_angle: f64, } -new_orientation :: proc(f0, f1, f2, f3, b0, b1, b2, b3, start_angle: f64) -> Orientation { - return {f0, f1, f2, f3, b0, b1, b2, b3, start_angle} -} - flat_orientation :: proc() -> Orientation { return { 3.0 / 2.0, From ea95cde92f4859737bd2b8f90eab350c76853f07 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:36:39 +0000 Subject: [PATCH 17/38] feat: use vector types --- hex.odin | 38 ++++++++++++++++++-------------------- hex_test.odin | 16 ++++++++-------- orientation.odin | 18 +++++++++--------- point.odin | 14 ++++---------- sector.odin | 18 +++++++++--------- 5 files changed, 48 insertions(+), 56 deletions(-) diff --git a/hex.odin b/hex.odin index c1c6d1c..f431a86 100644 --- a/hex.odin +++ b/hex.odin @@ -6,47 +6,45 @@ package main -import "base:intrinsics" import "core:fmt" import "core:math" +import rl "vendor:raylib" -Hex :: struct($T: typeid) where intrinsics.type_is_numeric(T) { - q, r, s: T, -} +Hex :: rl.Vector3 -new_hex :: proc(q, r, s: $T) -> Hex(T) { - assert(math.round(f64(q + r + s)) == 0) +new_hex :: proc(q, r, s: f32) -> Hex { + assert(math.round(q + r + s) == 0) return {q, r, s} } -hex_index :: proc(h: Hex(int)) -> string { - return fmt.tprintf("%02d%02d", h.q + 1, h.r + 1) +hex_index :: proc(h: Hex) -> string { + return fmt.tprintf("%02d%02d", i32(h.x + 1), i32(h.y + 1)) } -hex_lerp :: proc(a, b: Hex(f64), t: f64) -> Hex(f64) { - return new_hex(math.lerp(a.q, b.q, t), math.lerp(a.r, b.r, t), math.lerp(a.s, b.s, t)) +hex_lerp :: proc(a, b: Hex, t: f32) -> Hex { + return math.lerp(a, b, t) } -hex_to_pixel :: proc(layout: Layout, h: Hex(int)) -> Point { +hex_to_pixel :: proc(layout: Layout, h: Hex) -> Point { M := layout.orientation size := layout.size origin := layout.origin - x := (M.f0 * f64(h.q) + M.f1 * f64(h.r)) * size.x - y := (M.f2 * f64(h.q) + M.f3 * f64(h.r)) * size.y + x := (M.f0 * h.x + M.f1 * h.y) * size.x + y := (M.f2 * h.x + M.f3 * h.y) * size.y return new_point(x + origin.x, y + origin.y) } -hex_round :: proc(h: Hex(f64)) -> Hex(int) { - q := int(math.round(h.q)) - r := int(math.round(h.r)) - s := int(math.round(h.s)) +hex_round :: proc(h: Hex) -> Hex { + q := math.round(h.x) + r := math.round(h.y) + s := math.round(h.z) - q_diff := math.abs(f64(q) - h.q) - r_diff := math.abs(f64(r) - h.r) - s_diff := math.abs(f64(s) - h.s) + q_diff := math.abs(q - h.x) + r_diff := math.abs(r - h.y) + s_diff := math.abs(s - h.z) if q_diff > r_diff && q_diff > s_diff { q = -r - s diff --git a/hex_test.odin b/hex_test.odin index c94083e..273b9e4 100644 --- a/hex_test.odin +++ b/hex_test.odin @@ -10,8 +10,8 @@ import "core:testing" @(test) test_new_hex :: proc(t: ^testing.T) { - testing.expect_value(t, new_hex(1, 1, -2), Hex(int){1, 1, -2}) - testing.expect_value(t, new_hex(1.0, 1.0, -2.0), Hex(f64){1.0, 1.0, -2.0}) + testing.expect_value(t, new_hex(1, 1, -2), Hex{1, 1, -2}) + testing.expect_value(t, new_hex(1.0, 1.0, -2.0), Hex{1.0, 1.0, -2.0}) } @(test) @@ -30,14 +30,14 @@ test_hex_round :: proc(t: ^testing.T) { d := new_hex(10.0, -20.0, 10.0) e := new_hex(5, -10, 5) f := new_hex( - a.q * 0.4 + b.q * 0.3 + c.q * 0.3, - a.r * 0.4 + b.r * 0.3 + c.r * 0.3, - a.s * 0.4 + b.s * 0.3 + c.s * 0.3, + a.x * 0.4 + b.x * 0.3 + c.x * 0.3, + a.y * 0.4 + b.y * 0.3 + c.y * 0.3, + a.z * 0.4 + b.z * 0.3 + c.z * 0.3, ) g := new_hex( - a.q * 0.3 + b.q * 0.3 + c.q * 0.4, - a.r * 0.3 + b.r * 0.3 + c.r * 0.4, - a.s * 0.3 + b.s * 0.3 + c.s * 0.4, + a.x * 0.3 + b.x * 0.3 + c.x * 0.4, + a.y * 0.3 + b.y * 0.3 + c.y * 0.4, + a.z * 0.3 + b.z * 0.3 + c.z * 0.4, ) testing.expect_value(t, hex_round(hex_lerp(a, d, 0.5)), e) diff --git a/orientation.odin b/orientation.odin index 907ae20..d9eac17 100644 --- a/orientation.odin +++ b/orientation.odin @@ -9,15 +9,15 @@ package main import "core:math" Orientation :: struct { - f0: f64, - f1: f64, - f2: f64, - f3: f64, - b0: f64, - b1: f64, - b2: f64, - b3: f64, - start_angle: f64, + f0: f32, + f1: f32, + f2: f32, + f3: f32, + b0: f32, + b1: f32, + b2: f32, + b3: f32, + start_angle: f32, } flat_orientation :: proc() -> Orientation { diff --git a/point.odin b/point.odin index a9fe355..23fb7c8 100644 --- a/point.odin +++ b/point.odin @@ -8,19 +8,13 @@ package main import rl "vendor:raylib" -Point :: struct { - x, y: f64, -} +Point :: rl.Vector2 -new_point :: proc(x, y: f64) -> Point { +new_point :: proc(x, y: f32) -> Point { return {x, y} } -point_to_vector :: proc(p: Point) -> rl.Vector2 { - return {f32(p.x), f32(p.y)} -} - -pixel_to_hex_fractional :: proc(layout: Layout, p: Point) -> Hex(f64) { +pixel_to_hex_fractional :: proc(layout: Layout, p: Point) -> Hex { M := layout.orientation origin := layout.origin size := layout.size @@ -33,6 +27,6 @@ pixel_to_hex_fractional :: proc(layout: Layout, p: Point) -> Hex(f64) { return new_hex(q, r, -q - r) } -pixel_to_hex_rounded :: proc(layout: Layout, p: Point) -> Hex(int) { +pixel_to_hex_rounded :: proc(layout: Layout, p: Point) -> Hex { return hex_round(pixel_to_hex_fractional(layout, p)) } diff --git a/sector.odin b/sector.odin index a9fc49a..faabac3 100644 --- a/sector.odin +++ b/sector.odin @@ -10,7 +10,7 @@ import rl "vendor:raylib" Sector :: struct { name: cstring, - hexes: map[string]Hex(int), + hexes: map[string]Hex, layout: Layout, } @@ -19,15 +19,15 @@ new_sector :: proc( origin: Point = {0, 0}, size: Point = {SECTOR_COLUMNS, SECTOR_ROWS}, ) -> Sector { - hexes: map[string]Hex(int) + hexes: map[string]Hex - left := int(origin.x) - right := int(size.x) - top := int(origin.y) - bottom := int(size.y) + left := origin.x + right := size.x + top := origin.y + bottom := size.y for q := left; q < right; q += 1 { - q_offset := q >> 1 + q_offset := f32(i32(q) >> 1) for r := top - q_offset; r <= bottom - q_offset; r += 1 { hex := new_hex(q, r, -q - r) index := hex_index(hex) @@ -46,7 +46,7 @@ draw_sector :: proc(sector: Sector) { } } -draw_hex :: proc(layout: Layout, hex: Hex(int)) { - center := point_to_vector(hex_to_pixel(layout, hex)) +draw_hex :: proc(layout: Layout, h: Hex) { + center := hex_to_pixel(layout, h) rl.DrawPolyLines(center, 6, HEX_SIZE, 0, rl.DARKGRAY) } From ffdc170306ffd398c00572953b628daa34ea8d11 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:47:26 +0000 Subject: [PATCH 18/38] fix: use floor function --- sector.odin | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sector.odin b/sector.odin index faabac3..d207955 100644 --- a/sector.odin +++ b/sector.odin @@ -6,6 +6,7 @@ package main +import "core:math" import rl "vendor:raylib" Sector :: struct { @@ -27,7 +28,7 @@ new_sector :: proc( bottom := size.y for q := left; q < right; q += 1 { - q_offset := f32(i32(q) >> 1) + q_offset := math.floor(q / 2.0) for r := top - q_offset; r <= bottom - q_offset; r += 1 { hex := new_hex(q, r, -q - r) index := hex_index(hex) From 4c0778aaaf59eb0a63ed5b50c1a98de1d86cb36e Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:06:16 +0000 Subject: [PATCH 19/38] feat: use matrix types --- hex.odin | 4 ++-- orientation.odin | 26 ++++++++------------------ point.odin | 4 ++-- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/hex.odin b/hex.odin index f431a86..d69f964 100644 --- a/hex.odin +++ b/hex.odin @@ -31,8 +31,8 @@ hex_to_pixel :: proc(layout: Layout, h: Hex) -> Point { size := layout.size origin := layout.origin - x := (M.f0 * h.x + M.f1 * h.y) * size.x - y := (M.f2 * h.x + M.f3 * h.y) * size.y + x := (M.f[0, 0] * h.x + M.f[0, 1] * h.y) * size.x + y := (M.f[1, 0] * h.x + M.f[1, 1] * h.y) * size.y return new_point(x + origin.x, y + origin.y) } diff --git a/orientation.odin b/orientation.odin index d9eac17..d9dc662 100644 --- a/orientation.odin +++ b/orientation.odin @@ -7,29 +7,19 @@ package main import "core:math" +import "core:math/linalg" Orientation :: struct { - f0: f32, - f1: f32, - f2: f32, - f3: f32, - b0: f32, - b1: f32, - b2: f32, - b3: f32, + f: matrix[2, 2]f32, + b: matrix[2, 2]f32, start_angle: f32, } flat_orientation :: proc() -> Orientation { - return { - 3.0 / 2.0, - 0.0, - math.SQRT_THREE / 2.0, - math.SQRT_THREE, - 2.0 / 3.0, - 0.0, - -1.0 / 3.0, - math.SQRT_THREE / 3.0, - 0.0, + f := matrix[2, 2]f32{ + 3.0 / 2.0, 0.0, + math.SQRT_THREE / 2.0, math.SQRT_THREE, } + + return {f, linalg.inverse(f), 0.0} } diff --git a/point.odin b/point.odin index 23fb7c8..8603b58 100644 --- a/point.odin +++ b/point.odin @@ -21,8 +21,8 @@ pixel_to_hex_fractional :: proc(layout: Layout, p: Point) -> Hex { pt := new_point((p.x - origin.x) / size.x, (p.y - origin.y) / size.y) - q := M.b0 * pt.x + M.b1 * pt.y - r := M.b2 * pt.x + M.b3 * pt.y + q := M.b[0, 0] * pt.x + M.b[0, 1] * pt.y + r := M.b[1, 0] * pt.x + M.b[1, 1] * pt.y return new_hex(q, r, -q - r) } From b17135b1fbf01152899545a10548f86c980c7269 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:03:58 +0000 Subject: [PATCH 20/38] feat: add unit test for camera constructor --- camera_test.odin | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 camera_test.odin diff --git a/camera_test.odin b/camera_test.odin new file mode 100644 index 0000000..8319208 --- /dev/null +++ b/camera_test.odin @@ -0,0 +1,15 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package main + +import "core:testing" +import rl "vendor:raylib" + +@(test) +test_new_camera :: proc(t: ^testing.T) { + testing.expect_value(t, new_camera(), rl.Camera2D{zoom = 1.0}) +} From ce16a166ef7485f0da581688792f281d982ebc18 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:07:43 +0000 Subject: [PATCH 21/38] feat: add unit test for point constructor --- point_test.odin | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 point_test.odin diff --git a/point_test.odin b/point_test.odin new file mode 100644 index 0000000..b89d0b9 --- /dev/null +++ b/point_test.odin @@ -0,0 +1,14 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package main + +import "core:testing" + +@(test) +test_new_point :: proc(t: ^testing.T) { + testing.expect_value(t, new_point(1, 2), Point{1, 2}) +} From 8712e30beaa032187ea616775d5482934fa8d395 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:37:48 +0000 Subject: [PATCH 22/38] feat: condense camera logic --- app.odin | 3 +-- camera.odin | 28 +++++++++++++--------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/app.odin b/app.odin index 770468a..dab3631 100644 --- a/app.odin +++ b/app.odin @@ -29,8 +29,7 @@ run :: proc() -> Error { rl.ClearBackground(rl.BLACK) rl.DrawFPS(16, WINDOW_HEIGHT - 32) - pan_camera(&camera) - zoom_camera(&camera) + poll_camera(&camera) rl.BeginMode2D(camera) draw_sector(sector) diff --git a/camera.odin b/camera.odin index 550ff52..7ebf1a2 100644 --- a/camera.odin +++ b/camera.odin @@ -13,26 +13,24 @@ new_camera :: proc() -> rl.Camera2D { return {zoom = 1.0} } -pan_camera :: proc(camera: ^rl.Camera2D) { +poll_camera :: proc(camera: ^rl.Camera2D) { if rl.IsMouseButtonDown(.RIGHT) { - delta := rl.GetMouseDelta() - delta *= -1.0 / camera.zoom - - camera.target += delta + pan_camera(camera, rl.GetMouseDelta()) } -} -zoom_camera :: proc(camera: ^rl.Camera2D) { if wheel := rl.GetMouseWheelMove(); wheel != 0.0 { - position := rl.GetMousePosition() - world := rl.GetScreenToWorld2D(position, camera^) - - camera.offset = position - camera.target = world + zoom_camera(camera, wheel, rl.GetMousePosition()) + } +} - scale := 0.2 * wheel - value := math.exp(math.log(camera.zoom, math.E) + scale) +pan_camera :: proc(camera: ^rl.Camera2D, delta: rl.Vector2) { + camera.target += delta * -1.0 / camera.zoom +} - camera.zoom = clamp(value, 0.125, 64.0) +zoom_camera :: proc(camera: ^rl.Camera2D, wheel: f32, position: rl.Vector2) { + camera^ = { + offset = position, + target = rl.GetScreenToWorld2D(position, camera^), + zoom = clamp(math.exp(math.log(camera.zoom, math.E) + 0.2 * wheel), 0.125, 64.0), } } From 6751aaca11dcc7138b18c3fc0fd910621161a043 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:48:32 +0000 Subject: [PATCH 23/38] fix: address operator error --- sector.odin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sector.odin b/sector.odin index d207955..97a3684 100644 --- a/sector.odin +++ b/sector.odin @@ -27,7 +27,7 @@ new_sector :: proc( top := origin.y bottom := size.y - for q := left; q < right; q += 1 { + for q := left; q <= right; q += 1 { q_offset := math.floor(q / 2.0) for r := top - q_offset; r <= bottom - q_offset; r += 1 { hex := new_hex(q, r, -q - r) From 89b282e29816847594d3574193a480521e35e0b2 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:00:08 +0000 Subject: [PATCH 24/38] feat: add unit test for camera panning --- camera_test.odin | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/camera_test.odin b/camera_test.odin index 8319208..94664ce 100644 --- a/camera_test.odin +++ b/camera_test.odin @@ -13,3 +13,12 @@ import rl "vendor:raylib" test_new_camera :: proc(t: ^testing.T) { testing.expect_value(t, new_camera(), rl.Camera2D{zoom = 1.0}) } + +@(test) +test_pan_camera :: proc(t: ^testing.T) { + camera := new_camera() + + pan_camera(&camera, {1.0, 2.0}) + + testing.expect_value(t, camera.target, rl.Vector2{-1.0, -2.0}) +} From ff463b2a6779a7d3ba15e80fc67e14982413dfde Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:05:56 +0000 Subject: [PATCH 25/38] feat: add unit test for camera zooming --- camera_test.odin | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/camera_test.odin b/camera_test.odin index 94664ce..6d25c69 100644 --- a/camera_test.odin +++ b/camera_test.odin @@ -22,3 +22,14 @@ test_pan_camera :: proc(t: ^testing.T) { testing.expect_value(t, camera.target, rl.Vector2{-1.0, -2.0}) } + +@(test) +test_zoom_camera :: proc(t: ^testing.T) { + camera := new_camera() + + zoom_camera(&camera, 1.0, {1.0, 2.0}) + + testing.expect_value(t, camera.offset, rl.Vector2{1, 2}) + testing.expect_value(t, camera.target, rl.Vector2{1, 2}) + testing.expect_value(t, camera.zoom, 1.2214028) +} From c1ad7ef806af0193575ebcf99a68a9bab5846f9c Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:08:24 +0000 Subject: [PATCH 26/38] fix: rename constants file --- config.odin => constants.odin | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename config.odin => constants.odin (100%) diff --git a/config.odin b/constants.odin similarity index 100% rename from config.odin rename to constants.odin From ef9d947920698b947ea4d4f41e26748d45cb8886 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:18:21 +0000 Subject: [PATCH 27/38] feat: create separate file for types --- app.odin | 4 ---- hex.odin | 3 --- layout.odin | 5 ----- orientation.odin | 6 ------ point.odin | 4 ---- sector.odin | 6 ------ types.odin | 34 ++++++++++++++++++++++++++++++++++ 7 files changed, 34 insertions(+), 28 deletions(-) create mode 100644 types.odin diff --git a/app.odin b/app.odin index dab3631..108abc5 100644 --- a/app.odin +++ b/app.odin @@ -8,10 +8,6 @@ package main import rl "vendor:raylib" -Error :: enum { - Initialization_Failed, -} - run :: proc() -> Error { rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE) defer rl.CloseWindow() diff --git a/hex.odin b/hex.odin index d69f964..5888795 100644 --- a/hex.odin +++ b/hex.odin @@ -8,9 +8,6 @@ package main import "core:fmt" import "core:math" -import rl "vendor:raylib" - -Hex :: rl.Vector3 new_hex :: proc(q, r, s: f32) -> Hex { assert(math.round(q + r + s) == 0) diff --git a/layout.odin b/layout.odin index 1c4ea37..4856442 100644 --- a/layout.odin +++ b/layout.odin @@ -6,11 +6,6 @@ package main -Layout :: struct { - orientation: Orientation, - origin, size: Point, -} - new_layout :: proc(orientation: Orientation, origin, size: Point) -> Layout { return {orientation, size, origin} } diff --git a/orientation.odin b/orientation.odin index d9dc662..95314f5 100644 --- a/orientation.odin +++ b/orientation.odin @@ -9,12 +9,6 @@ package main import "core:math" import "core:math/linalg" -Orientation :: struct { - f: matrix[2, 2]f32, - b: matrix[2, 2]f32, - start_angle: f32, -} - flat_orientation :: proc() -> Orientation { f := matrix[2, 2]f32{ 3.0 / 2.0, 0.0, diff --git a/point.odin b/point.odin index 8603b58..afac172 100644 --- a/point.odin +++ b/point.odin @@ -6,10 +6,6 @@ package main -import rl "vendor:raylib" - -Point :: rl.Vector2 - new_point :: proc(x, y: f32) -> Point { return {x, y} } diff --git a/sector.odin b/sector.odin index 97a3684..6294151 100644 --- a/sector.odin +++ b/sector.odin @@ -9,12 +9,6 @@ package main import "core:math" import rl "vendor:raylib" -Sector :: struct { - name: cstring, - hexes: map[string]Hex, - layout: Layout, -} - new_sector :: proc( name: cstring, origin: Point = {0, 0}, diff --git a/types.odin b/types.odin new file mode 100644 index 0000000..f44965f --- /dev/null +++ b/types.odin @@ -0,0 +1,34 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package main + +import rl "vendor:raylib" + +Error :: enum { + Initialization_Failed, +} + +Hex :: rl.Vector3 + +Layout :: struct { + orientation: Orientation, + origin, size: Point, +} + +Orientation :: struct { + f: matrix[2, 2]f32, + b: matrix[2, 2]f32, + start_angle: f32, +} + +Point :: rl.Vector2 + +Sector :: struct { + name: cstring, + hexes: map[string]Hex, + layout: Layout, +} From d279f509badf39be700d8db6480513da61e1c1bf Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:49:53 +0000 Subject: [PATCH 28/38] feat: add unit test for flat orientations --- orientation_test.odin | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 orientation_test.odin diff --git a/orientation_test.odin b/orientation_test.odin new file mode 100644 index 0000000..f578cb7 --- /dev/null +++ b/orientation_test.odin @@ -0,0 +1,25 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package main + +import "core:math" +import "core:testing" + +@(test) +test_flat_orientation :: proc(t: ^testing.T) { + f := matrix[2, 2]f32{ + 3.0 / 2.0, 0.0, + math.SQRT_THREE / 2.0, math.SQRT_THREE, + } + + b := matrix[2, 2]f32{ + 2.0 / 3.0, -0.0, + -1.0 / 3.0, math.SQRT_THREE / 3.0, + } + + testing.expect_value(t, flat_orientation(), Orientation{f, b, 0.0}) +} From 1c4b004c50c9ba216ea0c6bfba745b567f549d7a Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:10:43 +0000 Subject: [PATCH 29/38] feat: add code coverage measuring --- .devcontainer/devcontainer.json | 5 ++++- .gitignore | 1 + Makefile | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 81bc0d2..81de289 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,5 +21,8 @@ "ols.server.path": "ols" } } - } + }, + "securityOpt": [ + "seccomp=unconfined" + ] } diff --git a/.gitignore b/.gitignore index 19066f6..cc61f8b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. .env +coverage/ spinward diff --git a/Makefile b/Makefile index a3e95ff..42e28ea 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ ifneq (,$(wildcard ./.env)) export endif -.PHONY: all build clean format lint run test +.PHONY: all build clean coverage format lint run test all: lint test build @@ -17,6 +17,10 @@ build: clean: git clean -fdxe ".env" +coverage: + odin build . -build-mode:test -debug + kcov --dump-summary --exclude-pattern=_test.odin --include-path=. --include-pattern=.odin ./coverage ./spinward + format: odinfmt . -w From a9581d58dc1b303c9c09afae473e9c6702765f03 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Thu, 16 Jul 2026 03:46:38 +0000 Subject: [PATCH 30/38] fix: dump code coverage results to hidden folder --- .gitignore | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index cc61f8b..6e00695 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,5 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. .env -coverage/ +.kcov/ spinward diff --git a/Makefile b/Makefile index 42e28ea..701ad4f 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ clean: coverage: odin build . -build-mode:test -debug - kcov --dump-summary --exclude-pattern=_test.odin --include-path=. --include-pattern=.odin ./coverage ./spinward + kcov --dump-summary --exclude-pattern=_test.odin --include-path=. --include-pattern=.odin .kcov spinward format: odinfmt . -w From 54e686784c5012c721c17a59859d56fa1d1778f4 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Thu, 16 Jul 2026 03:49:35 +0000 Subject: [PATCH 31/38] fix: remove redundant pattern inclusion --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 701ad4f..5fb527e 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ clean: coverage: odin build . -build-mode:test -debug - kcov --dump-summary --exclude-pattern=_test.odin --include-path=. --include-pattern=.odin .kcov spinward + kcov --dump-summary --exclude-pattern=_test.odin --include-path=. .kcov spinward format: odinfmt . -w From 05d0d8aa69a3cfc7db4fdf63aab190c51f8886cd Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Thu, 16 Jul 2026 04:07:11 +0000 Subject: [PATCH 32/38] feat: add unit test for sectors --- layout.odin | 2 +- layout_test.odin | 2 +- sector.odin | 6 +++++- sector_test.odin | 19 +++++++++++++++++++ types.odin | 2 +- 5 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 sector_test.odin diff --git a/layout.odin b/layout.odin index 4856442..0425a66 100644 --- a/layout.odin +++ b/layout.odin @@ -7,5 +7,5 @@ package main new_layout :: proc(orientation: Orientation, origin, size: Point) -> Layout { - return {orientation, size, origin} + return {orientation, origin, size} } diff --git a/layout_test.odin b/layout_test.odin index b7e55e0..2d5214c 100644 --- a/layout_test.odin +++ b/layout_test.odin @@ -9,7 +9,7 @@ package main import "core:testing" @(test) -test_layout :: proc(t: ^testing.T) { +test_new_layout :: proc(t: ^testing.T) { h := new_hex(3, 4, -7) flat := new_layout(flat_orientation(), new_point(10.0, 15.0), new_point(35.0, 71.0)) diff --git a/sector.odin b/sector.odin index 6294151..aab2bc8 100644 --- a/sector.odin +++ b/sector.odin @@ -10,7 +10,7 @@ import "core:math" import rl "vendor:raylib" new_sector :: proc( - name: cstring, + name: string, origin: Point = {0, 0}, size: Point = {SECTOR_COLUMNS, SECTOR_ROWS}, ) -> Sector { @@ -35,6 +35,10 @@ new_sector :: proc( return {name, hexes, layout} } +delete_sector :: proc(sector: Sector) { + delete(sector.hexes) +} + draw_sector :: proc(sector: Sector) { for _, hex in sector.hexes { draw_hex(sector.layout, hex) diff --git a/sector_test.odin b/sector_test.odin new file mode 100644 index 0000000..50f2a2f --- /dev/null +++ b/sector_test.odin @@ -0,0 +1,19 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package main + +import "core:testing" + +@(test) +test_new_sector :: proc(t: ^testing.T) { + sector := new_sector("Foo") + defer delete_sector(sector) + + testing.expect_value(t, sector.name, "Foo") + testing.expect_value(t, sector.layout.origin, Point{0, 0}) + testing.expect_value(t, sector.layout.size, Point{SECTOR_COLUMNS, SECTOR_ROWS}) +} diff --git a/types.odin b/types.odin index f44965f..0d3f884 100644 --- a/types.odin +++ b/types.odin @@ -28,7 +28,7 @@ Orientation :: struct { Point :: rl.Vector2 Sector :: struct { - name: cstring, + name: string, hexes: map[string]Hex, layout: Layout, } From a28083ac5aaacfe76165b71d2d48c514bd11429c Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:17:46 +0000 Subject: [PATCH 33/38] feat: add offset coordinate type --- offset.odin | 11 +++++++++++ offset_test.odin | 14 ++++++++++++++ types.odin | 2 ++ 3 files changed, 27 insertions(+) create mode 100644 offset.odin create mode 100644 offset_test.odin diff --git a/offset.odin b/offset.odin new file mode 100644 index 0000000..fa94f72 --- /dev/null +++ b/offset.odin @@ -0,0 +1,11 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package main + +new_offset :: proc(col, row: f32) -> Offset { + return {col, row} +} diff --git a/offset_test.odin b/offset_test.odin new file mode 100644 index 0000000..a14df37 --- /dev/null +++ b/offset_test.odin @@ -0,0 +1,14 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package main + +import "core:testing" + +@(test) +test_new_offset :: proc(t: ^testing.T) { + testing.expect_value(t, new_offset(1, 2), Offset{1, 2}) +} diff --git a/types.odin b/types.odin index 0d3f884..5f87e13 100644 --- a/types.odin +++ b/types.odin @@ -19,6 +19,8 @@ Layout :: struct { origin, size: Point, } +Offset :: rl.Vector2 + Orientation :: struct { f: matrix[2, 2]f32, b: matrix[2, 2]f32, From 8391acbf0b7368f8ebc2c2936789a016285a966e Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:37:44 +0000 Subject: [PATCH 34/38] feat: add unit tests for offset coordinates --- constants.odin | 2 ++ offset.odin | 15 +++++++++++++++ offset_test.odin | 12 ++++++++++++ 3 files changed, 29 insertions(+) diff --git a/constants.odin b/constants.odin index 9f42072..e2932ee 100644 --- a/constants.odin +++ b/constants.odin @@ -8,6 +8,8 @@ package main HEX_SIZE :: 12 +ODD_OFFSET :: -1 + WINDOW_WIDTH :: 1920 WINDOW_HEIGHT :: 1080 WINDOW_TITLE :: "Spinward" diff --git a/offset.odin b/offset.odin index fa94f72..1755e91 100644 --- a/offset.odin +++ b/offset.odin @@ -9,3 +9,18 @@ package main new_offset :: proc(col, row: f32) -> Offset { return {col, row} } + +qoffset_from_cube :: proc(hex: Hex) -> Offset { + col := hex.x + row := hex.y + (hex.x + ODD_OFFSET * f32(i32(hex.x) & 1)) / 2 + + return new_offset(col, row) +} + +qoffset_to_cube :: proc(offset: Offset) -> Hex { + q := offset.x + r := offset.y - (offset.x + ODD_OFFSET * f32(i32(offset.x) & 1)) / 2 + s := -q - r + + return new_hex(q, r, s) +} diff --git a/offset_test.odin b/offset_test.odin index a14df37..5d2855d 100644 --- a/offset_test.odin +++ b/offset_test.odin @@ -12,3 +12,15 @@ import "core:testing" test_new_offset :: proc(t: ^testing.T) { testing.expect_value(t, new_offset(1, 2), Offset{1, 2}) } + +@(test) +test_qoffset_from_cube :: proc(t: ^testing.T) { + testing.expect_value(t, qoffset_from_cube(new_hex(-2, 3, -1)), new_offset(-2, 2)) + testing.expect_value(t, qoffset_from_cube(new_hex(-1, -1, 2)), new_offset(-1, -2)) +} + +@(test) +test_qoffset_to_cube :: proc(t: ^testing.T) { + testing.expect_value(t, qoffset_to_cube(new_offset(-2, 2)), new_hex(-2, 3, -1)) + testing.expect_value(t, qoffset_to_cube(new_offset(-1, -2)), new_hex(-1, -1, 2)) +} From e57e1d96b3ff5520abc1c1bfcf944f11c861ced7 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Thu, 16 Jul 2026 08:01:34 +0000 Subject: [PATCH 35/38] feat: add offset coordinate into hex indexer --- hex.odin | 26 ++++++++++++++------------ layout_test.odin | 4 ++-- sector.odin | 7 ++++--- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/hex.odin b/hex.odin index 5888795..394c1be 100644 --- a/hex.odin +++ b/hex.odin @@ -15,33 +15,35 @@ new_hex :: proc(q, r, s: f32) -> Hex { return {q, r, s} } -hex_index :: proc(h: Hex) -> string { - return fmt.tprintf("%02d%02d", i32(h.x + 1), i32(h.y + 1)) +hex_index :: proc(hex: Hex) -> string { + offset := qoffset_from_cube(hex) + + return fmt.tprintf("%02d%02d", i32(offset.x + 1), i32(offset.y + 1)) } hex_lerp :: proc(a, b: Hex, t: f32) -> Hex { return math.lerp(a, b, t) } -hex_to_pixel :: proc(layout: Layout, h: Hex) -> Point { +hex_to_pixel :: proc(layout: Layout, hex: Hex) -> Point { M := layout.orientation size := layout.size origin := layout.origin - x := (M.f[0, 0] * h.x + M.f[0, 1] * h.y) * size.x - y := (M.f[1, 0] * h.x + M.f[1, 1] * h.y) * size.y + x := (M.f[0, 0] * hex.x + M.f[0, 1] * hex.y) * size.x + y := (M.f[1, 0] * hex.x + M.f[1, 1] * hex.y) * size.y return new_point(x + origin.x, y + origin.y) } -hex_round :: proc(h: Hex) -> Hex { - q := math.round(h.x) - r := math.round(h.y) - s := math.round(h.z) +hex_round :: proc(hex: Hex) -> Hex { + q := math.round(hex.x) + r := math.round(hex.y) + s := math.round(hex.z) - q_diff := math.abs(q - h.x) - r_diff := math.abs(r - h.y) - s_diff := math.abs(s - h.z) + q_diff := math.abs(q - hex.x) + r_diff := math.abs(r - hex.y) + s_diff := math.abs(s - hex.z) if q_diff > r_diff && q_diff > s_diff { q = -r - s diff --git a/layout_test.odin b/layout_test.odin index 2d5214c..16a6705 100644 --- a/layout_test.odin +++ b/layout_test.odin @@ -10,8 +10,8 @@ import "core:testing" @(test) test_new_layout :: proc(t: ^testing.T) { - h := new_hex(3, 4, -7) + hex := new_hex(3, 4, -7) flat := new_layout(flat_orientation(), new_point(10.0, 15.0), new_point(35.0, 71.0)) - testing.expect_value(t, pixel_to_hex_rounded(flat, hex_to_pixel(flat, h)), h) + testing.expect_value(t, pixel_to_hex_rounded(flat, hex_to_pixel(flat, hex)), hex) } diff --git a/sector.odin b/sector.odin index aab2bc8..a834ddf 100644 --- a/sector.odin +++ b/sector.odin @@ -6,6 +6,7 @@ package main +import "core:fmt" import "core:math" import rl "vendor:raylib" @@ -21,7 +22,7 @@ new_sector :: proc( top := origin.y bottom := size.y - for q := left; q <= right; q += 1 { + for q := left; q < right; q += 1 { q_offset := math.floor(q / 2.0) for r := top - q_offset; r <= bottom - q_offset; r += 1 { hex := new_hex(q, r, -q - r) @@ -45,7 +46,7 @@ draw_sector :: proc(sector: Sector) { } } -draw_hex :: proc(layout: Layout, h: Hex) { - center := hex_to_pixel(layout, h) +draw_hex :: proc(layout: Layout, hex: Hex) { + center := hex_to_pixel(layout, hex) rl.DrawPolyLines(center, 6, HEX_SIZE, 0, rl.DARKGRAY) } From 40e62b4cfb487a56c00f88f1808be6e99405a717 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Thu, 16 Jul 2026 08:04:48 +0000 Subject: [PATCH 36/38] fix: remove unused import --- sector.odin | 1 - 1 file changed, 1 deletion(-) diff --git a/sector.odin b/sector.odin index a834ddf..f8cd920 100644 --- a/sector.odin +++ b/sector.odin @@ -6,7 +6,6 @@ package main -import "core:fmt" import "core:math" import rl "vendor:raylib" From 545a52f8eeb2bc7ec9f87c781ac9bb2d096d4d5d Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Thu, 16 Jul 2026 08:22:49 +0000 Subject: [PATCH 37/38] fix: layout size needs to be equal to hex size --- sector.odin | 12 ++++-------- sector_test.odin | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/sector.odin b/sector.odin index f8cd920..a9bf5b7 100644 --- a/sector.odin +++ b/sector.odin @@ -9,17 +9,13 @@ package main import "core:math" import rl "vendor:raylib" -new_sector :: proc( - name: string, - origin: Point = {0, 0}, - size: Point = {SECTOR_COLUMNS, SECTOR_ROWS}, -) -> Sector { +new_sector :: proc(name: string, origin: Point = {0, 0}) -> Sector { hexes: map[string]Hex left := origin.x - right := size.x + right := origin.x + SECTOR_COLUMNS top := origin.y - bottom := size.y + bottom := origin.y + SECTOR_ROWS for q := left; q < right; q += 1 { q_offset := math.floor(q / 2.0) @@ -30,7 +26,7 @@ new_sector :: proc( } } - layout := new_layout(flat_orientation(), origin, size) + layout := new_layout(flat_orientation(), origin, {HEX_SIZE, HEX_SIZE}) return {name, hexes, layout} } diff --git a/sector_test.odin b/sector_test.odin index 50f2a2f..3623d5e 100644 --- a/sector_test.odin +++ b/sector_test.odin @@ -15,5 +15,5 @@ test_new_sector :: proc(t: ^testing.T) { testing.expect_value(t, sector.name, "Foo") testing.expect_value(t, sector.layout.origin, Point{0, 0}) - testing.expect_value(t, sector.layout.size, Point{SECTOR_COLUMNS, SECTOR_ROWS}) + testing.expect_value(t, sector.layout.size, Point{12, 12}) } From b542bb9bdae445effab66f4567df9434b3ea92f7 Mon Sep 17 00:00:00 2001 From: Theomund <34360334+theomund@users.noreply.github.com> Date: Thu, 16 Jul 2026 09:25:09 +0000 Subject: [PATCH 38/38] feat: add hex hovering --- app.odin | 2 +- constants.odin | 2 +- sector.odin | 29 ++++++++++++++++++++++++----- sector_test.odin | 2 +- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/app.odin b/app.odin index 108abc5..c5e1771 100644 --- a/app.odin +++ b/app.odin @@ -28,7 +28,7 @@ run :: proc() -> Error { poll_camera(&camera) rl.BeginMode2D(camera) - draw_sector(sector) + draw_sector(sector, camera) rl.EndMode2D() rl.EndDrawing() diff --git a/constants.odin b/constants.odin index e2932ee..4a4ff4b 100644 --- a/constants.odin +++ b/constants.odin @@ -6,7 +6,7 @@ package main -HEX_SIZE :: 12 +HEX_SIZE :: 24 ODD_OFFSET :: -1 diff --git a/sector.odin b/sector.odin index a9bf5b7..ff010af 100644 --- a/sector.odin +++ b/sector.odin @@ -7,6 +7,7 @@ package main import "core:math" +import "core:strings" import rl "vendor:raylib" new_sector :: proc(name: string, origin: Point = {0, 0}) -> Sector { @@ -15,7 +16,7 @@ new_sector :: proc(name: string, origin: Point = {0, 0}) -> Sector { left := origin.x right := origin.x + SECTOR_COLUMNS top := origin.y - bottom := origin.y + SECTOR_ROWS + bottom := origin.y + SECTOR_ROWS - 1 for q := left; q < right; q += 1 { q_offset := math.floor(q / 2.0) @@ -35,13 +36,31 @@ delete_sector :: proc(sector: Sector) { delete(sector.hexes) } -draw_sector :: proc(sector: Sector) { +contains_hex :: proc(hex: Hex) -> bool { + offset := qoffset_from_cube(hex) + + return offset.x >= 0 && offset.y >= 0 && offset.x < SECTOR_COLUMNS && offset.y < SECTOR_ROWS +} + +draw_sector :: proc(sector: Sector, camera: rl.Camera2D) { for _, hex in sector.hexes { - draw_hex(sector.layout, hex) + draw_hex(sector.layout, hex, rl.DARKGRAY) + } + + position := rl.GetScreenToWorld2D(rl.GetMousePosition(), camera) + hovered := pixel_to_hex_rounded(sector.layout, position) + + if contains_hex(hovered) { + index := strings.clone_to_cstring(hex_index(hovered)) + + rl.DrawText(index, i32(position.x), i32(position.y - 8), 8, rl.WHITE) + + draw_hex(sector.layout, hovered, rl.RED) } } -draw_hex :: proc(layout: Layout, hex: Hex) { +draw_hex :: proc(layout: Layout, hex: Hex, color: rl.Color) { center := hex_to_pixel(layout, hex) - rl.DrawPolyLines(center, 6, HEX_SIZE, 0, rl.DARKGRAY) + + rl.DrawPolyLines(center, 6, HEX_SIZE, 0, color) } diff --git a/sector_test.odin b/sector_test.odin index 3623d5e..2a4c99b 100644 --- a/sector_test.odin +++ b/sector_test.odin @@ -15,5 +15,5 @@ test_new_sector :: proc(t: ^testing.T) { testing.expect_value(t, sector.name, "Foo") testing.expect_value(t, sector.layout.origin, Point{0, 0}) - testing.expect_value(t, sector.layout.size, Point{12, 12}) + testing.expect_value(t, sector.layout.size, Point{HEX_SIZE, HEX_SIZE}) }