Skip to content

Prep for Rotation Feature in Crosshairs - #38

Closed
samtcifihi wants to merge 2 commits into
AbstractPlay:developfrom
samtcifihi:claude/crosshairs-rotation-chappy-kver8m
Closed

samtcifihi wants to merge 2 commits into
AbstractPlay:developfrom
samtcifihi:claude/crosshairs-rotation-chappy-kver8m

Conversation

@samtcifihi

Copy link
Copy Markdown

What

tldr, it adds an option that Crosshairs can use to implement rotation without numbers being either sideways or overlapping plane glyphs sometimes. Chappy made initial branches here and in the gameslib for me, but I wasn't sure if it was right, so I had Claude go over it, and he recommended resolving this PR first and only once it was working, do the PR for the gameslib change, since otherwise the gameslib might ask for a renderer function which didn't exist

What follows was generated by Claude; I've kept it unchanged since it has a lot of specific detail which might be helpful:

Adds nudge.relativeTo ("glyph" | "piece", default "glyph") to the glyph schema.

Today a nudge offset is always applied in the glyph's own post-rotation/flip axes. For an upright label (orientation: "vertical") attached to a piece that itself rotates with the board, that means the label's offset is pinned to a fixed screen angle — it does not travel with the piece as the board rotates. relativeTo: "piece" translates in the enclosing piece/face axes instead, so an upright label can stay at a fixed position relative to a rotating piece (e.g. beside its nose, or its tail) at any board rotation.

This is needed for the AbstractPlay/gameslib Crosshairs board-rotation feature: Crosshairs' numeric/numeric-abstract display modes put small altitude digits at a fixed angular offset from each plane's heading, and those digits must stay in that same relative position as the user rotates the board in 30° increments.

Why here, not in gameslib

Gameslib has no way to know the user's chosen board rotation at legend-construction time — only the renderer applies it, when it counter-rotates upright glyphs and bakes in the board transform. So the "stay relative to the piece as it rotates" behavior can only be implemented in the renderer.

Compatibility

  • Purely additive and opt-in: relativeTo is optional and defaults to "glyph", which is exactly the prior use.dmove(dx, dy) behavior. No existing nudge usage is affected.
  • Schema, .d.ts, and docs (docs/glyphs.md) updated together.

Testing

  • test/glyphNudge.render.test.ts (new): for every combination of board rotation (0°–330° in 30° steps) and relativeTo (undefined / "glyph" / "piece"), renders an upright, flipped, rotated glyph with a nudge and checks the resulting screen-space offset against the expected value for each coordinate system.
  • Full suite: 341 passing.
  • npm run typecheck and npm run lint clean.
  • Reverting just the _base.ts hunk and re-running the downstream gameslib Crosshairs rotation test reproduces 22 failures (digits land at the wrong angle at every rotation except 0°), confirming the change is load-bearing, not decorative.

🤖 Generated with Claude Code

samtcifihi and others added 2 commits September 10, 2026 09:26
Crosshairs needs altitude numbers to remain upright while their positions follow the rotating planes. Existing glyph nudges follow the glyph's own counter-rotation and therefore stay fixed on screen.

Add opt-in nudge.relativeTo = "piece" to translate in enclosing piece/face axes after glyph rotation and flips, retaining scaled offset units. Preserve existing behavior when omitted or set to "glyph". Regenerate schema types and document the option.

Validation: 18 new offset tests and 4 existing flip/rotation tests pass; renderer compilation with ES2022 libraries and targeted lint pass. Tested jointly with the Crosshairs change across all 12 angles and 4 display modes.

Companion proposal: samtcifihi/ap-gameslib branch crosshairs-board-rotation-30. Release the renderer update before updating gameslib's renderer dependency and integrating that branch.
@samtcifihi

Copy link
Copy Markdown
Author

Here was Claude's step recommendations to me (edited for irrelevant information):

  1. This PR
  2. Wait for AbstractPlay's package registry to publish a new renderer build from that merge (or find out how that publish step works in their pipeline — I haven't seen a workflow file for it, so I'd check .github/workflows in ap-renderer before assuming it's automatic).
  3. Bump gameslib's @abstractplay/renderer pin to that new build in the same commit/PR that includes Chappy's game logic + my dependency fix, then push and let gameslib's CI validate against the real published package (not my local symlink workaround).
  4. Open the gameslib PR. (ie, the actual Crosshairs change that should allow rotation)

I'm not entirely sure what steps 2 and 3 entail, so I've left them verbatim as Claude put them. Barring more specific instructions, I'll assume that step 2 is complete when you say the renderer change is good to go, and I'll ask Claude to walk me through step 3 and verify whether it's something on my end or not

@Perlkonig

Copy link
Copy Markdown
Member

I am very protective of the renderer. It is the core of everything. AI has routinely gotten things wrong. Please tell me what problem this PR is trying to solve. Yes I will read the code closely, but I'd like to hear from you what the goal is. Thanks!

@samtcifihi

Copy link
Copy Markdown
Author

Makes total sense to be cautious, yeah.

