feat(mobile): show the rules
A How to play screen with the same rules and examples as the web app, opened from the help screen and from the game screen's help button.
This commit is contained in:
parent
f8ce8f215f
commit
636127dae4
@ -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 }} />
|
||||
<Stack.Screen name="Help" component={HelpScreen}
|
||||
options={{ title: t("Help"), headerStyle, headerTintColor }} />
|
||||
<Stack.Screen name="Rules" component={RulesScreen}
|
||||
options={{ title: t("HowToPlay"), headerStyle, headerTintColor }} />
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
49
apps/mobile/src/components/RuleBoard.tsx
Normal file
49
apps/mobile/src/components/RuleBoard.tsx
Normal file
@ -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 (
|
||||
<View key={index} style={{
|
||||
flex: store ? 1.2 : 1, aspectRatio: store ? undefined : 1, minHeight: store ? undefined : 0,
|
||||
borderRadius: 100, backgroundColor: color, alignItems: 'center', justifyContent: 'center',
|
||||
borderWidth: index === played ? 2 : 0, borderColor: theme.playerTurnColor,
|
||||
}}>
|
||||
<Text style={{ fontSize: 11, fontWeight: '700', color: textOn(color) }}>{board[index] || ''}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', gap: 4, padding: 6, borderRadius: 12, backgroundColor: theme.boardColor }}>
|
||||
{cell(13, true)}
|
||||
<View style={{ flex: 6, gap: 4 }}>
|
||||
<View style={{ flexDirection: 'row', gap: 4 }}>{TOP_PITS.map((index) => cell(index, false))}</View>
|
||||
<View style={{ flexDirection: 'row', gap: 4 }}>{BOTTOM_PITS.map((index) => cell(index, false))}</View>
|
||||
</View>
|
||||
{cell(6, true)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// 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 <MiniBoard board={example.board} theme={theme} />;
|
||||
const outcome = getMoveOutcome(example.board, example.pitIndex);
|
||||
return (
|
||||
<View style={{ gap: 6 }}>
|
||||
<MiniBoard board={example.board} theme={theme} played={example.pitIndex} />
|
||||
<Text style={{ textAlign: 'center', color: theme.textColor }}>↓</Text>
|
||||
<MiniBoard board={outcome.board} theme={theme} landed={outcome.path[outcome.path.length - 1]} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default RuleBoard;
|
||||
@ -255,7 +255,7 @@ export function GameScreen({ navigation, route }: GameScreenProps) {
|
||||
<CircularPanel color={context.themeManager.theme?.background} children={<Text style={{ color: context.themeManager.theme.textColor }}>{isError ? t('Home') : (isGameEnded ? (!isSpectator && getBotLevel(getOpponentId()) ? t('PlayAgain') : t('NewGame')) : t('Leave'))}</Text>} />
|
||||
</Pressable>
|
||||
<Pressable onPress={() => {
|
||||
navigation.push("Help", { context })
|
||||
navigation.push("Rules", { context })
|
||||
}}>
|
||||
<SvgXml xml={helpSvg.replaceAll("customColor", textColorOnAppBar)} width="24" height="24" />
|
||||
</Pressable>
|
||||
|
||||
@ -31,6 +31,9 @@ export default function HelpScreen({ navigation, route }: HelpScreenProps) {
|
||||
alignContent: "center",
|
||||
justifyContent: "center"
|
||||
}}>
|
||||
<Pressable onPress={() => navigation.push("Rules", { context })}>
|
||||
<Text style={{ textAlign: "center", color: context.themeManager.theme.textColor }}>{t('HowToPlay')}</Text>
|
||||
</Pressable>
|
||||
<Pressable onPress={onGotoWebAppClick}>
|
||||
<Text style={{ textAlign: "center", color: context.themeManager.theme.textColor }}>{t('WebApp')}</Text>
|
||||
</Pressable>
|
||||
|
||||
26
apps/mobile/src/screens/RulesScreen.tsx
Normal file
26
apps/mobile/src/screens/RulesScreen.tsx
Normal file
@ -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 (
|
||||
<ScrollView style={{ flex: 1, backgroundColor: theme.background }} contentContainerStyle={{ padding: 16, gap: 24 }}>
|
||||
{RULES.map((rule) => (
|
||||
<View key={rule.id} style={{ gap: 8 }}>
|
||||
<Text style={{ fontSize: 18, fontWeight: '700', color: theme.textColor }}>{t(rule.titleKey)}</Text>
|
||||
{rule.textKeys.map((key) => (
|
||||
<Text key={key} style={{ color: theme.textColor, lineHeight: 20 }}>{t(key)}</Text>
|
||||
))}
|
||||
{rule.example && <RuleBoard example={rule.example} theme={theme} />}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
@ -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<RootStackParamList, 'Home'>;
|
||||
@ -16,4 +17,6 @@ export type LobyScreenProps = NativeStackScreenProps<RootStackParamList, 'Loby'>
|
||||
|
||||
export type GameScreenProps = NativeStackScreenProps<RootStackParamList, 'Game'>;
|
||||
|
||||
export type HelpScreenProps = NativeStackScreenProps<RootStackParamList, 'Help'>;
|
||||
export type HelpScreenProps = NativeStackScreenProps<RootStackParamList, 'Help'>;
|
||||
|
||||
export type RulesScreenProps = NativeStackScreenProps<RootStackParamList, 'Rules'>;
|
||||
|
||||
@ -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).
|
||||
|
||||
Loading…
Reference in New Issue
Block a user