@masonf539/egyptian-war 0.0.0-stage → 1.0.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Egyptian War License
2
+
3
+ Copyright (c) 2026 Mason Fong
4
+
5
+ Required Notice: Copyright (c) 2026 Mason Fong
6
+ Required Notice: Egyptian War source code is licensed under the PolyForm Noncommercial License 1.0.0.
7
+
8
+ The Egyptian War software, including its source code, browser code, styles,
9
+ templates, tests, documentation, rules engine, and networking implementation,
10
+ is licensed under the PolyForm Noncommercial License 1.0.0.
11
+
12
+ You may use, copy, modify, and distribute the Egyptian War software only for
13
+ purposes permitted by that license. Commercial use requires a separate written
14
+ license from the copyright holder.
15
+
16
+ The complete PolyForm Noncommercial License 1.0.0 is available at:
17
+
18
+ https://polyformproject.org/licenses/noncommercial/1.0.0
19
+
20
+ The creative assets located in public/assets are part of the Egyptian War software
21
+ and are licensed under the PolyForm Noncommercial License 1.0.0, like the rest of this package.
22
+
23
+ Third-party dependencies and separately identified third-party materials remain
24
+ governed by their respective licenses.
25
+
26
+ THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OR CONDITION, TO THE EXTENT
27
+ PERMITTED BY LAW.
package/README.md CHANGED
@@ -1,3 +1,68 @@
1
- # Temporary Holding Version
1
+ # Egyptian War for GameHub
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ This repository contains the server-authoritative Egyptian War game plugin
4
+ for GameHub.
5
+
6
+ Egyptian War is designed to be installed and run by GameHub through the
7
+ `@masonf539/game-sdk` contracts. It is independently versioned, but it is not a
8
+ standalone web application and depends on GameHub for rooms, networking, the
9
+ lobby, shared settings, and browser hosting.
10
+
11
+ The package owns its rules, state engine, slap arbitration, tests, browser
12
+ presentation, game-specific audio, preview media, and design documentation.
13
+ GameHub supplies rooms, members, spectators, transport, and shared services.
14
+
15
+ `src/session.ts` owns the authoritative runtime lifecycle, including turn
16
+ timers, slap collection, pause/disconnect recovery, animations, member
17
+ removal, and completion. `public/client.js` owns the browser controller and
18
+ registers it with GameHub's generic client host.
19
+
20
+ Game-specific music, effects, and picker previews live under `public/assets`
21
+ and are served by GameHub from `/games/egyptian-war`.
22
+ The package-owned `public/register.js` registers those sounds with GameHub's
23
+ shared audio service.
24
+ The game board lives in `public/template.html`, and its presentation lives in
25
+ `public/style.css`. GameHub injects the fragment into a package-specific root;
26
+ the browser controller scopes its element lookups to that root.
27
+ During active play, the stylesheet fixes the board to the available viewport,
28
+ accounts for mobile safe areas, and keeps its compact Pause/connection row
29
+ below GameHub's Settings shortcut. Full Screen and confirmed Exit/Leave
30
+ actions remain platform-owned controls in GameHub's reusable Game tab. The
31
+ shortcut is mounted inside the active game viewport, while the Settings tabs
32
+ move into one horizontal row at medium and small widths.
33
+ Egyptian War declares `play-card` and `slap` input actions for GameHub's
34
+ package-aware Keybinds tab. Their defaults are E and Space respectively, and
35
+ the game subscribes through the shared input service so remapping remains a
36
+ personal GameHub preference.
37
+ The package's public state also states whether the current player may legally
38
+ play a card, keeping the action disabled during the final slap window after a
39
+ failed challenge. Its numeric countdown stays fixed at the right edge inside
40
+ the full-width timer track while the yellow fill passes beneath it, and the
41
+ complete timer scales with the action controls on narrow screens.
42
+
43
+ The package consumes an exact `@masonf539/game-sdk` release. Once both packages
44
+ are published, GameHub installs an exact Egyptian War version and enables it
45
+ through `gamehub.config.json`. GameHub's `npm run verify:packages` command
46
+ currently verifies that both packed artifacts work without monorepo source.
47
+
48
+ ## Slap rules and penalties
49
+
50
+ Slap patterns are validated by the server and configured in the lobby. The
51
+ "Three in a Row" pattern checks the latest three cards for an ascending or
52
+ descending rank sequence, including Ace-low and Ace-high sequences. The
53
+ "Tens" pattern checks only two directly adjacent number cards that total ten;
54
+ an Ace counts as one. An active player who false-slaps forfeits the configured
55
+ number of cards, up to the number they hold. An eliminated player has no cards
56
+ to forfeit, so a false slap prevents them from slapping the unchanged pile
57
+ again. The cooldown clears when the next card is played, allowing that card to
58
+ be slapped and the eliminated player to re-enter immediately.
59
+ Invalid reentry attempts use the same visible one-hand slap animation as other
60
+ false slaps; additional attempts during the cooldown do not create extra hands.
61
+
62
+ ## Installation preparation
63
+
64
+ The repository includes its own README, license, TypeScript settings, tests,
65
+ public assets, package metadata, and ignore rules. Its only GameHub code
66
+ dependency is the exact `@masonf539/game-sdk` version declared in `package.json`.
67
+ A lockfile will be generated after that SDK version is published so it records
68
+ the registry artifact rather than the former monorepo workspace link.
@@ -0,0 +1,10 @@
1
+ import { type GameDefinition, type GamePluginManifest } from "@masonf539/game-sdk";
2
+ export type CardSuit = "clubs" | "diamonds" | "hearts" | "spades";
3
+ export type CardRank = "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "10" | "jack" | "queen" | "king" | "ace" | "joker";
4
+ export type Card = {
5
+ id: string;
6
+ suit: CardSuit | null;
7
+ rank: CardRank;
8
+ };
9
+ export declare const egyptianWar: GameDefinition;
10
+ export declare const egyptianWarManifest: GamePluginManifest;
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.egyptianWarManifest = exports.egyptianWar = void 0;
4
+ const game_sdk_1 = require("@masonf539/game-sdk");
5
+ exports.egyptianWar = {
6
+ id: "egyptian-war",
7
+ name: "Egyptian War",
8
+ description: "Play cards into a central pile, survive face-card challenges, and race to slap enabled patterns. False slaps carry penalties, and eliminated players must wait for another card before slapping again. The last player holding all the cards wins.",
9
+ isPlayable: true,
10
+ chatEnabled: true,
11
+ minPlayers: 2,
12
+ maxPlayers: 6,
13
+ preview: {
14
+ videoPath: "/games/egyptian-war/assets/game-previews/preview.mp4",
15
+ posterPath: "/games/egyptian-war/assets/game-previews/poster.webp"
16
+ },
17
+ keybinds: [
18
+ {
19
+ id: "play-card",
20
+ label: "Play a card",
21
+ description: "Play the top card when it is your turn.",
22
+ defaultKey: "KeyE"
23
+ },
24
+ {
25
+ id: "slap",
26
+ label: "Slap",
27
+ description: "Attempt to slap the central pile.",
28
+ defaultKey: "Space"
29
+ }
30
+ ],
31
+ rules: [
32
+ "The server shuffles the deck, deals every card face down, and randomly selects the first player.",
33
+ "Players cannot view or shuffle their decks.",
34
+ "Each player has the configured number of seconds to take their turn. If time expires, the server plays that player's top card automatically.",
35
+ "On a turn, a player flips the top card of their deck onto the central pile.",
36
+ "Play proceeds clockwise until someone plays a face card (Jack, Queen, or King), an Ace, or a Joker.",
37
+ "The next player must respond with another face card, Ace, or Joker. A Jack allows 1 attempt, a Queen 2 attempts, a King 3 attempts, an Ace 4 attempts, and a Joker 5 attempts.",
38
+ "If the challenged player reveals another face card, Ace, or Joker, the challenge passes to the next player with the new number of attempts.",
39
+ "If the challenged player uses every attempt without revealing a face card, Ace, or Joker, the player who most recently played a challenge card wins the pile unless the final card creates a valid slap combination.",
40
+ "When the final failed challenge card creates a valid slap combination, players get a short window of about four seconds to slap; if nobody slaps, the most recent challenge card player wins the pile.",
41
+ "If the challenged player runs out of cards before completing their attempts, the next player with cards inherits the challenge with the remaining attempts.",
42
+ "A player who runs out of cards is skipped until they reenter by winning a valid slap. An eliminated player who false-slaps cannot slap the unchanged pile again, but may slap as soon as the next card is played.",
43
+ "A player who wins the pile places all of its cards at the bottom of their deck without shuffling and begins the next pile.",
44
+ "When a valid slap combination appears, the first player to slap wins the central pile.",
45
+ "Slapping without a valid combination forfeits the configured number of cards face down to the bottom of the pile, or all remaining cards if fewer remain. Penalty cards do not create slap combinations. A player with no cards forfeits no cards for a false slap and must wait for another card before trying again.",
46
+ "When only one player still has cards, that player wins unless the current face-up pile has a valid slap that allows an eliminated player to reenter.",
47
+ "If several players slap at nearly the same moment, the pile goes to one of them by weighted chance that favors the earlier slap. The others forfeit nothing.",
48
+ "The winner is the player who collects every card."
49
+ ],
50
+ settings: [
51
+ {
52
+ type: "number",
53
+ key: "deckCount",
54
+ label: "Number of Decks",
55
+ description: "Choose whether the game uses one, two, or three complete decks of cards.",
56
+ defaultValue: 1,
57
+ options: [1, 2, 3],
58
+ unit: "deck"
59
+ },
60
+ {
61
+ key: "includeJokers",
62
+ label: "Include Jokers",
63
+ description: "Add two Jokers per deck. Playing a Joker starts a five-attempt challenge and makes the pile slappable.",
64
+ defaultValue: true
65
+ },
66
+ {
67
+ key: "allowDoubles",
68
+ label: "Doubles",
69
+ description: "Allow a slap when two consecutive cards have the same rank.",
70
+ defaultValue: true
71
+ },
72
+ {
73
+ key: "allowSandwiches",
74
+ label: "Sandwiches",
75
+ description: "Allow a slap when matching ranks have exactly one card between them.",
76
+ defaultValue: true
77
+ },
78
+ {
79
+ key: "allowThreeInARow",
80
+ label: "Three in a Row",
81
+ description: "Allow a slap when three cards form an ascending or descending sequence.",
82
+ defaultValue: true
83
+ },
84
+ {
85
+ key: "allowTopBottom",
86
+ label: "Top Bottom",
87
+ description: "Allow a slap when the newest card matches the first card in the pile.",
88
+ defaultValue: false
89
+ },
90
+ {
91
+ key: "allowTens",
92
+ label: "Tens",
93
+ description: "Allow a slap when the two most recent cards are adjacent number cards that total ten. An Ace counts as one.",
94
+ defaultValue: false
95
+ },
96
+ {
97
+ key: "allowMarriage",
98
+ label: "Marriage",
99
+ description: "Allow a slap when a King and Queen are played consecutively in either order.",
100
+ defaultValue: false
101
+ },
102
+ {
103
+ type: "number",
104
+ key: "falseSlapPenaltyCards",
105
+ label: "False Slap Penalty",
106
+ description: "Cards forfeited to the bottom of the pile for an invalid slap. An eliminated player forfeits no cards and must wait for another card before slapping again.",
107
+ defaultValue: 2,
108
+ options: [1, 2, 3],
109
+ unit: "card"
110
+ },
111
+ {
112
+ type: "number",
113
+ control: "range",
114
+ key: "turnTimerSeconds",
115
+ label: "Player Timer",
116
+ description: "Seconds each player has to play a card before the server plays their top card automatically.",
117
+ defaultValue: 15,
118
+ min: 5,
119
+ max: 120,
120
+ step: 1,
121
+ unit: "second"
122
+ }
123
+ ]
124
+ };
125
+ exports.egyptianWarManifest = {
126
+ apiVersion: game_sdk_1.gameHubGameApiVersion,
127
+ packageVersion: "1.0.0",
128
+ definition: exports.egyptianWar,
129
+ client: {
130
+ delivery: "embedded",
131
+ markupPath: "template.html",
132
+ entryPaths: ["register.js", "client.js"],
133
+ stylePaths: ["style.css"]
134
+ }
135
+ };
@@ -0,0 +1,94 @@
1
+ import type { Card } from "./definition.js";
2
+ export type EgyptianWarSettings = {
3
+ deckCount: number;
4
+ includeJokers: boolean;
5
+ allowDoubles: boolean;
6
+ allowSandwiches: boolean;
7
+ allowThreeInARow: boolean;
8
+ allowTopBottom: boolean;
9
+ allowTens: boolean;
10
+ allowMarriage: boolean;
11
+ falseSlapPenaltyCards: number;
12
+ };
13
+ export type EgyptianWarPlayerInput = {
14
+ id: string;
15
+ name: string;
16
+ avatar: string;
17
+ };
18
+ export type EgyptianWarPlayerState = EgyptianWarPlayerInput & {
19
+ cards: Card[];
20
+ isEliminated: boolean;
21
+ /** Card-play count at which this eliminated player may slap again. */
22
+ slapCooldownUntilCardPlayCount?: number | null;
23
+ };
24
+ export type EgyptianWarState = {
25
+ players: EgyptianWarPlayerState[];
26
+ pile: Card[];
27
+ deferredCards: Card[];
28
+ penaltyPileCardCount: number;
29
+ pileVersion: number;
30
+ /** Total cards played from players' hands during this game. */
31
+ cardsPlayedCount?: number;
32
+ currentPlayerIndex: number;
33
+ settings: EgyptianWarSettings;
34
+ challenge: {
35
+ challengerId: string;
36
+ responderId: string;
37
+ attemptsRemaining: number;
38
+ } | null;
39
+ pendingPileWinnerId: string | null;
40
+ totalCardCount: number;
41
+ winnerId: string | null;
42
+ status: "playing" | "finished";
43
+ activityMessage: string;
44
+ };
45
+ export type PublicEgyptianWarState = {
46
+ status: EgyptianWarState["status"];
47
+ currentPlayerId: string | null;
48
+ canPlayCard: boolean;
49
+ winnerId: string | null;
50
+ pileCardCount: number;
51
+ hasFaceUpCards: boolean;
52
+ topCard: Card | null;
53
+ recentCards: Card[];
54
+ pileVersion: number;
55
+ isPaused: boolean;
56
+ pauseMessage: string | null;
57
+ isAnimating: boolean;
58
+ turnTimerSeconds: number;
59
+ turnTimeRemainingMs: number | null;
60
+ challenge: EgyptianWarState["challenge"];
61
+ activityMessage: string;
62
+ players: Array<{
63
+ id: string;
64
+ name: string;
65
+ avatar: string;
66
+ cardCount: number;
67
+ isConnected: boolean;
68
+ isEliminated: boolean;
69
+ isCurrentPlayer: boolean;
70
+ }>;
71
+ };
72
+ export type EgyptianWarAction = "play-card" | "slap";
73
+ export type RandomInteger = (maxExclusive: number) => number;
74
+ export declare class EgyptianWarRuleError extends Error {
75
+ constructor(message: string);
76
+ }
77
+ export declare function createDeck(includeJokers: boolean, deckCount?: number): Card[];
78
+ export declare function shuffleDeck(cards: readonly Card[], randomInteger?: RandomInteger): Card[];
79
+ export declare function createEgyptianWarState(playerInputs: readonly EgyptianWarPlayerInput[], settings: EgyptianWarSettings, randomInteger?: RandomInteger): EgyptianWarState;
80
+ export type EgyptianWarDeparture = "removed" | "left";
81
+ export declare function removeEgyptianWarPlayer(state: EgyptianWarState, playerId: string, departure?: EgyptianWarDeparture): string;
82
+ export declare function isEgyptianWarPileSlappable(pile: readonly Card[], settings: EgyptianWarSettings): boolean;
83
+ export declare function isEgyptianWarStateSlappable(state: Pick<EgyptianWarState, "pile" | "penaltyPileCardCount" | "settings">): boolean;
84
+ export type EgyptianWarSlapCooldownStatus = "none" | "waiting";
85
+ /** Prevent repeated free slaps until one more card has been played. */
86
+ export declare function startEgyptianWarSlapCooldown(state: EgyptianWarState, playerId: string): void;
87
+ /**
88
+ * Returns the status of an eliminated player's slap cooldown. Once another
89
+ * card has been played, the cooldown clears and that new card may be slapped.
90
+ */
91
+ export declare function checkEgyptianWarSlapCooldown(state: EgyptianWarState, playerId: string): EgyptianWarSlapCooldownStatus;
92
+ export declare function applyEgyptianWarAction(state: EgyptianWarState, playerId: string, action: EgyptianWarAction): string;
93
+ export declare function resolveEgyptianWarTurnTimeout(state: EgyptianWarState): string;
94
+ export declare function createPublicEgyptianWarState(state: EgyptianWarState, isPaused?: boolean, isAnimating?: boolean, turnTimerSeconds?: number, turnTimeRemainingMs?: number | null, pauseMessage?: string | null, connectedPlayerIds?: ReadonlySet<string>): PublicEgyptianWarState;