Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
87f23dc
oonpia basics
th555 Dec 20, 2025
2e3e547
oonpia basics
th555 Dec 20, 2025
a59dba5
automatically pick valid or most likely stone type on click
th555 Dec 20, 2025
1ec4396
better move string validation
th555 Dec 21, 2025
a4dde42
click handling
th555 Dec 22, 2025
581b44c
apply move from object
th555 Dec 22, 2025
c935b1a
small fixes, move generation
th555 Dec 22, 2025
fa99a72
fix isSelfCapture, disable move generation
th555 Dec 22, 2025
9cec6f8
custom random moves
th555 Dec 22, 2025
665a6ef
game end, all rules implemented
th555 Dec 25, 2025
41eb681
custom colours, komi pie using builtin pie
th555 Dec 28, 2025
4fc6a55
display customization
th555 Dec 29, 2025
6d2aea7
oonpia wip
th555 Jan 8, 2026
07930b6
Merge branch 'develop' of github.com:th555/abstractplay_gameslib into…
th555 Jan 15, 2026
c89a9ad
small tweaks
th555 Feb 21, 2026
95f2101
experimental
th555 Feb 21, 2026
19d66bb
Merge branch 'develop' of github.com:th555/abstractplay_gameslib into…
th555 Feb 21, 2026
cd6f033
fix msg
th555 Feb 22, 2026
6c4aded
description
th555 Feb 22, 2026
731185e
complete=0 to make cycling through pieces possible
th555 Feb 22, 2026
85d7bec
Merge branch 'develop' of github.com:th555/abstractplay_gameslib into…
th555 Feb 22, 2026
24ef185
set canrender
th555 Feb 22, 2026
d794ab7
Merge branch 'develop' of github.com:th555/abstractplay_gameslib into…
th555 Feb 22, 2026
6dc1063
Merge branch 'develop' of github.com:th555/abstractplay_gameslib into…
th555 Mar 5, 2026
e3ecb21
oonpia: use new customization functions
th555 Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions locales/en/apgames.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@
"omny": "Generalized connection game where players try to split star cells into different regions so that no single region contains a majority of star cells.",
"onager": "In Onager each player tries to reach the opponent's back rank. Onager is named after a Roman siege engine that is a type of catapult, as the way the pieces move resembles how projectiles are hurled forward with this device.",
"onyx": "A connection game on a modified snub-square board with a capture rule.",
"oonpia": "A hexagonal go-like where pieces come in two types. Only pieces of opposite type are connected, and a 4-arc of pieces of the same type (regardless of colour) is forbidden. Either use the legend to select a piece, or click the same location multiple times to cycle through all possible pieces.",
"oonpia": "A hexagonal go-like where pieces come in two types. Only pieces of opposite type are connected, and a 4-arc of pieces of the same type (regardless of colour) is forbidden.",
"orb": "Generatorb is 2-player game played on a standard chess board. Players start in opposite corners and attempt to reach their opponent's generator core or occupy the majority of cells on the front line. During play, you can stack up to three checkers in a space. Stacks of different heights behave differently, leading to engaging strategic options.",
"ordo": "Ordo is a \"get to your opponent's home row\" game in which you must always keep your pieces connected. Pieces can move singly and also as a group in certain situations. You can also win by breaking up your opponent's group in such a way that they can't reconnect it.",
"oust": "Oust is the classic \"exnihilation\" game where the game starts with an empty board and the goal is to eliminate all of your opponent's pieces. On your turn, you may make multiple capturing placements if available, but you must end it with a non-capturing placement. You must pass if you are not able to make any placements.",
Expand Down Expand Up @@ -261,6 +261,7 @@
"magnate": "The terminology of some Magnate actions has been altered for clarity and brevity. Completely developing a new property is called the \"Buy\" action; purchasing a deed for a new property is called \"Deed\", developing deeds (that is, adding tokens to a deeded property, whether it results in the deed becoming fully developed or not) is called \"Add\". (Selling a card and trading suit tokens 3 for 1 are unchanged.)\n\nIn order to speed up the process of rolling for resources, there are two additional actions:\n* \"Prefer\" is for setting your preference of which suit token to take when a deed pays out on your opponent's roll. If you do not set an explicit preference, the code will choose the rarer token for you based on your non-crown suits and current supply of tokens. The currently preferred token is circled in the UI, but your personal preference is never visible to the other player.\n* \"Choose\" is a mandatory first action for collecting suit tokens when a deed pays out on your own roll. (In all cases where you need to choose a suit token that is not already among your tokens, you still click on the appropriate token pile.)\n\nBecause you can perform several actions during a ply in any order, there is also an \"Undo\" action to back out your most recent action, whether or not it was complete.\n\nNote that only the final resource die result is displayed, but the distribution of expected outcomes is still that of rolling 2d10 and taking the higher value. Taxation happens when the lower of 2d10 comes up 1; a suit die is rolled (or two, in the double taxation variant), and the suit(s) will be displayed underneath the resource result. The roll is logged at the end of a player's turn, and is attributed to the next player (who would have rolled in the physical game). Except for a \"Choose\", no user action is required; resources are added and/or removed automatically by the server in between turns.\n\nWhen a player is ahead in a district, the Pawn or Excuse for that district is outlined in that player's color. The first tiebreaker score (total property value) is displayed in parentheses after the district score. The second tiebreaker is total number of tokens remaining.",
"mchess": "If there have been seven consecutive turns without a capture, someone can \"call the clock\" by adding an asterisk (*) to the end of their move. This can only be done by selecting the move from the drop-down list. After another seven turns with no capture, the game will end and be scored.",
"murus": "The default ruleset is \"Advanced Murus Gallicus\" (with catapults). By default, your first move is to redistribute one of your starting towers as walls on your second row. Additionally, the standard pie rule is also available. There are three variants you can mix and match:\n\n* \"Basic\" reverts the game to the \"no catapult\" state.\n* \"Static\" disables the initial tower redistribution.\n* \"Escape\" eliminates the breakthrough win condition.",
"oonpia": "Either use the legend to select a piece, or click the same location multiple times to cycle through all possible pieces. Blocked cells are highlighted: a translucent dot means only a dotted stone can be placed there (i.e. blocked for plain pieces), a translucent piece means only a plain piece can be placed there (i.e. blocked for dotted pieces). If both highlights are present the the cell is blocked for all pieces.",
"oware": "This implementation follows the common tournament rule that grand slam moves are allowed, but no pieces are captured. Depicting state changes in sowing games is challenging. The initial chosen pit is marked, as is any capture. Small numbers appear to show the change in the number of stones in each pit. If you believe you have encountered a bug, please let us know in Discord.",
"pacru": "This implementation adheres to the 2011 rule change that requires at least one opponent to have at least nine tiles on the board before meetings will trigger.",
"pigs": "Unlike the old Super Duper Games implementation, this one implements the core rule set. Each player enters all five moves, and they are resolved at once.\n\nMovement is resolved before damage is applied.",
Expand Down Expand Up @@ -3147,17 +3148,9 @@
}
},
"oonpia": {
"palette_no_blocked_no": {
"description": "Black/white/blue colours, don't highlight blocked cells",
"name": "palette_no_blocked_no"
},
"palette_yes_blocked_yes": {
"description": "Palette colours, highlight blocked cells.",
"name": "palette_yes_blocked_yes"
},
"palette_yes_blocked_no": {
"description": "Palette colours, don't highlight blocked cells.",
"name": "palette_yes_blocked_no"
"blocked_no": {
"description": "Don't highlight blocked cells",
"name": "blocked_no"
}
},
"oust": {
Expand Down
199 changes: 74 additions & 125 deletions src/games/oonpia.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GameBase, IAPGameState, IClickResult, ICustomButton, IIndividualState, IRenderOpts, IValidationResult } from "./_base";
import { APGamesInformation } from "../schemas/gameinfo";
import { APRenderRep, AreaKey, BoardBasic } from "@abstractplay/renderer/src/schemas/schema";
import { APRenderRep, AreaKey, BoardBasic, Colourfuncs } from "@abstractplay/renderer/src/schemas/schema";
import { APMoveResult } from "../schemas/moveresults";
import { randomInt, reviver, UserFacingError } from "../common";
import i18next from "i18next";
Expand Down Expand Up @@ -39,8 +39,8 @@ export class OonpiaGame extends GameBase {
name: "Oonpia",
uid: "oonpia",
playercounts: [2],
version: "20260222",
dateAdded: "2026-02-22",
version: "20260305",
dateAdded: "2026-03-05",
// i18next.t("apgames:descriptions.oonpia")
description: "apgames:descriptions.oonpia",
// i18next.t("apgames:notes.oonpia")
Expand Down Expand Up @@ -72,11 +72,31 @@ export class OonpiaGame extends GameBase {
{ uid: "size-11", group: "board" },
{ uid: "size-12", group: "board" }
],
displays: [ // default: palette_no_blocked_yes
{uid: "palette_no_blocked_no"},
{uid: "palette_yes_blocked_yes"},
{uid: "palette_yes_blocked_no"},
]
displays: [ // default: blocked_yes
{uid: "blocked_no"}
],
customizations: [
{
num: 1,
default: "#eeeeee",
explanation: "Player 1 colour"
},
{
num: 2,
default: "#252525",
explanation: "Player 2 colour"
},
{
num: 3,
default: "#0165fc",
explanation: "Colour of the neutral stones"
},
{
name: "board",
default: "#e0bb6c",
explanation: "Board colour"
}
],
};

public numplayers = 2;
Expand All @@ -91,7 +111,6 @@ export class OonpiaGame extends GameBase {
public results: Array<APMoveResult> = [];
public prison: [number, number] = [0, 0];
private boardSize = 0;
private usePalette = false;

constructor(state?: IOonpiaState | string, variants?: string[]) {
super();
Expand Down Expand Up @@ -890,13 +909,29 @@ export class OonpiaGame extends GameBase {
}


public getPlayerColour(p: playerid): number|string {
// previous versions, just return the player id
if (this.usePalette) {
return p;
} else {
return {1: "#eeeeee", 2: "#252525", 3: "#0165fc"}[p] // colours from besogo viewer
public getPlayerColour(p: playerid): number|Colourfuncs {
if (p === 1) {
return {
func: "custom",
default: "#eeeeee",
palette: 1
}
}
if (p === 2) {
return {
func: "custom",
default: "#252525",
palette: 2
}
}
if (p === 3) {
return {
func: "custom",
default: "#0165fc",
palette: 3
}
}
return p;
}

public render(opts?: IRenderOpts): APRenderRep {
Expand All @@ -905,19 +940,8 @@ export class OonpiaGame extends GameBase {
altDisplay = opts.altDisplay;
}
let highlightBlocked = true;
if (altDisplay !== undefined) {
if (altDisplay === "palette_no_blocked_no") {
this.usePalette = false;
highlightBlocked = false;
}
if (altDisplay === "palette_yes_blocked_yes") {
this.usePalette = true;
highlightBlocked = true;
}
if (altDisplay === "palette_yes_blocked_no") {
this.usePalette = true;
highlightBlocked = false;
}
if (altDisplay !== undefined && altDisplay === "blocked_no") {
highlightBlocked = false;
}

const p1 = this.getPlayerColour(1);
Expand Down Expand Up @@ -958,10 +982,6 @@ export class OonpiaGame extends GameBase {
pstr.push(pieces);
}

const s = this.boardSize - 1;
const boardcol = this.usePalette ? 4 : "#e0bb6c";
const boardEdgeW = 55;

const hasPrison = this.prison.reduce((prev, curr) => prev + curr, 0) > 0;
const prisonPiece: Glyph[] = [];
if (hasPrison) {
Expand Down Expand Up @@ -995,82 +1015,14 @@ export class OonpiaGame extends GameBase {
minWidth: this.boardSize,
maxWidth: this.boardSize * 2 - 1,
strokeWeight: 0.5,
markers: [
{
type: "shading",
belowGrid: true,
points: [
{ row: 0, col: 0 },
{ row: 0, col: s },
{ row: s, col: s*2 },
{ row: s*2, col: s },
{ row: s*2, col: 0 },
{ row: s, col: 0 },
],
colour: boardcol,
opacity: 1,
},
{
type: "line",
belowGrid: true,
points: [
{ row: 0, col: 0 },
{ row: 0, col: s}
],
colour: boardcol,
width: boardEdgeW,
},
{
type: "line",
belowGrid: true,
points: [
{ row: 0, col: s },
{ row: s, col: s*2 },
],
colour: boardcol,
width: boardEdgeW,
},
{
type: "line",
belowGrid: true,
points: [
{ row: s, col: s*2 },
{ row: s*2, col: s },
],
colour: boardcol,
width: boardEdgeW,
},
{
type: "line",
belowGrid: true,
points: [
{ row: s*2, col: s },
{ row: s*2, col: 0 },
],
colour: boardcol,
width: boardEdgeW,
},
{
type: "line",
belowGrid: true,
points: [
{ row: s*2, col: 0 },
{ row: s, col: 0 },
],
colour: boardcol,
width: boardEdgeW,
},
{
type: "line",
belowGrid: true,
points: [
{ row: s, col: 0 },
{ row: 0, col: 0 },
],
colour: boardcol,
width: boardEdgeW,
backFill: {
type: "board",
colour: {
func: "custom",
default: "#e0bb6c",
palette: "_context_board"
}
]
}
},
legend: {
A: {name: "piece-borderless", colour: p1, scale: 1.1},
Expand Down Expand Up @@ -1157,30 +1109,27 @@ export class OonpiaGame extends GameBase {
}

if (highlightBlocked) {
(rep.board as BoardBasic).markers = [];
const {1: blockedPlain, 2: blockedDotted} = this.blockedCells();
for (const cell of blockedPlain) {
const [x, y] = this.graph.algebraic2coords(cell);
if ("markers" in (rep.board! as BoardBasic)) { // make the compiler happy
((rep.board! as BoardBasic).markers!).push({
type: "dots",
points: [{row: y, col: x}],
colour: "#000",
opacity: 0.2,
size: 0.3
})
}
((rep.board! as BoardBasic).markers!).push({
type: "dots",
points: [{row: y, col: x}],
colour: "#000",
opacity: 0.2,
size: 0.3
})
}
for (const cell of blockedDotted) {
const [x, y] = this.graph.algebraic2coords(cell);
if ("markers" in (rep.board! as BoardBasic)) { // make the compiler happy
((rep.board! as BoardBasic).markers!).push({
type: "dots",
points: [{row: y, col: x}],
colour: "#000",
opacity: 0.2,
size: 0.9
})
}
((rep.board! as BoardBasic).markers!).push({
type: "dots",
points: [{row: y, col: x}],
colour: "#000",
opacity: 0.2,
size: 0.9
})
}
}

Expand Down