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
|
// https://github.com/uuidjs/uuid/issues/514#issuecomment-691475020
|
||||||
import 'react-native-get-random-values';
|
import 'react-native-get-random-values';
|
||||||
import HelpScreen from './screens/HelpScreen';
|
import HelpScreen from './screens/HelpScreen';
|
||||||
|
import RulesScreen from './screens/RulesScreen';
|
||||||
import { getColorByBrightness } from "@mancala/core";
|
import { getColorByBrightness } from "@mancala/core";
|
||||||
import { server } from './const/config';
|
import { server } from './const/config';
|
||||||
|
|
||||||
@ -120,6 +121,8 @@ function App() {
|
|||||||
options={{ title: t("Mancala"), headerStyle, headerTintColor }} />
|
options={{ title: t("Mancala"), headerStyle, headerTintColor }} />
|
||||||
<Stack.Screen name="Help" component={HelpScreen}
|
<Stack.Screen name="Help" component={HelpScreen}
|
||||||
options={{ title: t("Help"), headerStyle, headerTintColor }} />
|
options={{ title: t("Help"), headerStyle, headerTintColor }} />
|
||||||
|
<Stack.Screen name="Rules" component={RulesScreen}
|
||||||
|
options={{ title: t("HowToPlay"), headerStyle, headerTintColor }} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
</NavigationContainer>
|
</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>} />
|
<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>
|
||||||
<Pressable onPress={() => {
|
<Pressable onPress={() => {
|
||||||
navigation.push("Help", { context })
|
navigation.push("Rules", { context })
|
||||||
}}>
|
}}>
|
||||||
<SvgXml xml={helpSvg.replaceAll("customColor", textColorOnAppBar)} width="24" height="24" />
|
<SvgXml xml={helpSvg.replaceAll("customColor", textColorOnAppBar)} width="24" height="24" />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
@ -31,6 +31,9 @@ export default function HelpScreen({ navigation, route }: HelpScreenProps) {
|
|||||||
alignContent: "center",
|
alignContent: "center",
|
||||||
justifyContent: "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}>
|
<Pressable onPress={onGotoWebAppClick}>
|
||||||
<Text style={{ textAlign: "center", color: context.themeManager.theme.textColor }}>{t('WebApp')}</Text>
|
<Text style={{ textAlign: "center", color: context.themeManager.theme.textColor }}>{t('WebApp')}</Text>
|
||||||
</Pressable>
|
</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 },
|
Loby: { context: Context, botLevel?: BotLevel },
|
||||||
Game: { context: Context, gameId: string, userKey: string }
|
Game: { context: Context, gameId: string, userKey: string }
|
||||||
Help: { context: Context }
|
Help: { context: Context }
|
||||||
|
Rules: { context: Context }
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'>;
|
export type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'>;
|
||||||
@ -17,3 +18,5 @@ export type LobyScreenProps = NativeStackScreenProps<RootStackParamList, 'Loby'>
|
|||||||
export type GameScreenProps = NativeStackScreenProps<RootStackParamList, 'Game'>;
|
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.
|
reset, scripted opponent replies.
|
||||||
- [x] 5. **Newcomers and practice game**: `TutorialBanner`, `markGamePlayed`, the practice game button.
|
- [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`.
|
- [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
|
- [ ] 8. **End to end and docs**: the Chrome tests from `design.md`; `AGENTS.md` (specs list), `TODO.md` (out of scope
|
||||||
items).
|
items).
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user