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).