Skip to content

Commit 9777531

Browse files
committed
Jacynth: Ready for live testing
1 parent a98708c commit 9777531

File tree

10 files changed

+1113
-7
lines changed

10 files changed

+1113
-7
lines changed

locales/en/apgames.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"hula": "Two players aim to form a loop around the center of a regular hexagonal board, using their own stones and, possibly, some number of neutral stones.",
8181
"iqishiqi": "Iqishiqi (pronounced EE-chee-shee-chee) is an abstract strategy game that is played on a hexagonal board composed of hexagonal cells. Players own designated edges of the board, and by clever placement of stones they push a neutral stone around the board. A player wins if he/she moves the neutral stone to one of his/her edges, or leaves his/her opponent unable to move.",
8282
"irensei": "Irensei is a mixture of Go and Gomoku. Get a seven in a row inside the middle 15x15 region of the Go board to win. To reduce the first player's advantage, the first player loses if they make an overline (even if it extends beyond the 15x15 region), but the second player may win by overline.",
83+
"jacynth": "A Decktet card game where opponents vye for control of the city of Jacynth. Place cards, exert influence, and control the most area to win.",
8384
"konane": "A traditional Hawaiian game that was almost completely lost to time but for the memory of a single woman. Played on a square board, each player captures by jumping over their opponent's pieces. Last to move wins.",
8485
"lielow": "A wartime tale of Machiavellian usurpership. You win if your piece lands on your opponent's king. You also win if your opponent chooses to move their king off the board.",
8586
"lifeline": "Conquer territory and capture enemy groups by cutting them off from all their allies.",
@@ -180,6 +181,7 @@
180181
"exxit": "Because the board is built out as you play in irregular shapes, the hexes are labelled numerically instead of algebraically. We chose this so that the labels wouldn't change as the map grew. Because the labels aren't particularly helpful while you play, they are not displayed on the board. Also, translations of the rules tend to omit certain nuances. This implementation conforms with the original French edition of the rules.",
181182
"garden": "To make it very clear what happened on a previous turn, each move is displayed over four separate boards. The first board shows the game after the piece was first placed. The second board shows the state after adjacent pieces were flipped. The third board shows any harvests. The fourth board is the final game state and is where you make your moves.\n\nIn our implementation, black is always the \"tome\" or tie-breaker colour. The last player to harvest black will have a `0.1` after their score.",
182183
"homeworlds": "The win condition is what's called \"Sinister Homeworlds.\" You only win by defeating the opponent to your left. If someone else does that, the game continues, but your left-hand opponent now shifts clockwise. For example, in a four-player game, if I'm South, then I win if I eliminate West. But if the North player ends up eliminating West, the game continues, but now my left-hand opponent is North.",
184+
"jacynth": "More information on the Decktet system can be found on the [official Decktet website](https://www.decktet.com). Cards in players' hands are hidden from observers and opponents.",
183185
"konane": "Several competing opening protocols exist, but the most common ruleset is described in the BGG reference and is what is implemented here.",
184186
"loa": "In the centre of the 9x9 board is a \"black hole.\". Landing on the black hole means the piece is removed from the game. Simultaneous connections are scored as a draw.",
185187
"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.",
@@ -830,6 +832,20 @@
830832
"name": "Toroidal 15x15 board"
831833
}
832834
},
835+
"jacynth": {
836+
"extended": {
837+
"name": "Extended deck",
838+
"description": "Deck includes the Excuse, the pawns, and the courts (influence tokens may not be placed on any of these cards)."
839+
},
840+
"towers": {
841+
"name": "Towers",
842+
"description": "The board starts with four cards just inside of each corner."
843+
},
844+
"oldcity": {
845+
"name": "The Old City",
846+
"description": "The board starts with six cards in a circular pattern."
847+
}
848+
},
833849
"konane": {
834850
"size-8": {
835851
"name": "Size 8 board"
@@ -1817,6 +1833,15 @@
18171833
"name": "no tiebreak"
18181834
}
18191835
},
1836+
"jacynth": {
1837+
"deck": {
1838+
"description": "The basic 36-card deck (aces through crowns)"
1839+
},
1840+
"setup": {
1841+
"name": "setup: The Razeway",
1842+
"description": "The board starts with six cards in a diagonal line."
1843+
}
1844+
},
18201845
"lielow": {
18211846
"board": {
18221847
"name": "size-8 board"
@@ -3434,6 +3459,16 @@
34343459
"KO": "Ko rule: you may not immediately recapture your opponent's single stone if they just used it to capture one of yours.",
34353460
"SELF_CAPTURE": "You may not place your stones in self-capture unless it is a winning move."
34363461
},
3462+
"jacynth": {
3463+
"ALREADY_CLAIMED": "The card at {{cell}} has already been claimed.",
3464+
"ALREADY_OWNED": "The card at {{cell}} is part of a district owned by an opponent.",
3465+
"BAD_INFLUENCE": "You may not exert influence on the card at {{cell}}. The Excuse, pawns, and courts may not be directly influenced.",
3466+
"INITIAL_INSTRUCTIONS": "Click a card in your hand to place on the board.",
3467+
"LABEL_STASH": "Player {{playerNum}}'s hand",
3468+
"NO_CARD": "The card \"{{card}}\" does not appear to be in your hand.",
3469+
"NO_INFLUENCE": "You don't have any influence left.",
3470+
"PARTIAL_PLACEMENT": "Click an empty space on the board to place your card."
3471+
},
34373472
"konane": {
34383473
"FIRST_MOVE": "Remove one of your pieces from the center 2x2 or from a corner.",
34393474
"SECOND_MOVE": "Remove one of your pieces adjacent to the piece your opponent removed.",

locales/en/apresults.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
"fnap_col": "{{player}} claimed column {{where}}.",
111111
"fnap_fnap": "{{player}} has claimed the FNAP token.",
112112
"fnap_row": "{{player}} claimed row {{where}}.",
113+
"jacynth": "{{player}} exerted influence at {{where}}.",
113114
"logger": "{{player}} claimed a protestor from the tree felled at {{where}}.",
114115
"majorities_line": "{{player}} claimed the line {{where}}.",
115116
"majorities_H": "{{player}} claimed the horizontal direction.",

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
},
6262
"dependencies": {
6363
"@abstractplay/recranks": "latest",
64-
"@abstractplay/renderer": "^1.0.0-ci-12244554392.0",
64+
"@abstractplay/renderer": "^1.0.0-ci-12323485398.0",
6565
"@turf/boolean-contains": "^6.5.0",
6666
"@turf/boolean-intersects": "^6.5.0",
6767
"@turf/boolean-point-in-polygon": "^6.5.0",

src/common/decktet/Card.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/* eslint-disable @typescript-eslint/no-unsafe-call */
2+
import { Component, ranks, suits } from "./Component";
3+
4+
type Params = {
5+
name: string;
6+
rank: Component;
7+
suits: Component[];
8+
personality?: boolean;
9+
event?: boolean;
10+
location?: boolean;
11+
};
12+
13+
export const cardSortAsc = (a: Card, b: Card): number => {
14+
if (a.rank.seq === b.rank.seq) {
15+
if (a.suits.length === b.suits.length) {
16+
for (let i = 0; i < a.suits.length; i++) {
17+
if (a.suits[i].seq !== b.suits[i].seq) {
18+
return a.suits[i].seq - b.suits[i].seq;
19+
}
20+
}
21+
return 0;
22+
} else {
23+
return a.suits.length - b.suits.length;
24+
}
25+
} else {
26+
return a.rank.seq - b.rank.seq;
27+
}
28+
}
29+
30+
export const cardSortDesc = (a: Card, b: Card): number => {
31+
if (a.rank.seq === b.rank.seq) {
32+
if (a.suits.length === b.suits.length) {
33+
for (let i = 0; i < a.suits.length; i++) {
34+
if (a.suits[i].seq !== b.suits[i].seq) {
35+
return b.suits[i].seq - a.suits[i].seq;
36+
}
37+
}
38+
return 0;
39+
} else {
40+
return b.suits.length - a.suits.length;
41+
}
42+
} else {
43+
return b.rank.seq - a.rank.seq;
44+
}
45+
}
46+
47+
export class Card {
48+
private readonly _name: string;
49+
private readonly _rank: Component;
50+
private readonly _suits: Component[];
51+
private readonly _personality: boolean = false;
52+
private readonly _event: boolean = false;
53+
private readonly _location: boolean = false;
54+
55+
constructor(params: Params) {
56+
this._name = params.name;
57+
this._rank = params.rank;
58+
this._suits = [...params.suits];
59+
if (params.personality !== undefined) {
60+
this._personality = params.personality;
61+
}
62+
if (params.event !== undefined) {
63+
this._event = params.event;
64+
}
65+
if (params.location !== undefined) {
66+
this._location = params.location;
67+
}
68+
}
69+
70+
public get name(): string {
71+
return this._name;
72+
}
73+
public get rank(): Component {
74+
return new Component(this._rank);
75+
}
76+
public get suits(): Component[] {
77+
return [...this._suits.map(s => new Component(s))];
78+
}
79+
public get personality(): boolean {
80+
return this._personality;
81+
}
82+
public get event(): boolean {
83+
return this._event;
84+
}
85+
public get location(): boolean {
86+
return this._location;
87+
}
88+
89+
public get uid(): string {
90+
return [this.rank.uid, ...this.suits.map(s => s.uid)].join("");
91+
}
92+
93+
public clone(): Card {
94+
return new Card({name: this.name, rank: this.rank, suits: [...this.suits.map(s => s.clone())], personality: this.personality, location: this.location, event: this.event});
95+
}
96+
97+
public static deserialize(card: Card|string): Card|undefined {
98+
if (typeof card === "string") {
99+
return [...cardsBasic, ...cardsExtended].find(c => c.uid === card.toUpperCase());
100+
}
101+
return new Card({name: card.name, rank: card.rank, suits: [...card.suits.map(s => s.clone())], personality: card.personality, location: card.location, event: card.event});
102+
}
103+
}
104+
105+
const [moons, suns, waves, leaves, wyrms, knots] = suits;
106+
const [zero, one, two, three, four, five, six, seven, eight, nine, pawn, court, crown] = ranks;
107+
108+
export const cardsBasic: Card[] = [
109+
new Card({name: "Ace of Moons", rank: one, suits: [moons]}),
110+
new Card({name: "Ace of Suns", rank: one, suits: [suns]}),
111+
new Card({name: "Ace of Waves", rank: one, suits: [waves]}),
112+
new Card({name: "Ace of Leaves", rank: one, suits: [leaves]}),
113+
new Card({name: "Ace of Wyrms", rank: one, suits: [wyrms]}),
114+
new Card({name: "Ace of Knots", rank: one, suits: [knots]}),
115+
new Card({name: "The Author", rank: two, suits: [moons, knots], personality: true}),
116+
new Card({name: "The Desert", rank: two, suits: [suns, wyrms], location: true}),
117+
new Card({name: "The Origin", rank: two, suits: [waves, leaves], event: true, location: true}),
118+
new Card({name: "The Painter", rank: three, suits: [suns, knots], personality: true}),
119+
new Card({name: "The Savage", rank: three, suits: [leaves, wyrms], personality: true}),
120+
new Card({name: "The Journey", rank: three, suits: [moons, waves], event: true}),
121+
new Card({name: "The Battle", rank: four, suits: [wyrms, knots], event: true}),
122+
new Card({name: "The Sailor", rank: four, suits: [waves, leaves], personality: true}),
123+
new Card({name: "The Mountain", rank: four, suits: [moons, suns], location: true}),
124+
new Card({name: "The Discovery", rank: five, suits: [suns, waves], event: true}),
125+
new Card({name: "The Soldier", rank: five, suits: [wyrms, knots], personality: true}),
126+
new Card({name: "The Forest", rank: five, suits: [moons, leaves], location: true}),
127+
new Card({name: "The Penitent", rank: six, suits: [suns, wyrms], personality: true}),
128+
new Card({name: "The Lunatic", rank: six, suits: [moons, waves], personality: true}),
129+
new Card({name: "The Market", rank: six, suits: [leaves, knots], event: true, location: true}),
130+
new Card({name: "The Castle", rank: seven, suits: [suns, knots], location: true}),
131+
new Card({name: "The Chance Meeting", rank: seven, suits: [moons, leaves], event: true}),
132+
new Card({name: "The Cave", rank: seven, suits: [waves, wyrms], location: true}),
133+
new Card({name: "The Betrayal", rank: eight, suits: [wyrms, knots], event: true}),
134+
new Card({name: "The Diplomat", rank: eight, suits: [moons, suns], personality: true}),
135+
new Card({name: "The Mill", rank: eight, suits: [waves, leaves], location: true}),
136+
new Card({name: "The Pact", rank: nine, suits: [moons, suns], event: true}),
137+
new Card({name: "The Merchant", rank: nine, suits: [leaves, knots], personality: true}),
138+
new Card({name: "The Darkness", rank: nine, suits: [waves, wyrms], location: true}),
139+
new Card({name: "The Huntress", rank: crown, suits: [moons], personality: true}),
140+
new Card({name: "The Bard", rank: crown, suits: [suns], personality: true}),
141+
new Card({name: "The Sea", rank: crown, suits: [waves], location: true}),
142+
new Card({name: "The End", rank: crown, suits: [leaves], event: true, location: true}),
143+
new Card({name: "The Calamity", rank: crown, suits: [wyrms], event: true}),
144+
new Card({name: "The Windfall", rank: crown, suits: [knots], event: true}),
145+
];
146+
147+
export const cardsExtended: Card[] = [
148+
new Card({name: "The Excuse", rank: zero, suits: []}),
149+
new Card({name: "The Watchman", rank: pawn, suits: [moons, wyrms, knots], personality: true}),
150+
new Card({name: "The Borderland", rank: pawn, suits: [waves, leaves, wyrms], location: true}),
151+
new Card({name: "The Harvest", rank: pawn, suits: [moons, suns, leaves], event: true}),
152+
new Card({name: "The Light Keeper", rank: pawn, suits: [suns, waves, knots], personality: true}),
153+
new Card({name: "The Consul", rank: court, suits: [moons, waves, knots], personality: true}),
154+
new Card({name: "The Rite", rank: court, suits: [moons, leaves, wyrms], event: true}),
155+
new Card({name: "The Window", rank: court, suits: [suns, leaves, knots], location: true}),
156+
new Card({name: "The Island", rank: court, suits: [suns, waves, wyrms], location: true}),
157+
];

src/common/decktet/Component.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
export type ComponentParams = {
2+
uid: string;
3+
seq: number;
4+
name: string;
5+
glyph?: string;
6+
};
7+
8+
export class Component {
9+
private readonly _uid: string;
10+
private readonly _seq: number;
11+
private readonly _name: string;
12+
private readonly _glyph?: string;
13+
14+
constructor(params: ComponentParams) {
15+
this._uid = params.uid;
16+
this._seq = params.seq;
17+
this._name = params.name;
18+
this._glyph = params.glyph;
19+
}
20+
21+
public get uid(): string {
22+
return this._uid;
23+
}
24+
public get seq(): number {
25+
return this._seq;
26+
}
27+
public get name(): string {
28+
return this._name;
29+
}
30+
public get glyph(): string|undefined {
31+
return this._glyph;
32+
}
33+
34+
public clone(): Component {
35+
return new Component({uid: this.uid, seq: this.seq, name: this.name, glyph: this.glyph});
36+
}
37+
38+
public static deserialize(comp: Component|string): Component|undefined {
39+
if (typeof comp === "string") {
40+
return [...suits, ...ranks].find(c => c.uid === comp);
41+
}
42+
return new Component({uid: comp.uid, seq: comp.seq, name: comp.name, glyph: comp.glyph});
43+
}
44+
45+
public toString(): string {
46+
return this.uid;
47+
}
48+
}
49+
50+
export const suits: Component[] = [
51+
new Component({uid: "M", seq: 1, name: "Moons", glyph: "decktet-moons"}),
52+
new Component({uid: "S", seq: 2, name: "Suns", glyph: "decktet-suns"}),
53+
new Component({uid: "V", seq: 3, name: "Waves", glyph: "decktet-waves"}),
54+
new Component({uid: "L", seq: 4, name: "Leaves", glyph: "decktet-leaves"}),
55+
new Component({uid: "Y", seq: 5, name: "Wyrms", glyph: "decktet-wyrms"}),
56+
new Component({uid: "K", seq: 6, name: "Knots", glyph: "decktet-knots"}),
57+
];
58+
59+
export const ranks: Component[] = [
60+
new Component({uid: "0", seq: 0, name: "Excuse", glyph: "decktet-0"}),
61+
new Component({uid: "1", seq: 1, name: "Ace"}),
62+
new Component({uid: "2", seq: 2, name: "2", glyph: "decktet-2"}),
63+
new Component({uid: "3", seq: 3, name: "3", glyph: "decktet-3"}),
64+
new Component({uid: "4", seq: 4, name: "4", glyph: "decktet-4"}),
65+
new Component({uid: "5", seq: 5, name: "5", glyph: "decktet-5"}),
66+
new Component({uid: "6", seq: 6, name: "6", glyph: "decktet-6"}),
67+
new Component({uid: "7", seq: 7, name: "7", glyph: "decktet-7"}),
68+
new Component({uid: "8", seq: 8, name: "8", glyph: "decktet-8"}),
69+
new Component({uid: "9", seq: 9, name: "9", glyph: "decktet-9"}),
70+
new Component({uid: "P", seq: 9.3, name: "Pawn", glyph: "decktet-pawn"}),
71+
new Component({uid: "T", seq: 9.6, name: "Court", glyph: "decktet-court"}),
72+
new Component({uid: "N", seq: 10, name: "Crown", glyph: "decktet-crown"}),
73+
];

0 commit comments

Comments
 (0)