Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local

# typescript
*.tsbuildinfo

# generated native folders
/ios
/android

# Log files
expo.log
expo_output.log
98 changes: 98 additions & 0 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useState } from 'react';
import { StyleSheet, View, SafeAreaView } from 'react-native';
import SetupScreen from './screens/SetupScreen';
import TransitionScreen from './screens/TransitionScreen';
import GuessingScreen from './screens/GuessingScreen';
import GameOverScreen from './screens/GameOverScreen';
import { checkGuess } from './utils/gameLogic';

type GamePhase = 'SETUP' | 'TRANSITION' | 'GUESSING' | 'GAME_OVER';

interface Guess {
id: number;
guess: number[];
wellPlaced: number;
misplaced: number;
}

const MAX_ATTEMPTS = 10;

export default function App() {
const [gamePhase, setGamePhase] = useState<GamePhase>('SETUP');
const [secretCode, setSecretCode] = useState<number[]>([]);
const [guesses, setGuesses] = useState<Guess[]>([]);

const handleCodeSet = (code: number[]) => {
setSecretCode(code);
setGamePhase('TRANSITION');
};

const handleStartGuessing = () => {
setGamePhase('GUESSING');
};

const handleSubmitGuess = (guess: number[]) => {
const result = checkGuess(guess, secretCode);
const newGuess: Guess = {
id: guesses.length + 1,
guess,
...result,
};
const newGuesses = [...guesses, newGuess];
setGuesses(newGuesses);

if (result.wellPlaced === 4 || newGuesses.length === MAX_ATTEMPTS) {
setGamePhase('GAME_OVER');
}
};

const handlePlayAgain = () => {
setGamePhase('SETUP');
setSecretCode([]);
setGuesses([]);
};

const renderScreen = () => {
switch (gamePhase) {
case 'SETUP':
return <SetupScreen onCodeSet={handleCodeSet} />;
case 'TRANSITION':
return <TransitionScreen onStartGuessing={handleStartGuessing} />;
case 'GUESSING':
return (
<GuessingScreen
guesses={guesses}
maxAttempts={MAX_ATTEMPTS}
onSubmitGuess={handleSubmitGuess}
/>
);
case 'GAME_OVER':
const isVictory = guesses[guesses.length - 1]?.wellPlaced === 4;
return (
<GameOverScreen
isVictory={isVictory}
secretCode={secretCode}
onPlayAgain={handlePlayAgain}
/>
);
default:
return null;
}
};

return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>{renderScreen()}</View>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f7f9fc',
},
content: {
flex: 1,
},
});
59 changes: 57 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,57 @@
# codebreaker-ui
First MVP of codebreaker, a candidate testing challenge for a job as full stack developer.
# Code Breaker MVP

This is a React Native (Expo) implementation of the classic "Code Breaker" game. It's a 2-player, pass-and-play game where one player sets a secret code and the other tries to guess it within a limited number of attempts.

## Features

- **2-Player, Pass-and-Play**: Designed for two players to play on a single device.
- **Secret Code**: A 4-digit code using numbers from 1 to 9. Duplicates are allowed.
- **Limited Attempts**: The guesser has 10 attempts to crack the code.
- **Detailed Feedback**: For each guess, the game provides feedback on the number of "Well Placed" and "Misplaced" digits.
- **State-Driven UI**: The game flow is managed by a central state machine, ensuring a clean and predictable user experience.
- **Cross-Platform**: Built with Expo, allowing it to run on web, Android, and iOS.

## Project Structure

The project is organized into the following directories:

- `/assets`: Contains static assets like images and fonts.
- `/components`: Reusable React Native components used across different screens.
- `/screens`: The main screens of the application, each corresponding to a specific game phase.
- `/utils`: Utility functions, including the core game logic.

## Component Usage

### `App.tsx`

This is the main component of the application. It manages the game's state, including the `gamePhase`, `secretCode`, and `guesses`. It's responsible for rendering the correct screen based on the current `gamePhase`.

### Screens

- **`SetupScreen.tsx`**: The initial screen where the Codemaker sets the 4-digit secret code.
- **`TransitionScreen.tsx`**: A simple screen to hide the secret code while the device is passed to the Guesser.
- **`GuessingScreen.tsx`**: The main game screen where the Guesser submits their guesses and views the history of their attempts.
- **`GameOverScreen.tsx`**: The final screen that displays whether the Guesser won or lost, reveals the secret code, and provides an option to play again.

### Components

- **`GuessInput.tsx`**: A reusable component that provides a 4-slot input for the code and a number pad for entering digits. It's used in both the `SetupScreen` and `GuessingScreen`.
- **`GuessHistory.tsx`**: A component that displays a list of the Guesser's previous attempts and the feedback for each guess.

## Installation

To get started with the project, clone the repository and install the dependencies:

```bash
npm install
```

## Running the Application

You can run the application on web, Android, or iOS using the following commands:

- **Web**: `npm run web`
- **Android**: `npm run android`
- **iOS**: `npm run ios`

The application will start in development mode with hot-reloading enabled.
30 changes: 30 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"expo": {
"name": "CodeBreakerMVP",
"slug": "CodeBreakerMVP",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Binary file added assets/adaptive-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/splash-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
78 changes: 78 additions & 0 deletions components/GuessHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import { View, Text, FlatList, StyleSheet } from 'react-native';

interface Guess {
id: number;
guess: number[];
wellPlaced: number;
misplaced: number;
}

interface GuessHistoryProps {
guesses: Guess[];
}

const GuessHistory: React.FC<GuessHistoryProps> = ({ guesses }) => {
if (guesses.length === 0) {
return (
<View style={styles.emptyContainer}>
<Text>Make your first guess!</Text>
</View>
);
}

return (
<FlatList
data={guesses}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<View style={styles.guessItem}>
<Text style={styles.guessText}>Guess #{item.id}: {item.guess.join(' ')}</Text>
<Text style={styles.resultText}>
Well Placed: {item.wellPlaced}, Misplaced: {item.misplaced}
</Text>
</View>
)}
/>
);
};

const styles = StyleSheet.create({
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
emptyText: {
fontSize: 18,
color: '#999',
},
list: {
width: '100%',
},
guessItem: {
backgroundColor: '#fff',
borderRadius: 8,
padding: 15,
marginVertical: 5,
marginHorizontal: 10,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 1,
elevation: 2,
},
guessText: {
fontSize: 16,
color: '#333',
},
resultText: {
fontSize: 14,
color: '#666',
},
});

export default GuessHistory;
Loading