Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 438 KiB |
@ -1,25 +0,0 @@
|
||||
Copyright 2019 Yukkei Choi
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
@ -1,55 +0,0 @@
|
||||
A fun game to test your color perception abilities.
|
||||
|
||||
Contributed as part of the Flutter Create 5K challenge by Yukkei Choi.
|
||||
|
||||
## How to play
|
||||
|
||||
Tap the unique color block as fast as possible.
|
||||
|
||||
## Features
|
||||
|
||||
1. Each round when user taps the unique color block, score will be increased by one.
|
||||
2. Timer: 30 seconds countdown.
|
||||
3. Color difference will be stepwise reduced when user reached a higher score.
|
||||
4. If it is difficult to distinguish the unique color block, user can "SHAKE" the device to shift to another theme color, while the position of the unique color block still keep the same.
|
||||
5. Provide a restart button at the end, user can infinitely play again without relaunching the app.
|
||||
6. After each replay, game board's theme color will be different from the previous play.
|
||||
7. Give user a grade based on the final score:
|
||||
|
||||
| score range | grade |
|
||||
|-------------|-------|
|
||||
| 0 - 9 | Fail |
|
||||
| 10 - 19 | D |
|
||||
| 20 - 29 | C |
|
||||
| 30 - 34 | B |
|
||||
| 35 - 39 | B+ |
|
||||
| 40 - 44 | A |
|
||||
| 45 or above | A+ |
|
||||
|
||||
## Graphics
|
||||
|
||||
1. I created all graphics used on the app by using Photoshop.
|
||||
2. Flutter is great and now I'm able to demonstrate my artwork on the app into practice.
|
||||
|
||||
## Techniques used
|
||||
|
||||
1. Use stateful widget to run the timer countdown animation.
|
||||
2. Since only 5kb is allowed, the grade is calculated by using math, instead of writing if-else statement.
|
||||
3. Use redux to store the game states:
|
||||
|
||||
| state | description | data type |
|
||||
|-------|----------------------------------------------------------|-------------------|
|
||||
| score | Store the player score | int |
|
||||
| board | Locate the position of unique color block | [[int],[int],...] |
|
||||
| count | Count the no. of replay, for switching the theme color | int |
|
||||
| page | Current page / game status | int |
|
||||
|
||||
| page | description |
|
||||
|------|----------------------------------------------------------------|
|
||||
| -1 | First launch the app, show the welcome screen with instruction |
|
||||
| 0 | Game in progress |
|
||||
| 1 | Game end, show result |
|
||||
|
||||
## Limitation
|
||||
|
||||
Limited to portrait view.
|
Before Width: | Height: | Size: 117 KiB |
Before Width: | Height: | Size: 113 KiB |
Before Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 113 KiB |
Before Width: | Height: | Size: 116 KiB |
Before Width: | Height: | Size: 111 KiB |
Before Width: | Height: | Size: 114 KiB |
@ -1,10 +0,0 @@
|
||||
[
|
||||
{
|
||||
"family": "MaterialIcons",
|
||||
"fonts": [
|
||||
{
|
||||
"asset": "https://fonts.gstatic.com/s/materialicons/v42/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
Before Width: | Height: | Size: 703 KiB |
Before Width: | Height: | Size: 343 KiB |
Before Width: | Height: | Size: 22 KiB |
@ -1,207 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_redux/flutter_redux.dart';
|
||||
import 'package:redux/redux.dart';
|
||||
|
||||
setText(text, size, color) => Text(text,
|
||||
style: TextStyle(
|
||||
fontSize: size,
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
decoration: TextDecoration.none));
|
||||
|
||||
pad(double left, double top) => EdgeInsets.fromLTRB(left, top, 0, 0);
|
||||
|
||||
setBg(name) => BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
alignment: Alignment.topLeft,
|
||||
image: AssetImage(name)));
|
||||
|
||||
class Game extends StatelessWidget {
|
||||
final Store<AppState> store;
|
||||
Game(this.store);
|
||||
_grade(int score) => [10, 20, 30, 35, 40, 45, 99]
|
||||
.where((i) => i > score)
|
||||
.reduce(min)
|
||||
.toString();
|
||||
|
||||
_createBoard(double size, List<List<int>> blocks, int depth,
|
||||
MaterialColor color) =>
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: blocks
|
||||
.map((cols) => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: cols
|
||||
.map((item) => Flexible(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (item == 1) store.dispatch(Action.next);
|
||||
},
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
color: item > 0 ? color[depth] : color)),
|
||||
))
|
||||
.toList()))
|
||||
.toList());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => StoreConnector<AppState, AppState>(
|
||||
// onInit: (state) => ShakeDetector.autoStart(
|
||||
// onPhoneShake: () => store.dispatch(Action.shake)),
|
||||
converter: (store) => store.state,
|
||||
builder: (context, state) {
|
||||
var w = MediaQuery.of(context).size.height / 16 * 9,
|
||||
size = w / (state.board.length + 1),
|
||||
depth = [1 + state.score ~/ 5, 4].reduce(min) * 100,
|
||||
colors = [
|
||||
Colors.blue,
|
||||
Colors.orange,
|
||||
Colors.pink,
|
||||
Colors.purple,
|
||||
Colors.cyan
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Color(0xFFBCE1F6),
|
||||
body: Center(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).size.height,
|
||||
width: MediaQuery.of(context).size.height / 16 * 9,
|
||||
child: Container(
|
||||
decoration: setBg(state.page < 0 ? 'p0.jpg' : 'p1.jpg'),
|
||||
child: state.page >= 0
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: w * 0.325,
|
||||
padding: pad(0, w * 0.145),
|
||||
child: setText(state.score.toString(),
|
||||
w * 0.2, Colors.white)),
|
||||
Container(
|
||||
height: w * 0.35,
|
||||
padding: pad(w * 0.69, state.page * 7.0),
|
||||
child: state.page < 1
|
||||
? Timer(
|
||||
onEnd: () =>
|
||||
store.dispatch(Action.end),
|
||||
width: w)
|
||||
: setText('End', w * 0.08, Colors.red)),
|
||||
state.page < 1
|
||||
? Container(
|
||||
width: w,
|
||||
height: w * 1.05,
|
||||
padding: pad(0, w * 0.05),
|
||||
child: _createBoard(
|
||||
size,
|
||||
state.board,
|
||||
depth,
|
||||
colors[
|
||||
state.count % colors.length]))
|
||||
: Container(
|
||||
width: w,
|
||||
height: w,
|
||||
decoration:
|
||||
setBg(_grade(state.score) + '.png'))
|
||||
])
|
||||
: Container()),
|
||||
),
|
||||
),
|
||||
floatingActionButton: state.page != 0
|
||||
? Container(
|
||||
// width: w * 0.2,
|
||||
// height: w * 0.2,
|
||||
child: FloatingActionButton(
|
||||
child: Icon(
|
||||
state.page < 1 ? Icons.play_arrow : Icons.refresh),
|
||||
onPressed: () => store.dispatch(Action.start)))
|
||||
: Container());
|
||||
});
|
||||
}
|
||||
|
||||
class Timer extends StatefulWidget {
|
||||
Timer({this.onEnd, this.width});
|
||||
final VoidCallback onEnd;
|
||||
final double width;
|
||||
@override
|
||||
_TimerState createState() => _TimerState();
|
||||
}
|
||||
|
||||
class _TimerState extends State<Timer> with TickerProviderStateMixin {
|
||||
Animation _animate;
|
||||
int _sec = 31;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animate = StepTween(begin: _sec, end: 0).animate(
|
||||
AnimationController(duration: Duration(seconds: _sec), vsync: this)
|
||||
..forward(from: 0.0))
|
||||
..addStatusListener((AnimationStatus s) {
|
||||
if (s == AnimationStatus.completed) widget.onEnd();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AnimatedBuilder(
|
||||
animation: _animate,
|
||||
builder: (BuildContext context, Widget child) => setText(
|
||||
_animate.value.toString().padLeft(2, '0'),
|
||||
widget.width * 0.12,
|
||||
Colors.green));
|
||||
}
|
||||
|
||||
//REDUX
|
||||
@immutable
|
||||
class AppState {
|
||||
final int score, page, count;
|
||||
final List<List<int>> board;
|
||||
AppState({this.score, this.page, this.board, this.count});
|
||||
AppState.init()
|
||||
: score = 0,
|
||||
page = -1,
|
||||
count = 0,
|
||||
board = newBoard(0);
|
||||
}
|
||||
|
||||
enum Action { next, end, start, shake }
|
||||
|
||||
AppState reducer(AppState s, act) {
|
||||
switch (act) {
|
||||
case Action.next:
|
||||
return AppState(
|
||||
score: s.score + 1,
|
||||
page: s.page,
|
||||
count: s.count,
|
||||
board: newBoard(s.score + 1));
|
||||
case Action.end:
|
||||
return AppState(
|
||||
score: s.score, page: 1, count: s.count + 1, board: s.board);
|
||||
case Action.start:
|
||||
return AppState(score: 0, page: 0, count: s.count, board: newBoard(0));
|
||||
case Action.shake:
|
||||
return AppState(
|
||||
score: s.score, page: s.page, count: s.count + 1, board: s.board);
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
List<List<int>> newBoard(score) {
|
||||
var size = score < 7 ? score + 3 : 10,
|
||||
rng = Random(),
|
||||
bingoRow = rng.nextInt(size),
|
||||
bingoCol = rng.nextInt(size);
|
||||
List<List<int>> board = [];
|
||||
for (var i = 0; i < size; i++) {
|
||||
List<int> row = [];
|
||||
for (var j = 0; j < size; j++)
|
||||
row.add(i == bingoRow && j == bingoCol ? 1 : 0);
|
||||
board.add(row);
|
||||
}
|
||||
return board;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_redux/flutter_redux.dart';
|
||||
import 'package:redux/redux.dart';
|
||||
import 'game.dart';
|
||||
|
||||
void main() {
|
||||
final store = Store<AppState>(
|
||||
reducer,
|
||||
initialState: AppState.init(),
|
||||
);
|
||||
|
||||
runApp(
|
||||
StoreProvider<AppState>(
|
||||
store: store,
|
||||
child: MaterialApp(
|
||||
home: Game(store),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0-nullsafety.5"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0-nullsafety.5"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_redux:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_redux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.5.4"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0-nullsafety.6"
|
||||
redux:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: redux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0-nullsafety.5"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0-nullsafety.5"
|
||||
sdks:
|
||||
dart: ">=2.12.0-0 <3.0.0"
|
@ -1,25 +0,0 @@
|
||||
name: vision_challenge
|
||||
author: Yukkei Choi
|
||||
|
||||
environment:
|
||||
sdk: ">=2.10.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
redux: ^3.0.0
|
||||
flutter_redux: ^0.5.3
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- 10.png
|
||||
- 20.png
|
||||
- 30.png
|
||||
- 35.png
|
||||
- 40.png
|
||||
- 45.png
|
||||
- 99.png
|
||||
- p0.jpg
|
||||
- p1.jpg
|
||||
- preview.png
|
@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title></title>
|
||||
<script defer src="main.dart.js" type="application/javascript"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|