Some of the display options in Crosshairs use numbers to indicate height. But rather than being centered in the cell like in many games, or always in a particular corner like Decktet games, they rotate around with the plane to nicely sit in the whitespace next to the plane icon. But they also rotate themselves to stay upright because they're meant to be read. So unless we're missing something, there's currently a tradeoff in whether we want to have numbers sometimes overlap plane glyphs, or have them sometimes sideways and upside down. The intention here is to avoid needing to make that tradeoff

When I had Claude review it, I specifically highlighted that I wasn't sure a renderer change was necessary, but you're right that a way to do it without a renderer change could have slipped by him

PS: After writing it, I ran this comment by Claude for factual accuracy, and he agreed I was correctly representing what he was trying to do, but acknowledged that he may have missed an alternate approach

@Perlkonig

Copy link
Copy Markdown
Member

I'm at a disadvantage not having played or coded crosshairs. I will take a look at how Fritz builds the render. There may be a simpler solution. If you happen to have a game state that illustrates the problem, that would be great.

@Perlkonig

Copy link
Copy Markdown
Member

And yes, steps 2 and 3 above are about the workflow. Once a new version of the renderer gets published, it propagates through the tool chain, bumping the pinned I'm each of the consumers. It's one reason a full deployment takes as long as it does.

@samtcifihi

samtcifihi commented Sep 15, 2026 •

Copy link
Copy Markdown
Author

Since Crosshairs doesn't currently allow rotation, everything works, but here's a game state with some planes in various orientations. The "Plane glyphs with two small numeric altitude indicators." is the display option in question. The current code won't let you rotate, but if you rotate anyway, this should illustrate it

{"game":"crosshairs","numplayers":2,"variants":["asymmetric-random-start","unbounded-cloud-banks"],"gameover":false,"winner":[],"stack":[{"_version":"20260301","_results":[],"_timestamp":"2026-09-15T12:30:02.407Z","currplayer":1,"board":{"dataType":"Map","value":[]},"clouds":{"dataType":"Set","value":["i4","i7","e5","e9","h1","i2","b2","c3","b3","e2","e8","a3","i1","j2","c4","k4"]},"planesRemaining":[6,6],"turnNumber":1},{"_version":"20260301","_results":[{"type":"place","what":"plane","where":"a5"},{"type":"orient","where":"a5","facing":"NE"}],"_timestamp":"2026-09-15T12:30:07.045Z","currplayer":2,"lastmove":"enter:a5/ne","board":{"dataType":"Map","value":[["a5",[1,"NE",0]]]},"clouds":{"dataType":"Set","value":["i4","i7","e5","e9","h1","i2","b2","c3","b3","e2","e8","a3","i1","j2","c4","k4"]},"planesRemaining":[5,6],"turnNumber":2},{"_version":"20260301","_results":[{"type":"place","what":"plane","where":"k3"},{"type":"orient","where":"k3","facing":"NW"},{"type":"place","what":"plane","where":"k5"},{"type":"orient","where":"k5","facing":"SW"}],"_timestamp":"2026-09-15T12:30:11.522Z","currplayer":1,"lastmove":"enter:k3/nw, enter:k5/sw","board":{"dataType":"Map","value":[["a5",[1,"NE",0]],["k3",[2,"NW",0]],["k5",[2,"SW",0]]]},"clouds":{"dataType":"Set","value":["i4","i7","e5","e9","h1","i2","b2","c3","b3","e2","e8","a3","i1","j2","c4","k4"]},"planesRemaining":[5,4],"turnNumber":3},{"_version":"20260301","_results":[{"type":"move","from":"a5","to":"b5","what":"plane"},{"type":"orient","where":"b5","facing":"N"},{"type":"place","what":"plane","where":"a6"},{"type":"orient","where":"a6","facing":"SE"},{"type":"place","what":"plane","where":"a2"},{"type":"orient","where":"a2","facing":"SE"}],"_timestamp":"2026-09-15T12:30:20.170Z","currplayer":2,"lastmove":"a5+b5/n, enter:a6/se, enter:a2/se","board":{"dataType":"Map","value":[["k3",[2,"NW",0]],["k5",[2,"SW",0]],["b5",[1,"N",1]],["a6",[1,"SE",0]],["a2",[1,"SE",0]]]},"clouds":{"dataType":"Set","value":["i4","i7","e5","e9","h1","i2","b2","c3","b3","e2","e8","a3","i1","j2","c4","k4"]},"planesRemaining":[3,4],"turnNumber":4},{"_version":"20260301","_results":[{"type":"move","from":"k3","to":"j3","what":"plane"},{"type":"orient","where":"j3","facing":"N"},{"type":"move","from":"k5","to":"j6","what":"plane"},{"type":"orient","where":"j6","facing":"S"},{"type":"place","what":"plane","where":"k5"},{"type":"orient","where":"k5","facing":"NW"},{"type":"place","what":"plane","where":"k1"},{"type":"orient","where":"k1","facing":"NW"}],"_timestamp":"2026-09-15T12:30:27.826Z","currplayer":1,"lastmove":"k3+j3/n, k5+j6/s, enter:k5/nw, enter:k1/nw","board":{"dataType":"Map","value":[["b5",[1,"N",1]],["a6",[1,"SE",0]],["a2",[1,"SE",0]],["j3",[2,"N",1]],["j6",[2,"S",1]],["k5",[2,"NW",0]],["k1",[2,"NW",0]]]},"clouds":{"dataType":"Set","value":["i4","i7","e5","e9","h1","i2","b2","c3","b3","e2","e8","a3","i1","j2","c4","k4"]},"planesRemaining":[3,2],"turnNumber":5},{"_version":"20260301","_results":[{"type":"move","from":"a2","to":"b3","what":"plane"},{"type":"orient","where":"b3","facing":"NE"},{"type":"move","from":"b5","to":"b4","what":"plane"},{"type":"orient","where":"b4","facing":"NE"},{"type":"move","from":"a6","to":"b7","what":"plane"},{"type":"place","what":"plane","where":"a5"},{"type":"orient","where":"a5","facing":"N"},{"type":"place","what":"plane","where":"a1"},{"type":"orient","where":"a1","facing":"S"}],"_timestamp":"2026-09-15T12:30:40.495Z","currplayer":2,"lastmove":"a2+b3/ne, b5+b4/ne, a6+b7, enter:a5/n, enter:a1/s","board":{"dataType":"Map","value":[["j3",[2,"N",1]],["j6",[2,"S",1]],["k5",[2,"NW",0]],["k1",[2,"NW",0]],["b3",[1,"NE",1]],["b4",[1,"NE",2]],["b7",[1,"SE",1]],["a5",[1,"N",0]],["a1",[1,"S",0]]]},"clouds":{"dataType":"Set","value":["i4","i7","e5","e9","h1","i2","b2","c3","b3","e2","e8","a3","i1","j2","c4","k4"]},"planesRemaining":[1,2],"turnNumber":6},{"_version":"20260301","_results":[{"type":"move","from":"k1","to":"j1","what":"plane"},{"type":"move","from":"j3","to":"j2","what":"plane"},{"type":"orient","where":"j2","facing":"NW"},{"type":"move","from":"k5","to":"j5","what":"plane"},{"type":"orient","where":"j5","facing":"N"},{"type":"move","from":"j6","to":"j7","what":"plane"},{"type":"orient","where":"j7","facing":"SW"},{"type":"place","what":"plane","where":"k5"},{"type":"orient","where":"k5","facing":"SW"},{"type":"place","what":"plane","where":"k3"},{"type":"orient","where":"k3","facing":"N"}],"_timestamp":"2026-09-15T12:30:59.292Z","currplayer":1,"lastmove":"k1+j1, j3+j2/nw, k5+j5/n, j6+j7/sw, enter:k5/sw, enter:k3/n","board":{"dataType":"Map","value":[["b3",[1,"NE",1]],["b4",[1,"NE",2]],["b7",[1,"SE",1]],["a5",[1,"N",0]],["a1",[1,"S",0]],["j1",[2,"NW",1]],["j2",[2,"NW",2]],["j5",[2,"N",1]],["j7",[2,"SW",2]],["k5",[2,"SW",0]],["k3",[2,"N",0]]]},"clouds":{"dataType":"Set","value":["i4","i7","e5","e9","h1","i2","b2","c3","b3","e2","e8","a3","i1","j2","c4","k4"]},"planesRemaining":[1,0],"turnNumber":7}]}

