blob: c9c39a96d00e96dbcee2fc48ca038c915818494e [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async' show Timer;
import 'dart:math' show Random;
import 'dart:ui' show lerpDouble;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show RawKeyDownEvent, RawKeyEvent;
const _kStartLength = 5;
const _kMaxCrumbs = 20;
const _kCrumbChanceToAppear = 0.05;
const _kKeyLabelToDirection = <String, Coord>{
'w': Coord.up,
'a': Coord.left,
's': Coord.down,
'd': Coord.right,
};
const _kInitialCoords = <Coord>[
Coord(-2, 0),
Coord(-1, 0),
Coord(0, 0),
Coord(1, 0),
Coord(2, 0),
];
class ErrorPage extends StatefulWidget {
@override
_ErrorPageState createState() => _ErrorPageState();
}
class _ErrorPageState extends State<ErrorPage> {
final _focusNode = FocusNode();
final _direction = ValueNotifier<Coord?>(null);
final _coords = <Coord>[];
final _crumbCoords = <Coord>[];
int _length = _kStartLength;
Timer? _timer;
Size? _screenSize;
Offset? _screenCenter;
bool _lost = false;
final _random = Random();
double rnd() => _random.nextDouble();
@override
void initState() {
super.initState();
if (WidgetsBinding.instance != null) {
WidgetsBinding.instance!.addPostFrameCallback(
(_) => FocusScope.of(context).requestFocus(_focusNode));
}
_reset();
}
void _reset() {
_coords
..clear()
..addAll(_kInitialCoords);
_crumbCoords.clear();
_length = _kStartLength;
_lost = false;
}
void _startTimer() {
_timer?.cancel();
_timer = Timer.periodic(Duration(milliseconds: 250), _onTimer);
}
@override
void dispose() {
_timer?.cancel();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => RawKeyboardListener(
focusNode: FocusNode(),
onKey: _onKey,
child: GestureDetector(
child: Container(
color: Colors.transparent,
child: LayoutBuilder(builder: (context, constraints) {
_screenSize = constraints.biggest;
_screenCenter = _screenSize!.center(Offset.zero);
return Stack(
children: [
..._coords.asMap().map(_buildBody).values.toList(),
..._crumbCoords.map(_buildCrumb).toList(),
Opacity(
opacity: 0,
child: TextField(
focusNode: _focusNode,
autofocus: true,
),
),
],
);
}),
),
onTap: () => FocusScope.of(context).requestFocus(_focusNode),
),
);
void _onKey(RawKeyEvent value) {
if (value.runtimeType == RawKeyDownEvent) {
if (_lost) {
_reset();
}
Coord? newDirection = _kKeyLabelToDirection[value.logicalKey.keyLabel];
if (newDirection != null) {
if (_coords[_coords.length - 2] - _coords.last != newDirection) {
_direction.value = newDirection;
_onTimer(null);
_startTimer();
}
}
}
}
String _textForIndex(int index) {
if (index == 0) {
return 'E';
} else if (index == _coords.length - 2) {
return 'O';
} else {
return 'R';
}
}
Widget _buildCrumb(Coord coord) =>
_buildSquare(squaresToScreen(coord), 'R', false);
MapEntry<int, Widget> _buildBody(int index, Coord coord) => MapEntry(
index, _buildSquare(squaresToScreen(coord), _textForIndex(index), true));
Widget _buildSquare(Offset offset, String string, bool invert) => Positioned(
left: offset.dx,
top: offset.dy,
child: Container(
width: 16,
height: 16,
color: invert ? Colors.black : null,
child: Text(
string,
textAlign: TextAlign.center,
style: invert ? TextStyle(color: Colors.white) : null,
),
),
);
Coord screenToSquares(Offset screen) => Coord(
((screen.dx - _screenCenter!.dx) / 16).floor(),
((screen.dy - _screenCenter!.dy) / 16).floor());
Offset squaresToScreen(Coord squares) =>
Offset((squares.x - 0.5), (squares.y - 0.5)) * 16 + _screenCenter!;
void _addCrumb() {
Coord newCrumb = screenToSquares(Offset(
lerpDouble(0, _screenSize!.width, rnd())!,
lerpDouble(0, _screenSize!.height, rnd())!,
));
// add if coordinate is currently free
if (!_coords.contains(newCrumb) && !_crumbCoords.contains(newCrumb)) {
_crumbCoords.add(newCrumb);
}
}
void _onTimer(Timer? timer) {
if (_direction.value == null) {
return;
}
Coord newCoord = _coords.last + _direction.value!;
// lost: eating own tail
if (_coords.contains(newCoord)) {
_lost = true;
_timer?.cancel();
return;
}
// lost: leaving the screen
if (!(Offset.zero & _screenSize!).contains(squaresToScreen(newCoord))) {
_lost = true;
_timer?.cancel();
return;
}
setState(() {
_coords.add(newCoord);
if (_coords.length > _length) {
_coords.removeAt(0);
}
// yum
if (_crumbCoords.remove(newCoord)) {
_length++;
}
// more food
if (_crumbCoords.length < _kMaxCrumbs && rnd() < _kCrumbChanceToAppear) {
_addCrumb();
}
});
}
}
class Coord {
const Coord(this.x, this.y);
final int x;
final int y;
static const Coord up = Coord(0, -1);
static const Coord left = Coord(-1, 0);
static const Coord down = Coord(0, 1);
static const Coord right = Coord(1, 0);
Coord operator -(Coord other) => Coord(x - other.x, y - other.y);
Coord operator +(Coord other) => Coord(x + other.x, y + other.y);
@override
bool operator ==(dynamic other) {
if (other is! Coord) {
return false;
}
final Coord typedOther = other;
return x == typedOther.x && y == typedOther.y;
}
@override
int get hashCode => hashValues(x, y);
}