+ Hi, to make a game of {gameId}, you
+ {Game.meta.players === 'single' ? `can use ,${gameId} create!` : 'need to ask a staff member!'}
+
+
+ If you want to learn how to play, check{' '}
+ {' '}
+ out!
+
+
+ );
},
categories: ['game'],
children: {
+ help: {
+ name: 'help',
+ aliases: ['h'],
+ help: 'Shows this help message.',
+ syntax: 'CMD',
+ flags: { allowPMs: true, noDisplay: true },
+ async run({ command, run }) {
+ return run(`help ${command[0]}`);
+ },
+ },
create: {
name: 'create',
aliases: ['new', 'n'],
@@ -223,6 +322,17 @@ export const command: PSCommand[] = Object.entries(Games).map(([_gameId, Game]):
}
},
},
+ htp: {
+ name: 'htp',
+ aliases: ['howtoplay'],
+ help: 'Shows the how-to-play for this game.',
+ flags: { allowPMs: true },
+ syntax: 'CMD',
+ async run({ message }) {
+ const htpData = Game.meta.htp;
+ return message.replyHTML();
+ },
+ },
...conditionalCommand(
Game.meta.autostart === false,
{
diff --git a/src/ps/games/azul/meta.ts b/src/ps/games/azul/meta.ts
index 55369368..905b2f78 100644
--- a/src/ps/games/azul/meta.ts
+++ b/src/ps/games/azul/meta.ts
@@ -24,6 +24,50 @@ export const meta: Meta = {
pokeTimer: fromHumanTime('30 sec'),
timer: fromHumanTime('45 sec'),
+ htp: {
+ goal: 'Have the highest score when any player completes a wall row.',
+ sections: [
+ {
+ title: 'Setup',
+ lines: [
+ '- The game is played over several rounds, each consisting of two phases: drafting tiles and then scoring walls.',
+ '- Game begins with 4 tiles placed in each factory.',
+ ],
+ },
+ {
+ title: 'Drafting Phase',
+ lines: [''],
+ subsections: [
+ {
+ title: 'Picking tiles',
+ lines: [
+ '- Choose a tile from a factory to take all tiles of the same type from it.',
+ '- The remaining tiles from that factory are moved to the waste.',
+ '- The first to pick from the waste must also take the waste marker, giving them a penalty but also the first turn in the next round.',
+ ],
+ },
+ {
+ title: 'Placing tiles',
+ lines: [
+ '- You can only place the tiles in one of the rows on your player board.',
+ '- You must place all tiles of the same color in a row.',
+ '- Any overflow of tiles will be moved to the penalty line.',
+ ],
+ },
+ ],
+ },
+ {
+ title: 'Scoring',
+ lines: [
+ '- Happens when there are no tiles left in factories/waste.',
+ '- For each filled row, place one tile on the wall in the corresponding row and type.',
+ '- Each wall tile scores contiguous horizontal and vertical lines through it.',
+ '- After the game, each filled row gives 2 points, every column 7, every type 10.',
+ '- The game ends when a row is filled.',
+ ],
+ },
+ ],
+ },
// UGO-CODE
ugo: {
cap: 12,
diff --git a/src/ps/games/battleship/meta.ts b/src/ps/games/battleship/meta.ts
index bb2d1b6e..e78337ae 100644
--- a/src/ps/games/battleship/meta.ts
+++ b/src/ps/games/battleship/meta.ts
@@ -19,6 +19,29 @@ export const meta: Meta = {
pokeTimer: fromHumanTime('30 sec'),
timer: fromHumanTime('1 min'),
+ htp: {
+ goal: "Sink all of your opponent's ships before they sink yours!",
+ sections: [
+ {
+ title: 'Setup',
+ lines: [
+ '- 5 ships each to be placed on a 10x10 grid.',
+ '- They are: (C)arrier (5), (B)attleship (4), (D)estroyer (3), (S)ubmarine (3), and (P)atrol (2).',
+ '- Ships can be placed horizontally or vertically, but not diagonally.',
+ '- Ships cannot overlap or go out of bounds.',
+ ],
+ },
+ {
+ title: 'Gameplay',
+ lines: [
+ "- Players take turns shooting at coordinates on their opponent's board.",
+ '- If a ship is hit, it is marked as hit.',
+ '- If all ships on one side are sunk, that player loses, and the game ends.',
+ ],
+ },
+ ],
+ },
+
// UGO-CODE
ugo: {
cap: 12,
diff --git a/src/ps/games/chess/meta.ts b/src/ps/games/chess/meta.ts
index adfb0b21..05a47dc4 100644
--- a/src/ps/games/chess/meta.ts
+++ b/src/ps/games/chess/meta.ts
@@ -23,6 +23,10 @@ export const meta: Meta = {
themes: CHESS_THEMES,
defaultTheme,
+ htp: {
+ goal: 'Checkmate the opponent\'s king!',
+ sections: [{ title: 'Check this guide out for basics!', lines: ['https://www.chess.com/learn-how-to-play-chess'] }]
+ },
// UGO-CODE
ugo: {
cap: 12,
diff --git a/src/ps/games/connectfour/meta.ts b/src/ps/games/connectfour/meta.ts
index 8e4571e8..5a9e70fd 100644
--- a/src/ps/games/connectfour/meta.ts
+++ b/src/ps/games/connectfour/meta.ts
@@ -19,6 +19,18 @@ export const meta: Meta = {
pokeTimer: fromHumanTime('30 sec'),
timer: fromHumanTime('1 min'),
+ htp: {
+ goal: 'Get four of your colored discs in a row—horizontally, vertically, or diagonally.',
+ sections: [
+ {
+ title: 'Gameplay',
+ lines: [
+ '- Players take turns dropping one disc into one of the seven columns.',
+ '- Discs stack on top of each other in the selected column.',
+ ],
+ },
+ ],
+ },
// UGO-CODE
ugo: {
cap: 12,
diff --git a/src/ps/games/lightsout/meta.ts b/src/ps/games/lightsout/meta.ts
index ae20f450..09597a7f 100644
--- a/src/ps/games/lightsout/meta.ts
+++ b/src/ps/games/lightsout/meta.ts
@@ -9,6 +9,20 @@ export const meta: Meta = {
abbr: 'lo',
players: 'single',
+ htp: {
+ goal: 'Turn all the lights off! Bonus points if you can beat PartBot :p',
+ sections: [
+ {
+ title: 'How to Play',
+ lines: [
+ '- The game is played on a 5×5 grid of lights.',
+ '- When you click on a light, it toggles (turns on or off), including adjacent lights (up, down, left, right).',
+ '- A helpful guide: https://www.logicgamesonline.com/lightsout/tutorial.html',
+ ],
+ },
+ ],
+ },
+
// UGO-CODE
ugo: null,
};
diff --git a/src/ps/games/mastermind/meta.ts b/src/ps/games/mastermind/meta.ts
index 681a6575..4a77a2ad 100644
--- a/src/ps/games/mastermind/meta.ts
+++ b/src/ps/games/mastermind/meta.ts
@@ -9,6 +9,31 @@ export const meta: Meta = {
abbr: 'mm',
players: 'single',
+ htp: {
+ goal: 'Guess the 4 digit code before your guesses run out!',
+ sections: [
+ {
+ title: 'How to Play',
+ lines: [
+ '- Numbers go from 0-7, and they can repeat.',
+ '- Red pin - correct number correct position',
+ '- White pin - correct number wrong position',
+ '- No pin - incorrect number',
+ "- Reds/Whites don't specify that the number in the corresponding position is correct.",
+ ],
+ },
+ {
+ title: 'Example',
+ lines: [
+ "Let's assume the code is 0167...",
+ '- Guess: 1234. Result: White. (1 is the only correct number but in the wrong position.)',
+ '- Guess: 2567. Result: Red-Red. (6,7 are correct numbers and correct positions.)',
+ '- Guess: 6723. Result: White-White. (6,7 are correct numbers but in the wrong positions.)',
+ '- Guess: 2345. Result: Nothing. (All numbers are incorrect.)',
+ ],
+ },
+ ],
+ },
// UGO-CODE
- ugo: null
+ ugo: null,
};
diff --git a/src/ps/games/othello/meta.ts b/src/ps/games/othello/meta.ts
index 49a64bff..0228d8a9 100644
--- a/src/ps/games/othello/meta.ts
+++ b/src/ps/games/othello/meta.ts
@@ -18,6 +18,35 @@ export const meta: Meta = {
pokeTimer: fromHumanTime('30 sec'),
timer: fromHumanTime('1 min'),
+ htp: {
+ goal: 'Have more discs of your color than your opponent when the game ends.',
+ sections: [
+ {
+ title: 'Initial board setup',
+ lines: ['- 2 white and 2 black discs start in the center.'],
+ images: [{ path: 'othello/setup.png', alt: 'Initial Othello board setup', width: 170, height: 178 }],
+ },
+ {
+ title: 'Gameplay',
+ lines: [
+ '- Black moves first, then white.',
+ '- A valid move must outflank at least one opponent disc vertically, horizontally, or diagonally.',
+ '- Outflank: your disc borders opponent row(s) at both ends.',
+ '- If you have no valid move, your turn is skipped.',
+ '- Game ends when neither player can move or the board is full.',
+ ],
+ },
+ {
+ title: 'Valid move example',
+ lines: ['- Black outflanks White vertically, horizontally, and diagonally (not all three required).'],
+ images: [{ path: 'othello/valid-move.gif', alt: 'Valid Othello move example', width: 170 }],
+ },
+ {
+ title: 'Win conditions',
+ lines: ['- Most discs wins.'],
+ },
+ ],
+ },
// UGO-CODE
ugo: {
cap: 12,
diff --git a/src/ps/games/scrabble/meta.ts b/src/ps/games/scrabble/meta.ts
index d939ea61..6021e9c8 100644
--- a/src/ps/games/scrabble/meta.ts
+++ b/src/ps/games/scrabble/meta.ts
@@ -24,6 +24,53 @@ export const meta: Meta = {
pokeTimer: fromHumanTime('1 min'),
timer: fromHumanTime('5 min'),
+ htp: {
+ goal: 'Get the most points by placing letters on the board to form words.',
+ sections: [
+ {
+ title: 'Setup',
+ lines: [
+ '- Each player gets 7 letters (A-Z and blanks, which can represent any letter)',
+ '- The first word must cover the center (star) square',
+ ],
+ },
+ {
+ title: 'Gameplay',
+ lines: [
+ '- Players take turn using letters to form words vertically or horizontally',
+ '- Every new word must connect to at least one letter already on the board',
+ '- All words formed by your play must be valid Scrabble words',
+ '- After turn end, draw new tiles to bring your hand back up to 7 tiles',
+ '- You may also choose to swap letters with the bag instead of making a word, or pass the turn',
+ ],
+ },
+ {
+ title: 'Scores',
+ lines: [
+ '- Points are based on letter values + board multipliers',
+ '- Playing all 7 letters in one turn gives bonus 50 points',
+ ],
+ subsections: [
+ {
+ title: 'Board multipliers:',
+ lines: [
+ '- Double letter score (light blue): Double the value of one letter',
+ '- Triple letter score (dark blue): Triple the value of one letter',
+ '- Double word score (pink): Double the score of the entire word',
+ '- Triple word score (red): Triple the score of the entire word',
+ ],
+ },
+ ],
+ },
+ {
+ title: 'Win conditions',
+ lines: [
+ '- Game ends when all tiles are drawn and one player empties their rack, or when no player can make a move',
+ '- Highest score wins',
+ ],
+ },
+ ],
+ },
// UGO-CODE
ugo: {
cap: 6,
diff --git a/src/ps/games/snakesladders/meta.ts b/src/ps/games/snakesladders/meta.ts
index 4cfdc11a..7a0da47f 100644
--- a/src/ps/games/snakesladders/meta.ts
+++ b/src/ps/games/snakesladders/meta.ts
@@ -17,6 +17,20 @@ export const meta: Meta = {
pokeTimer: fromHumanTime('30 sec'),
timer: fromHumanTime('45 sec'),
+ htp: {
+ goal: 'Be the first to reach 100 to win.',
+ sections: [
+ {
+ title: 'Gameplay',
+ lines: [
+ '- Each player takes turns rolling a six-sided die to move their token along the board.',
+ '- Players move their token forward by the number of spaces indicated by the die roll.',
+ '- If a player lands on a snake, they move their token to the tail of the snake.',
+ '- If a player lands on a ladder, they move their token to the top of the ladder.',
+ ],
+ },
+ ],
+ },
// UGO-CODE
ugo: {
cap: 12,
diff --git a/src/ps/games/splendor/htpContent.tsx b/src/ps/games/splendor/htpContent.tsx
new file mode 100644
index 00000000..37fb3fe3
--- /dev/null
+++ b/src/ps/games/splendor/htpContent.tsx
@@ -0,0 +1,9 @@
+import { Small } from '@/ps/games/render';
+import metadata from '@/ps/games/splendor/metadata.json';
+import { PokemonCard } from '@/ps/games/splendor/render';
+
+export const swadloonExample = (
+
+
+
+);
diff --git a/src/ps/games/splendor/meta.ts b/src/ps/games/splendor/meta.ts
index d5c10fbb..8ffe3e2c 100644
--- a/src/ps/games/splendor/meta.ts
+++ b/src/ps/games/splendor/meta.ts
@@ -1,3 +1,4 @@
+import { swadloonExample } from '@/ps/games/splendor/htpContent';
import { GamesList } from '@/ps/games/types';
import { fromHumanTime } from '@/utils/humanTime';
@@ -15,6 +16,64 @@ export const meta: Meta = {
pokeTimer: fromHumanTime('1 min'),
timer: fromHumanTime('2 min'),
+ htp: {
+ goal: 'Be the first to get 15 points.',
+ sections: [
+ {
+ title: 'Cards',
+ lines: [
+ 'Each Card consists of 3 components:',
+ '- Cost (bottom-left): The amount of Gems needed to buy it.',
+ '- Type (top-right): Each Card acts as a permanent token of that type.',
+ '- Point (top-left): The value each Card is worth.',
+ ],
+ content: swadloonExample,
+ },
+ {
+ title: 'Gameplay',
+ lines: ['On your turn choose 1 action:'],
+ subsections: [
+ {
+ title: 'Pick up Gems',
+ lines: [
+ '- Take 3 different OR 2 of the same type (only if >=4 of that type available).',
+ '- Max 10 Gems can be held at a time.',
+ '- If >10, choose which to return.',
+ ],
+ },
+ {
+ title: 'Buy a Card',
+ lines: ['- Pay Gems equal to the cost to make it yours.', '- No game limit on number of Cards buyable.'],
+ },
+ {
+ title: 'Reserve a Card',
+ lines: [
+ '- Take a card so only you can buy it.',
+ '- Also gain a Dragon gem (if possible).',
+ '- Can reserve up to 3 cards only.',
+ ],
+ },
+ ],
+ },
+ {
+ title: 'Trainers',
+ lines: [
+ 'Each Trainer card consists of 2 components:',
+ '- Point (top-right): The value each Trainer is worth',
+ '- Requirement (bottom-left): Requires having Cards (not Gems) of that specific type.',
+ '- No payment needed: Trainers are gained automatically when requirements are fulfilled.',
+ ],
+ },
+ {
+ title: 'Win conditions',
+ lines: [
+ '- Once a player reaches 15 points, everyone after them in turn order gets one last turn.',
+ '- If points are tied, the one with fewer Cards wins.',
+ '- If still tied, Audiino comes and bullies all winners.',
+ ],
+ },
+ ],
+ },
// UGO-CODE
ugo: {
cap: 8,
diff --git a/src/ps/games/types.ts b/src/ps/games/types.ts
index 08f4b74c..2f3afe25 100644
--- a/src/ps/games/types.ts
+++ b/src/ps/games/types.ts
@@ -1,6 +1,28 @@
import type { TranslatedText } from '@/i18n/types';
import type { ModData, ModEnum } from '@/ps/games/mods';
import type { Satisfies } from '@/types/common';
+import type { ReactElement } from 'react';
+
+export interface HTPImage {
+ /** Path under `/static/guides/`, e.g. `othello/setup.png`. */
+ path: string;
+ alt?: string;
+ width?: number;
+ height?: number;
+}
+
+export interface HTPDropdown {
+ title: string;
+ lines?: string[];
+ images?: HTPImage[];
+ content?: ReactElement;
+ subsections?: HTPDropdown[];
+}
+
+export interface GameHTPData {
+ goal: string;
+ sections: HTPDropdown[];
+}
export type Theme> = Partial>> = {
id: string;
@@ -33,6 +55,9 @@ export type Meta = Readonly<
/** Enables `offerdraw`: pending offer expires after 1 minute or when any player makes a move. */
canOfferDraws?: boolean;
+ /** How to play instructions. */
+ htp: GameHTPData;
+
// UGO-CODE
/**
* Metadata for automatic UGO points.
diff --git a/src/web/static/guides/othello/setup.png b/src/web/static/guides/othello/setup.png
new file mode 100644
index 00000000..fbd51967
Binary files /dev/null and b/src/web/static/guides/othello/setup.png differ
diff --git a/src/web/static/guides/othello/valid-move.gif b/src/web/static/guides/othello/valid-move.gif
new file mode 100644
index 00000000..e12b2452
Binary files /dev/null and b/src/web/static/guides/othello/valid-move.gif differ