create-pika-minigame 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/bin/cli.js +189 -0
- package/package.json +28 -0
- package/templates/README.md +196 -0
- package/templates/app.json.template +29 -0
- package/templates/babel.config.js +7 -0
- package/templates/dev/DevApp.tsx +47 -0
- package/templates/dev/MockHost.ts +131 -0
- package/templates/dev/index.js +9 -0
- package/templates/index.js +8 -0
- package/templates/package.json.template +41 -0
- package/templates/rspack.config.mjs +81 -0
- package/templates/src/App.tsx +215 -0
- package/templates/src/components/Button.tsx +117 -0
- package/templates/src/components/Card.tsx +86 -0
- package/templates/src/components/Spinner.tsx +68 -0
- package/templates/src/components/TopBar.tsx +109 -0
- package/templates/src/components/index.ts +11 -0
- package/templates/src/tokens.ts +109 -0
- package/templates/src/types.ts +53 -0
- package/templates/tsconfig.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# create-pika-minigame
|
|
2
|
+
|
|
3
|
+
CLI tool to scaffold Pika mini-game projects with standalone development environment.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-pika-minigame my-awesome-game
|
|
9
|
+
cd my-awesome-game
|
|
10
|
+
yarn install
|
|
11
|
+
yarn dev
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What's Included
|
|
15
|
+
|
|
16
|
+
- **Design System Tokens** - Colors, spacing, typography aligned with Pika brand
|
|
17
|
+
- **UI Components** - Button, Card, TopBar, Spinner
|
|
18
|
+
- **Mock Host** - Test native features without the main app
|
|
19
|
+
- **Re.Pack Config** - Module Federation setup for production
|
|
20
|
+
- **TypeScript** - Full type safety
|
|
21
|
+
|
|
22
|
+
## Publishing
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd tools/create-pika-minigame
|
|
26
|
+
npm publish
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Development
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Test the CLI locally
|
|
33
|
+
node bin/cli.js test-game
|
|
34
|
+
|
|
35
|
+
# Or link globally
|
|
36
|
+
npm link
|
|
37
|
+
create-pika-minigame test-game
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## License
|
|
41
|
+
|
|
42
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// Colors for terminal output
|
|
7
|
+
const colors = {
|
|
8
|
+
reset: '\x1b[0m',
|
|
9
|
+
bright: '\x1b[1m',
|
|
10
|
+
green: '\x1b[32m',
|
|
11
|
+
cyan: '\x1b[36m',
|
|
12
|
+
yellow: '\x1b[33m',
|
|
13
|
+
red: '\x1b[31m',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const log = {
|
|
17
|
+
info: (msg) => console.log(`${colors.cyan}info${colors.reset} ${msg}`),
|
|
18
|
+
success: (msg) => console.log(`${colors.green}success${colors.reset} ${msg}`),
|
|
19
|
+
warn: (msg) => console.log(`${colors.yellow}warn${colors.reset} ${msg}`),
|
|
20
|
+
error: (msg) => console.log(`${colors.red}error${colors.reset} ${msg}`),
|
|
21
|
+
step: (num, msg) => console.log(`\n${colors.bright}[${num}]${colors.reset} ${msg}`),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function showHelp() {
|
|
25
|
+
console.log(`
|
|
26
|
+
${colors.bright}create-pika-minigame${colors.reset} - Scaffold a new Pika mini-game project
|
|
27
|
+
|
|
28
|
+
${colors.bright}Usage:${colors.reset}
|
|
29
|
+
npx create-pika-minigame <project-name> [options]
|
|
30
|
+
|
|
31
|
+
${colors.bright}Options:${colors.reset}
|
|
32
|
+
--help, -h Show this help message
|
|
33
|
+
--version, -v Show version
|
|
34
|
+
|
|
35
|
+
${colors.bright}Examples:${colors.reset}
|
|
36
|
+
npx create-pika-minigame my-awesome-game
|
|
37
|
+
npx create-pika-minigame pronunciation-game
|
|
38
|
+
npx create-pika-minigame quiz-game
|
|
39
|
+
|
|
40
|
+
${colors.bright}After creation:${colors.reset}
|
|
41
|
+
cd <project-name>
|
|
42
|
+
yarn install
|
|
43
|
+
yarn dev # Run with mock host (standalone)
|
|
44
|
+
yarn build:ios # Build for production
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function showVersion() {
|
|
49
|
+
const pkg = require('../package.json');
|
|
50
|
+
console.log(pkg.version);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toKebabCase(str) {
|
|
54
|
+
return str
|
|
55
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
56
|
+
.replace(/[\s_]+/g, '-')
|
|
57
|
+
.toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function toPascalCase(str) {
|
|
61
|
+
return str
|
|
62
|
+
.split(/[-_\s]+/)
|
|
63
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
64
|
+
.join('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function toSnakeCase(str) {
|
|
68
|
+
return str
|
|
69
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
70
|
+
.replace(/[\s-]+/g, '_')
|
|
71
|
+
.toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function copyDir(src, dest, replacements = {}) {
|
|
75
|
+
if (!fs.existsSync(dest)) {
|
|
76
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
80
|
+
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const srcPath = path.join(src, entry.name);
|
|
83
|
+
let destName = entry.name.replace('.template', '');
|
|
84
|
+
const destPath = path.join(dest, destName);
|
|
85
|
+
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
copyDir(srcPath, destPath, replacements);
|
|
88
|
+
} else {
|
|
89
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
90
|
+
|
|
91
|
+
// Apply replacements
|
|
92
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
93
|
+
content = content.replace(new RegExp(`{{${key}}}`, 'g'), value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fs.writeFileSync(destPath, content);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function main() {
|
|
102
|
+
const args = process.argv.slice(2);
|
|
103
|
+
|
|
104
|
+
// Handle flags
|
|
105
|
+
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
106
|
+
showHelp();
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
111
|
+
showVersion();
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const projectName = args[0];
|
|
116
|
+
|
|
117
|
+
if (!projectName) {
|
|
118
|
+
log.error('Please specify a project name:');
|
|
119
|
+
console.log(' npx create-pika-minigame my-awesome-game');
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Validate project name
|
|
124
|
+
if (!/^[a-zA-Z][a-zA-Z0-9-_]*$/.test(projectName)) {
|
|
125
|
+
log.error('Project name must start with a letter and contain only letters, numbers, hyphens, and underscores.');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
130
|
+
|
|
131
|
+
if (fs.existsSync(targetDir)) {
|
|
132
|
+
log.error(`Directory "${projectName}" already exists.`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log(`
|
|
137
|
+
${colors.bright}${colors.cyan}
|
|
138
|
+
____ _ _ __ __ _ _ ____
|
|
139
|
+
| _ \\(_) | ____ _| \\/ (_)_ __ (_) / ___| __ _ _ __ ___ ___
|
|
140
|
+
| |_) | | |/ / _\` | |\\/| | | '_ \\| | | | _ / _\` | '_ \` _ \\ / _ \\
|
|
141
|
+
| __/| | < (_| | | | | | | | | | | |_| | (_| | | | | | | __/
|
|
142
|
+
|_| |_|_|\\_\\__,_|_| |_|_|_| |_|_| \\____|\\__,_|_| |_| |_|\\___|
|
|
143
|
+
${colors.reset}
|
|
144
|
+
`);
|
|
145
|
+
|
|
146
|
+
log.info(`Creating a new Pika mini-game in ${colors.bright}${targetDir}${colors.reset}`);
|
|
147
|
+
|
|
148
|
+
const kebabName = toKebabCase(projectName);
|
|
149
|
+
const pascalName = toPascalCase(projectName);
|
|
150
|
+
const snakeName = toSnakeCase(projectName);
|
|
151
|
+
|
|
152
|
+
const replacements = {
|
|
153
|
+
PROJECT_NAME: projectName,
|
|
154
|
+
PROJECT_NAME_KEBAB: kebabName,
|
|
155
|
+
PROJECT_NAME_PASCAL: pascalName,
|
|
156
|
+
PROJECT_NAME_SNAKE: snakeName,
|
|
157
|
+
YEAR: new Date().getFullYear().toString(),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
log.step(1, 'Creating project structure...');
|
|
161
|
+
|
|
162
|
+
const templatesDir = path.join(__dirname, '..', 'templates');
|
|
163
|
+
copyDir(templatesDir, targetDir, replacements);
|
|
164
|
+
|
|
165
|
+
log.step(2, 'Project created successfully!');
|
|
166
|
+
|
|
167
|
+
console.log(`
|
|
168
|
+
${colors.green}Success!${colors.reset} Created ${colors.bright}${projectName}${colors.reset} at ${targetDir}
|
|
169
|
+
|
|
170
|
+
${colors.bright}Next steps:${colors.reset}
|
|
171
|
+
|
|
172
|
+
${colors.cyan}cd ${projectName}${colors.reset}
|
|
173
|
+
${colors.cyan}yarn install${colors.reset}
|
|
174
|
+
|
|
175
|
+
${colors.bright}Available commands:${colors.reset}
|
|
176
|
+
|
|
177
|
+
${colors.cyan}yarn dev${colors.reset} Start development with mock host (standalone)
|
|
178
|
+
${colors.cyan}yarn build:ios${colors.reset} Build for iOS production
|
|
179
|
+
${colors.cyan}yarn build:android${colors.reset} Build for Android production
|
|
180
|
+
${colors.cyan}yarn deploy${colors.reset} Deploy to CDN (requires setup)
|
|
181
|
+
|
|
182
|
+
${colors.bright}Documentation:${colors.reset}
|
|
183
|
+
See README.md in your project for detailed instructions.
|
|
184
|
+
|
|
185
|
+
Happy coding! ${colors.yellow}🎮${colors.reset}
|
|
186
|
+
`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-pika-minigame",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI to scaffold Pika mini-game projects with standalone dev environment",
|
|
5
|
+
"author": "Pika Team",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-pika-minigame": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"templates"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"pika",
|
|
16
|
+
"mini-game",
|
|
17
|
+
"react-native",
|
|
18
|
+
"module-federation",
|
|
19
|
+
"repack"
|
|
20
|
+
],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/pika/create-pika-minigame"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# {{PROJECT_NAME_PASCAL}}
|
|
2
|
+
|
|
3
|
+
A Pika mini-game built with React Native and Module Federation.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install dependencies
|
|
9
|
+
yarn install
|
|
10
|
+
|
|
11
|
+
# Run in development mode (standalone with mock host)
|
|
12
|
+
yarn dev
|
|
13
|
+
|
|
14
|
+
# Build for production
|
|
15
|
+
yarn build:ios
|
|
16
|
+
yarn build:android
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Project Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
{{PROJECT_NAME_KEBAB}}/
|
|
23
|
+
├── src/ # Your game source code
|
|
24
|
+
│ ├── App.tsx # Main entry component
|
|
25
|
+
│ ├── tokens.ts # Design system tokens
|
|
26
|
+
│ ├── types.ts # TypeScript types
|
|
27
|
+
│ └── components/ # Reusable UI components
|
|
28
|
+
│ ├── Button.tsx
|
|
29
|
+
│ ├── Card.tsx
|
|
30
|
+
│ ├── TopBar.tsx
|
|
31
|
+
│ └── Spinner.tsx
|
|
32
|
+
│
|
|
33
|
+
├── dev/ # Standalone development
|
|
34
|
+
│ ├── index.js # Dev entry point
|
|
35
|
+
│ ├── DevApp.tsx # Wrapper with mock host
|
|
36
|
+
│ └── MockHost.ts # Mock native capabilities
|
|
37
|
+
│
|
|
38
|
+
├── index.js # Production entry (Module Federation)
|
|
39
|
+
├── rspack.config.mjs # Re.Pack bundler config
|
|
40
|
+
├── app.json # Expo config (for dev mode)
|
|
41
|
+
└── package.json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
### Running Standalone
|
|
47
|
+
|
|
48
|
+
The `yarn dev` command runs your mini-game as a standalone Expo app with a mocked host bridge. This lets you:
|
|
49
|
+
|
|
50
|
+
- Develop without the main Pika app
|
|
51
|
+
- Test UI and game logic independently
|
|
52
|
+
- See console logs for mock API calls
|
|
53
|
+
|
|
54
|
+
### Mock Host
|
|
55
|
+
|
|
56
|
+
Edit `dev/MockHost.ts` to customize mock behavior:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// Return specific scores for testing
|
|
60
|
+
stopAndCheck: async (word) => ({
|
|
61
|
+
totalScore: 95, // Always high score
|
|
62
|
+
words: [{ word, score: 95, letters: [...] }]
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Simulate errors
|
|
66
|
+
startRecording: async () => {
|
|
67
|
+
throw new Error('Microphone permission denied');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Add delays for loading state testing
|
|
71
|
+
await delay(3000);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Design System
|
|
75
|
+
|
|
76
|
+
### Tokens
|
|
77
|
+
|
|
78
|
+
Use tokens from `src/tokens.ts` for consistent styling:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { color, semantic, space, radius, typography } from './tokens';
|
|
82
|
+
|
|
83
|
+
const styles = StyleSheet.create({
|
|
84
|
+
container: {
|
|
85
|
+
backgroundColor: semantic.bg, // Dark background
|
|
86
|
+
padding: space[4], // 16px
|
|
87
|
+
borderRadius: radius.lg, // 16px
|
|
88
|
+
},
|
|
89
|
+
title: {
|
|
90
|
+
color: semantic.text, // White
|
|
91
|
+
fontSize: typography.title1.size, // 28
|
|
92
|
+
fontWeight: typography.title1.fontWeight,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Components
|
|
98
|
+
|
|
99
|
+
Pre-built components in `src/components/`:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { Button, Card, TopBar, Spinner } from './components';
|
|
103
|
+
|
|
104
|
+
// Button variants: primary, secondary, danger, ghost
|
|
105
|
+
<Button label="Start" variant="primary" size="lg" onPress={...} />
|
|
106
|
+
|
|
107
|
+
// Card with press interaction
|
|
108
|
+
<Card onPress={...}>
|
|
109
|
+
<Text>Tap me</Text>
|
|
110
|
+
</Card>
|
|
111
|
+
|
|
112
|
+
// TopBar with back/close buttons
|
|
113
|
+
<TopBar left="back" onLeftPress={goBack} title="Game" />
|
|
114
|
+
|
|
115
|
+
// Loading spinner
|
|
116
|
+
<Spinner size="lg" color={color.cyan[400]} />
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Host Bridge
|
|
120
|
+
|
|
121
|
+
The `host` prop provides native capabilities from the main app:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
interface HostBridge {
|
|
125
|
+
pronunciation: {
|
|
126
|
+
startRecording: () => Promise<void>;
|
|
127
|
+
stopAndCheck: (word: string) => Promise<PronunciationResult>;
|
|
128
|
+
cancel: () => Promise<void>;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Usage in your game:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const App: React.FC<GameProps> = ({ host }) => {
|
|
137
|
+
const startRecording = async () => {
|
|
138
|
+
await host?.pronunciation.startRecording();
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const checkPronunciation = async (word: string) => {
|
|
142
|
+
const result = await host?.pronunciation.stopAndCheck(word);
|
|
143
|
+
console.log('Score:', result.totalScore);
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Building for Production
|
|
149
|
+
|
|
150
|
+
### 1. Build the bundle
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# iOS
|
|
154
|
+
yarn build:ios
|
|
155
|
+
|
|
156
|
+
# Android
|
|
157
|
+
yarn build:android
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 2. Serve locally for testing
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
yarn serve:ios # Serves on http://localhost:9000
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 3. Deploy to CDN
|
|
167
|
+
|
|
168
|
+
Edit `package.json` to add your deployment command:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"scripts": {
|
|
173
|
+
"deploy:ios": "MF_PUBLIC_PATH=https://your-cdn.com/games/{{PROJECT_NAME_KEBAB}}/ yarn build:ios && <your-deploy-command>"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Integration with Pika App
|
|
179
|
+
|
|
180
|
+
Once deployed, the main Pika app can load your mini-game:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// In the host app
|
|
184
|
+
const game = await loadRemote('{{PROJECT_NAME_SNAKE}}/App');
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Tips
|
|
188
|
+
|
|
189
|
+
1. **Test on real devices** - Mock host simulates behavior, but test on device for real performance
|
|
190
|
+
2. **Keep it fast** - Mini-games should load instantly; minimize bundle size
|
|
191
|
+
3. **Handle errors** - Always check if `host` is defined before using
|
|
192
|
+
4. **Dark theme** - Default tokens are dark-themed for immersive game feel
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT © {{YEAR}} Pika Team
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expo": {
|
|
3
|
+
"name": "{{PROJECT_NAME_PASCAL}}",
|
|
4
|
+
"slug": "{{PROJECT_NAME_KEBAB}}",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"orientation": "portrait",
|
|
7
|
+
"icon": "./assets/icon.png",
|
|
8
|
+
"userInterfaceStyle": "dark",
|
|
9
|
+
"splash": {
|
|
10
|
+
"image": "./assets/splash.png",
|
|
11
|
+
"resizeMode": "contain",
|
|
12
|
+
"backgroundColor": "#0B0B0B"
|
|
13
|
+
},
|
|
14
|
+
"ios": {
|
|
15
|
+
"supportsTablet": true,
|
|
16
|
+
"bundleIdentifier": "com.pika.minigame.{{PROJECT_NAME_KEBAB}}"
|
|
17
|
+
},
|
|
18
|
+
"android": {
|
|
19
|
+
"adaptiveIcon": {
|
|
20
|
+
"foregroundImage": "./assets/adaptive-icon.png",
|
|
21
|
+
"backgroundColor": "#0B0B0B"
|
|
22
|
+
},
|
|
23
|
+
"package": "com.pika.minigame.{{PROJECT_NAME_SNAKE}}"
|
|
24
|
+
},
|
|
25
|
+
"plugins": [
|
|
26
|
+
"expo-router"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Development wrapper for standalone mini-game testing.
|
|
3
|
+
*
|
|
4
|
+
* This wraps your App component with a mock host bridge
|
|
5
|
+
* so you can develop and test without the main Pika app.
|
|
6
|
+
*
|
|
7
|
+
* Run with: yarn dev
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { Alert } from 'react-native';
|
|
12
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
13
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
14
|
+
|
|
15
|
+
import App from '../src/App';
|
|
16
|
+
import mockHost from './MockHost';
|
|
17
|
+
|
|
18
|
+
export default function DevApp() {
|
|
19
|
+
const handleExit = () => {
|
|
20
|
+
Alert.alert(
|
|
21
|
+
'Exit Pressed',
|
|
22
|
+
'In the real app, this would close the mini-game and return to the main app.',
|
|
23
|
+
[{ text: 'OK' }]
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const handleGameOver = (result: { score: number }) => {
|
|
28
|
+
console.log('[DevApp] Game over with score:', result.score);
|
|
29
|
+
Alert.alert(
|
|
30
|
+
'Game Over!',
|
|
31
|
+
`Your score: ${result.score}\n\nIn the real app, this score would be saved.`,
|
|
32
|
+
[{ text: 'OK' }]
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
38
|
+
<SafeAreaProvider>
|
|
39
|
+
<App
|
|
40
|
+
host={mockHost}
|
|
41
|
+
onExit={handleExit}
|
|
42
|
+
onGameOver={handleGameOver}
|
|
43
|
+
/>
|
|
44
|
+
</SafeAreaProvider>
|
|
45
|
+
</GestureHandlerRootView>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Host Bridge for standalone development.
|
|
3
|
+
*
|
|
4
|
+
* This simulates the host app's native capabilities so you can
|
|
5
|
+
* develop and test your mini-game without the main app.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { HostBridge, PronunciationResult } from '../src/types';
|
|
9
|
+
|
|
10
|
+
// Simulate recording state
|
|
11
|
+
let isRecording = false;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate a random score for testing different UI states.
|
|
15
|
+
*/
|
|
16
|
+
const randomScore = (min = 40, max = 100): number =>
|
|
17
|
+
Math.floor(Math.random() * (max - min + 1)) + min;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate letter-by-letter scores for a word.
|
|
21
|
+
*/
|
|
22
|
+
const generateLetterScores = (word: string) =>
|
|
23
|
+
[...word].map((letter) => ({
|
|
24
|
+
letter,
|
|
25
|
+
score: randomScore(50, 100),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Simulate async delay (like network/native calls).
|
|
30
|
+
*/
|
|
31
|
+
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Mock implementation of the host bridge.
|
|
35
|
+
* Edit these to test different scenarios.
|
|
36
|
+
*/
|
|
37
|
+
export const mockHost: HostBridge = {
|
|
38
|
+
pronunciation: {
|
|
39
|
+
startRecording: async () => {
|
|
40
|
+
if (isRecording) {
|
|
41
|
+
console.warn('[MockHost] Already recording');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
console.log('[MockHost] 🎤 Recording started...');
|
|
45
|
+
isRecording = true;
|
|
46
|
+
// Simulate recording initialization delay
|
|
47
|
+
await delay(100);
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
stopAndCheck: async (targetWord: string): Promise<PronunciationResult> => {
|
|
51
|
+
if (!isRecording) {
|
|
52
|
+
console.warn('[MockHost] Not recording');
|
|
53
|
+
throw new Error('Not recording');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log(`[MockHost] 🛑 Recording stopped. Checking: "${targetWord}"`);
|
|
57
|
+
isRecording = false;
|
|
58
|
+
|
|
59
|
+
// Simulate processing delay (like API call)
|
|
60
|
+
await delay(800 + Math.random() * 500);
|
|
61
|
+
|
|
62
|
+
// Generate mock result
|
|
63
|
+
const wordScore = randomScore(50, 100);
|
|
64
|
+
const letters = generateLetterScores(targetWord);
|
|
65
|
+
const totalScore = Math.round(
|
|
66
|
+
letters.reduce((sum, l) => sum + l.score, 0) / letters.length
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
console.log(`[MockHost] ✅ Score: ${totalScore}`);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
totalScore,
|
|
73
|
+
words: [
|
|
74
|
+
{
|
|
75
|
+
word: targetWord,
|
|
76
|
+
score: wordScore,
|
|
77
|
+
letters,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
cancel: async () => {
|
|
84
|
+
console.log('[MockHost] ❌ Recording cancelled');
|
|
85
|
+
isRecording = false;
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// Add more mock capabilities here as needed:
|
|
90
|
+
// audio: {
|
|
91
|
+
// play: async (url) => console.log(`[MockHost] Playing: ${url}`),
|
|
92
|
+
// stop: () => console.log('[MockHost] Audio stopped'),
|
|
93
|
+
// },
|
|
94
|
+
// haptics: {
|
|
95
|
+
// impact: (style) => console.log(`[MockHost] Haptic: ${style}`),
|
|
96
|
+
// },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Mock host that always fails - useful for testing error states.
|
|
101
|
+
*/
|
|
102
|
+
export const mockHostWithErrors: HostBridge = {
|
|
103
|
+
pronunciation: {
|
|
104
|
+
startRecording: async () => {
|
|
105
|
+
throw new Error('Microphone permission denied');
|
|
106
|
+
},
|
|
107
|
+
stopAndCheck: async () => {
|
|
108
|
+
throw new Error('Speech recognition failed');
|
|
109
|
+
},
|
|
110
|
+
cancel: async () => {},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Mock host with slow responses - useful for testing loading states.
|
|
116
|
+
*/
|
|
117
|
+
export const mockHostSlow: HostBridge = {
|
|
118
|
+
pronunciation: {
|
|
119
|
+
startRecording: async () => {
|
|
120
|
+
await delay(2000);
|
|
121
|
+
console.log('[MockHost Slow] Recording started after delay');
|
|
122
|
+
},
|
|
123
|
+
stopAndCheck: async (word) => {
|
|
124
|
+
await delay(3000);
|
|
125
|
+
return mockHost.pronunciation.stopAndCheck(word);
|
|
126
|
+
},
|
|
127
|
+
cancel: async () => {},
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export default mockHost;
|