diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx
index c0776c4..b33957b 100644
--- a/apps/mobile/src/App.tsx
+++ b/apps/mobile/src/App.tsx
@@ -15,6 +15,7 @@ import { NativeModules, I18nManager, Platform } from 'react-native'
// https://github.com/uuidjs/uuid/issues/514#issuecomment-691475020
import 'react-native-get-random-values';
import HelpScreen from './screens/HelpScreen';
+import RulesScreen from './screens/RulesScreen';
import { getColorByBrightness } from "@mancala/core";
import { server } from './const/config';
@@ -120,6 +121,8 @@ function App() {
options={{ title: t("Mancala"), headerStyle, headerTintColor }} />
+
);
diff --git a/apps/mobile/src/components/RuleBoard.tsx b/apps/mobile/src/components/RuleBoard.tsx
new file mode 100644
index 0000000..6707f0a
--- /dev/null
+++ b/apps/mobile/src/components/RuleBoard.tsx
@@ -0,0 +1,49 @@
+import * as React from 'react';
+import { FunctionComponent } from 'react';
+import { Text, View } from 'react-native';
+import { getColorByBrightness, getMoveOutcome, RuleExample, Theme } from '@mancala/core';
+
+const TOP_PITS = [12, 11, 10, 9, 8, 7];
+const BOTTOM_PITS = [0, 1, 2, 3, 4, 5];
+
+// Stone counts on a small board as the bottom player sees it; `played` and `landed` mark a move.
+const MiniBoard: FunctionComponent<{ board: number[], theme: Theme, played?: number, landed?: number }> = ({ board, theme, played, landed }) => {
+ const textOn = (color: string) => getColorByBrightness(color, theme.textColor, theme.textLightColor);
+ const cell = (index: number, store: boolean) => {
+ const color = index === landed ? theme.pitSelectedColor : theme.pitColor;
+ return (
+
+ {board[index] || ''}
+
+ );
+ };
+ return (
+
+ {cell(13, true)}
+
+ {TOP_PITS.map((index) => cell(index, false))}
+ {BOTTOM_PITS.map((index) => cell(index, false))}
+
+ {cell(6, true)}
+
+ );
+};
+
+// A rule's example: the board before the move and after it, as the engine plays it.
+const RuleBoard: FunctionComponent<{ example: RuleExample, theme: Theme }> = ({ example, theme }) => {
+ if (example.pitIndex === undefined) return ;
+ const outcome = getMoveOutcome(example.board, example.pitIndex);
+ return (
+
+
+ ↓
+
+
+ );
+};
+
+export default RuleBoard;
diff --git a/apps/mobile/src/screens/GameScreen.tsx b/apps/mobile/src/screens/GameScreen.tsx
index 404b432..c979fc6 100644
--- a/apps/mobile/src/screens/GameScreen.tsx
+++ b/apps/mobile/src/screens/GameScreen.tsx
@@ -255,7 +255,7 @@ export function GameScreen({ navigation, route }: GameScreenProps) {
{isError ? t('Home') : (isGameEnded ? (!isSpectator && getBotLevel(getOpponentId()) ? t('PlayAgain') : t('NewGame')) : t('Leave'))}} />
{
- navigation.push("Help", { context })
+ navigation.push("Rules", { context })
}}>
diff --git a/apps/mobile/src/screens/HelpScreen.tsx b/apps/mobile/src/screens/HelpScreen.tsx
index efd6997..726f19d 100644
--- a/apps/mobile/src/screens/HelpScreen.tsx
+++ b/apps/mobile/src/screens/HelpScreen.tsx
@@ -31,6 +31,9 @@ export default function HelpScreen({ navigation, route }: HelpScreenProps) {
alignContent: "center",
justifyContent: "center"
}}>
+ navigation.push("Rules", { context })}>
+ {t('HowToPlay')}
+
{t('WebApp')}
diff --git a/apps/mobile/src/screens/RulesScreen.tsx b/apps/mobile/src/screens/RulesScreen.tsx
new file mode 100644
index 0000000..c36402f
--- /dev/null
+++ b/apps/mobile/src/screens/RulesScreen.tsx
@@ -0,0 +1,26 @@
+import * as React from 'react';
+import { ScrollView, Text, View } from 'react-native';
+import { useTranslation } from 'react-i18next';
+import { RULES } from '@mancala/core';
+import { RulesScreenProps } from '../types';
+import RuleBoard from '../components/RuleBoard';
+
+// How to play: the same rules and examples as the web app (docs/specs/tutorial, R5.1).
+export default function RulesScreen({ route }: RulesScreenProps) {
+ const { context } = route.params;
+ const { t } = useTranslation();
+ const theme = context.themeManager.theme;
+ return (
+
+ {RULES.map((rule) => (
+
+ {t(rule.titleKey)}
+ {rule.textKeys.map((key) => (
+ {t(key)}
+ ))}
+ {rule.example && }
+
+ ))}
+
+ );
+}
diff --git a/apps/mobile/src/types/index.ts b/apps/mobile/src/types/index.ts
index 190597a..3e93b1b 100644
--- a/apps/mobile/src/types/index.ts
+++ b/apps/mobile/src/types/index.ts
@@ -8,6 +8,7 @@ export type RootStackParamList = {
Loby: { context: Context, botLevel?: BotLevel },
Game: { context: Context, gameId: string, userKey: string }
Help: { context: Context }
+ Rules: { context: Context }
};
export type HomeScreenProps = NativeStackScreenProps;
@@ -16,4 +17,6 @@ export type LobyScreenProps = NativeStackScreenProps
export type GameScreenProps = NativeStackScreenProps;
-export type HelpScreenProps = NativeStackScreenProps;
\ No newline at end of file
+export type HelpScreenProps = NativeStackScreenProps;
+
+export type RulesScreenProps = NativeStackScreenProps;
diff --git a/docs/specs/tutorial/tasks.md b/docs/specs/tutorial/tasks.md
index 3e45aca..e47ec30 100644
--- a/docs/specs/tutorial/tasks.md
+++ b/docs/specs/tutorial/tasks.md
@@ -11,6 +11,6 @@ Work through the tasks in order; each ends with its tests passing and a commit.
reset, scripted opponent replies.
- [x] 5. **Newcomers and practice game**: `TutorialBanner`, `markGamePlayed`, the practice game button.
- [x] 6. **Hints**: hint chips on `BoardView`, the explanation line and the "Hints" switch in bot games on `GamePage`.
-- [ ] 7. **Mobile rules**: `RulesScreen`, links from the help and game screens; type check and bundle.
+- [x] 7. **Mobile rules**: `RulesScreen`, links from the help and game screens; type check and bundle.
- [ ] 8. **End to end and docs**: the Chrome tests from `design.md`; `AGENTS.md` (specs list), `TODO.md` (out of scope
items).