PS: I've asked Claude to see if he can find an alternate approach that doesn't need a gameslib change. I'll add a comment if he finds something. EDIT: Claude on my end couldn't find any alternate approaches, for what it's worth, and he gave some arguments as to why he thinks the gameslib change is necessary as well as cited a place where he thinks the renderer already includes a similar patch, but he could be wrong. Unless specified otherwise I'll assume that relaying what Claude says beyond generalities would be unhelpful and redundant with what Claude says on your end

@Perlkonig

Copy link
Copy Markdown
Member

It is absolutely true that gameslib does not know anything about user rotation. I'm not against adjusting the renderer, to be clear. I just wanted to make sure I fully understood the problem the PR was trying to solve before diving in to validate it. I just know from experience that AI has routinely blown the renderer up, so I'm just extra cautious.

@samtcifihi

Copy link
Copy Markdown
Author

Totally makes sense; measure twice cut once and all that. Feel free to ask me if you need anything else

@Perlkonig

Copy link
Copy Markdown
Member

So the ultimate goal here is just to be able to rotate the board and keep everything looking correct, including the little numbers?

@samtcifihi

Copy link
Copy Markdown
Author

That is correct

@Perlkonig

Copy link
Copy Markdown
Member

So I'm going with the spirit of the PR but not the PR itself. We're going to add relativeTo: ["piece"|"glyph"] to the schema, but the default will be "piece" for text elements and "glyph" for all the others. This seems to ensure backwards compatibility without editing the existing gameslib renders (though I'm still testing). Thanks for highlighting this nudging gap.

@Perlkonig Perlkonig closed this Sep 15, 2026
@samtcifihi
samtcifihi deleted the claude/crosshairs-rotation-chappy-kver8m branch September 15, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants