Release: Quarkus backend, Keycloak login, game history, clock, bot, tutorial and rematch #3

Merged
jhalitaksoy merged 88 commits from develop into main 2026-10-17 03:23:38 +03:00
7 changed files with 87 additions and 3 deletions
Showing only changes of commit 636127dae4 - Show all commits

View File

@ -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>
); );

View 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;

View File

@ -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>

View File

@ -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>

View 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>
);
}

View File

@ -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'>;

View File

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