diff --git a/app/game/game.styles.ts b/app/game/game.styles.ts index e2ed9d4..5e22ee0 100644 --- a/app/game/game.styles.ts +++ b/app/game/game.styles.ts @@ -4,5 +4,14 @@ export const GameStyles = StyleSheet.create({ container: { flex: 1, padding: 10 - } + }, + controls: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'space-between' + }, + mistakesCountText: { + fontWeight: 'bold' + }, + mistakesText: {} }); diff --git a/app/game/index.tsx b/app/game/index.tsx index d847603..4d646ea 100644 --- a/app/game/index.tsx +++ b/app/game/index.tsx @@ -1,14 +1,15 @@ import { useRouter } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import { useEffect } from 'react'; -import { Alert } from 'react-native'; +import { Alert, View, Text } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { AvailableValues } from '../../components/available-values/available-values'; import { BlackButton } from '../../components/black-button/black-button'; import { Field } from '../../components/field/field'; +import { MaxMistakesConstant } from '../../constants/max-mistakes.constant'; import { useAppSelector } from '../../hooks/redux.hook'; -import { appRootFieldSelector, appRootSelectedCellSelector } from '../../store/app-root/app-root.selectors'; +import { appRootFieldSelector, appRootMistakesSelector, appRootSelectedCellSelector } from '../../store/app-root/app-root.selectors'; import { hasBlankCells } from '../../utils/field/has-blank-cells.util'; import { GameStyles as styles } from './game.styles'; @@ -18,12 +19,18 @@ export default function Game() { const field = useAppSelector(appRootFieldSelector); const selectedCell = useAppSelector(appRootSelectedCellSelector); + const mistakes = useAppSelector(appRootMistakesSelector); useEffect(() => { if (!hasBlankCells(field)[0]) { router.push('winner'); } }, [field]); + useEffect(() => { + if (mistakes >= MaxMistakesConstant) { + router.push('loser'); + } + }, [mistakes]); const handleExit = () => { Alert.alert('Stop current run?', 'All progress will be lost', [ @@ -35,7 +42,12 @@ export default function Game() { return ( - + + + Mistakes: {mistakes}/{MaxMistakesConstant} + + + diff --git a/app/loser/index.tsx b/app/loser/index.tsx new file mode 100644 index 0000000..6a7f660 --- /dev/null +++ b/app/loser/index.tsx @@ -0,0 +1,17 @@ +import { StatusBar } from 'expo-status-bar'; +import { View } from 'react-native'; + +import { BlackButton } from '../../components/black-button/black-button'; +import { Header } from '../../components/header/header'; + +import { LoserStyles as styles } from './loser.styles'; + +export default function Loser() { + return ( + + +
+ + + ); +} diff --git a/app/loser/loser.styles.ts b/app/loser/loser.styles.ts new file mode 100644 index 0000000..fd716e7 --- /dev/null +++ b/app/loser/loser.styles.ts @@ -0,0 +1,9 @@ +import { StyleSheet } from 'react-native'; + +export const LoserStyles = StyleSheet.create({ + container: { + alignItems: 'center', + flex: 1, + justifyContent: 'center' + } +}); diff --git a/app/winner/index.tsx b/app/winner/index.tsx index 83ee63b..233a911 100644 --- a/app/winner/index.tsx +++ b/app/winner/index.tsx @@ -1,6 +1,8 @@ -import { Link } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; -import { Text, View } from 'react-native'; +import { View } from 'react-native'; + +import { BlackButton } from '../../components/black-button/black-button'; +import { Header } from '../../components/header/header'; import { WinnerStyles as styles } from './winner.styles'; @@ -8,10 +10,8 @@ export default function Winner() { return ( - Winner-winner, chicken dinner! - - Play again - +
+ ); } diff --git a/app/winner/winner.styles.ts b/app/winner/winner.styles.ts index 651a961..9b72f31 100644 --- a/app/winner/winner.styles.ts +++ b/app/winner/winner.styles.ts @@ -1,20 +1,9 @@ import { StyleSheet } from 'react-native'; export const WinnerStyles = StyleSheet.create({ - button: { - backgroundColor: '#000', - color: 'white', - paddingHorizontal: 20, - paddingVertical: 10 - }, container: { alignItems: 'center', flex: 1, justifyContent: 'center' - }, - header: { - fontFamily: 'Inter_700Bold', - fontSize: 22, - marginBottom: 10 } }); diff --git a/components/available-values/available-values.styles.ts b/components/available-values/available-values.styles.ts index c8335f1..5b4ae51 100644 --- a/components/available-values/available-values.styles.ts +++ b/components/available-values/available-values.styles.ts @@ -1,12 +1,12 @@ import { StyleSheet } from 'react-native'; -import { cellSize } from '../../constants/dimensions.contant'; +import { CellSizeConstant } from '../../constants/dimensions.contant'; export const AvailableValuesStyles = StyleSheet.create({ valueWrapper: { borderWidth: 1, - height: cellSize, - width: cellSize + height: CellSizeConstant, + width: CellSizeConstant }, wrapper: { flex: 1, diff --git a/components/black-button/black-button.tsx b/components/black-button/black-button.tsx index 0409f17..bf8773f 100644 --- a/components/black-button/black-button.tsx +++ b/components/black-button/black-button.tsx @@ -1,18 +1,39 @@ -import { Pressable, type PressableProps, type StyleProp, Text, type TextProps, type ViewStyle } from 'react-native'; +import { isNotEmptyString } from '@rnw-community/shared'; +import { useRouter } from 'expo-router'; +import { + type GestureResponderEvent, + Pressable, + type PressableProps, + type StyleProp, + Text, + type TextProps, + type ViewStyle +} from 'react-native'; import { BlackButtonStyles as styles } from './black-button.styles'; interface Props extends PressableProps { text: string; styleText?: TextProps['style']; + href?: string; } -export const BlackButton = ({ text, style, styleText, ...props }: Props) => { +export const BlackButton = ({ text, style, href, styleText, onPress, ...props }: Props) => { + const router = useRouter(); + const wrapperStyles = [styles.button, style] as StyleProp; const textStyles = [styles.buttonText, styleText]; + const handlePress = (event: GestureResponderEvent) => { + onPress?.(event); + + if (isNotEmptyString(href)) { + router.push(href); + } + }; + return ( - + {text} ); diff --git a/components/cell/cell.styles.ts b/components/cell/cell.styles.ts index 8525090..5f56d89 100644 --- a/components/cell/cell.styles.ts +++ b/components/cell/cell.styles.ts @@ -1,6 +1,6 @@ import { StyleSheet } from 'react-native'; -import { cellSize } from '../../constants/dimensions.contant'; +import { CellSizeConstant } from '../../constants/dimensions.contant'; // TODO: Add style theming support export const CellStyles = StyleSheet.create({ @@ -12,9 +12,9 @@ export const CellStyles = StyleSheet.create({ borderTopWidth: 1, flex: 1, fontFamily: 'Inter_500Medium', - height: cellSize, + height: CellSizeConstant, justifyContent: 'center', - width: cellSize + width: CellSizeConstant }, cellActive: { backgroundColor: 'green' diff --git a/components/header/header.styles.ts b/components/header/header.styles.ts new file mode 100644 index 0000000..e7f868d --- /dev/null +++ b/components/header/header.styles.ts @@ -0,0 +1,9 @@ +import { StyleSheet } from 'react-native'; + +export const HeaderStyles = StyleSheet.create({ + container: { + fontFamily: 'Inter_700Bold', + fontSize: 22, + marginBottom: 20 + } +}); diff --git a/components/header/header.tsx b/components/header/header.tsx new file mode 100644 index 0000000..51236c1 --- /dev/null +++ b/components/header/header.tsx @@ -0,0 +1,15 @@ +import { Text, type TextProps } from 'react-native'; + +import { HeaderStyles as styles } from './header.styles'; + +interface Props extends TextProps { + text: string; +} + +export const Header = ({ text, ...props }: Props) => { + return ( + + {text} + + ); +}; diff --git a/constants/dimensions.contant.ts b/constants/dimensions.contant.ts index e9bad51..2ebddff 100644 --- a/constants/dimensions.contant.ts +++ b/constants/dimensions.contant.ts @@ -1 +1 @@ -export const cellSize = 50; +export const CellSizeConstant = 50; diff --git a/constants/max-mistakes.constant.ts b/constants/max-mistakes.constant.ts new file mode 100644 index 0000000..a38ebea --- /dev/null +++ b/constants/max-mistakes.constant.ts @@ -0,0 +1 @@ +export const MaxMistakesConstant = 3; diff --git a/store/app-root/actions/app-root-select-value.action.ts b/store/app-root/actions/app-root-select-value.action.ts index 4800ba9..bee29db 100644 --- a/store/app-root/actions/app-root-select-value.action.ts +++ b/store/app-root/actions/app-root-select-value.action.ts @@ -1,16 +1,17 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { isDefined } from '@rnw-community/shared'; +import { BlankCellValueContant } from '../../../constants/blank-cell-value.contant'; import { isCorrectCell } from '../../../utils/field/is-correct-cell.util'; import { type AppDispatch, type RootState } from '../../create-store'; -import { appRootSetValueAction } from '../app-root.actions'; +import { appRootMadeAMistake, appRootSetValueAction } from '../app-root.actions'; export const appRootSelectValueAction = createAsyncThunk( 'appRoot/selectValue', async (value: number, thunkAPI) => { const state = thunkAPI.getState().appRoot; - if (isDefined(state.selectedCell)) { + if (isDefined(state.selectedCell) && state.selectedCell.value === BlankCellValueContant) { const newCell = { ...state.selectedCell, value }; if (isCorrectCell(newCell, state.gameField)) { thunkAPI.dispatch(appRootSetValueAction(newCell)); @@ -21,10 +22,11 @@ export const appRootSelectValueAction = createAsyncThunk sta export const appRootSelectedCellSelector = createSelector(appRootSelector, state => state.selectedCell); export const appRootSelectedValueSelector = createSelector(appRootSelector, state => state.selectedValue); export const appRootAvailableValuesSelector = createSelector(appRootSelector, state => getAvailableFieldValues(state.gameField)); +export const appRootMistakesSelector = createSelector(appRootSelector, state => state.mistakes); diff --git a/store/app-root/app-root.slice.ts b/store/app-root/app-root.slice.ts index 5edd6ed..e27bd7c 100644 --- a/store/app-root/app-root.slice.ts +++ b/store/app-root/app-root.slice.ts @@ -16,6 +16,7 @@ export const appRootSlice = createSlice({ state.filledField = createField(9); state.gameField = createGameField(state.filledField, blankCellsCount); + state.mistakes = 0; }, selectCell: (state, action: PayloadAction) => { state.selectedCell = action.payload; @@ -24,6 +25,9 @@ export const appRootSlice = createSlice({ const cell = action.payload; state.selectedCell = state.gameField[cell.y][cell.x]; state.gameField[cell.y][cell.x].value = cell.value; + }, + madeAMistake: state => { + state.mistakes++; } } }); diff --git a/store/app-root/app-root.state.ts b/store/app-root/app-root.state.ts index 9928ef7..1f88ada 100644 --- a/store/app-root/app-root.state.ts +++ b/store/app-root/app-root.state.ts @@ -5,9 +5,11 @@ export interface AppRootState { gameField: CellInterface[][]; selectedCell?: CellInterface; selectedValue?: number; + mistakes: number; } export const appRootInitialState: AppRootState = { filledField: [], - gameField: [] + gameField: [], + mistakes: 0 };