@masonf539/egyptian-war 0.0.0-stage → 1.0.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 +27 -0
- package/README.md +66 -3
- package/dist/definition.d.ts +10 -0
- package/dist/definition.js +135 -0
- package/dist/engine.d.ts +94 -0
- package/dist/engine.js +703 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +34 -0
- package/dist/session.d.ts +2 -0
- package/dist/session.js +612 -0
- package/dist/slapArbitration.d.ts +11 -0
- package/dist/slapArbitration.js +63 -0
- package/docs/design.md +126 -0
- package/package.json +52 -6
- package/public/assets/audio/effects/card-play.mp3 +0 -0
- package/public/assets/audio/effects/game-win.mp3 +0 -0
- package/public/assets/audio/effects/pile-win.mp3 +0 -0
- package/public/assets/audio/effects/slap.mp3 +0 -0
- package/public/assets/audio/music/egyptian-war.mp3 +0 -0
- package/public/assets/game-previews/poster.webp +0 -0
- package/public/assets/game-previews/preview.mp4 +0 -0
- package/public/client.js +1193 -0
- package/public/register.js +28 -0
- package/public/style.css +1505 -0
- package/public/template.html +152 -0
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,66 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
This
|
|
1
|
+
# Egyptian War for GameHub
|
|
2
|
+
|
|
3
|
+
This directory contains the server-authoritative Egyptian War game plugin for
|
|
4
|
+
GameHub and is exported to the `egyptian-war-standalone` branch for release.
|
|
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 instead makes them miss the next card's slap
|
|
57
|
+
opportunity. A valid slap can still let an eliminated player re-enter when
|
|
58
|
+
their cooldown is clear.
|
|
59
|
+
|
|
60
|
+
## Independent repository preparation
|
|
61
|
+
|
|
62
|
+
The package directory includes its own README, license, TypeScript settings,
|
|
63
|
+
tests, public assets, and package metadata. Its only GameHub code dependency is
|
|
64
|
+
the exact `@masonf539/game-sdk` version declared in `package.json`. A standalone
|
|
65
|
+
lockfile will be generated after that SDK version is published so it records
|
|
66
|
+
the registry artifact rather than the 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 skip the next card's slap opportunity after a false slap. 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 must miss the next card's slap opportunity before trying again.",
|
|
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, but will miss the next card's slap opportunity before being able to try 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, but must miss the next card's slap opportunity.",
|
|
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
|
+
};
|
package/dist/engine.d.ts
ADDED
|
@@ -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" | "blocked";
|
|
85
|
+
/** Start a cooldown that lasts 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 one-card slap cooldown.
|
|
89
|
+
* The next card's opportunity is consumed the first time they try to slap on it.
|
|
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;
|