Release: Quarkus backend, Keycloak login, game history, clock, bot, tutorial and rematch #3
@ -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