Newer
Older
// Copyright 2018 The Chromium 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 'package:flutter/material.dart';
import 'package:flutter/services.dart' show SystemChrome, DeviceOrientation;
import 'game_board.dart';
import 'game_model.dart';
import 'move_finder.dart';
import 'styling.dart';
import 'thinking_indicator.dart';
/// Main function for the app. Turns off the system overlays and locks portrait
/// orientation for a more game-like UI, and then runs the [Widget] tree.
void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
/// The App class. Unlike many Flutter apps, this one does not use Material
/// widgets, so there's no [MaterialApp] or [Theme] objects.
class FlutterFlipApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: GameScreen(),
);
}
}
/// The [GameScreen] Widget represents the entire game
/// display, from scores to board state and everything in between.
class GameScreen extends StatefulWidget {
@override
State createState() => _GameScreenState();
}
/// State class for [GameScreen].
///
/// The game is modeled as a [Stream] of immutable instances of [GameModel].
/// Each move by the player or CPU results in a new [GameModel], which is
/// sent downstream. [GameScreen] uses a [StreamBuilder] wired up to that stream
/// of models to build out its [Widget] tree.
class _GameScreenState extends State<GameScreen> {
final StreamController<GameModel> _userMovesController = StreamController<GameModel>();
final StreamController<GameModel> _restartController = StreamController<GameModel>();
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
Stream<GameModel>? _modelStream;
_GameScreenState() {
// Below is the combination of streams that controls the flow of the game.
// There are two streams of models produced by player interaction (either by
// restarting the game, which produces a brand new game model and sends it
// downstream, or tapping on one of the board locations to play a piece, and
// which creates a new board model with the result of the move and sends it
// downstream. The StreamGroup combines these into a single stream, then
// does a little trick with asyncExpand.
//
// The function used in asyncExpand checks to see if it's the CPU's turn
// (white), and if so creates a [MoveFinder] to look for the best move. It
// awaits the calculation, and then creates a new [GameModel] with the
// result of that move and sends it downstream by yielding it. If it's still
// the CPU's turn after making that move (which can happen in reversi), this
// is repeated.
//
// The final stream of models that exits the asyncExpand call is a
// combination of "new game" models, models with the results of player
// moves, and models with the results of CPU moves. These are fed into the
// StreamBuilder in [build], and used to create the widgets that comprise
// the game's display.
_modelStream = StreamGroup.merge([
_userMovesController.stream,
_restartController.stream,
]).asyncExpand((model) async* {
yield model;
var newModel = model;
while (newModel.player == PieceType.white) {
final finder = MoveFinder(newModel.board);
final move = await finder.findNextMove(newModel.player, 5);
if (move != null) {
newModel = newModel.updateForMove(move.x, move.y);
yield newModel;
}
}
});
}
// Thou shalt tidy up thy stream controllers.
@override
void dispose() {
_userMovesController.close();
_restartController.close();
super.dispose();
}
/// The build method mostly just sets up the StreamBuilder and leaves the
/// details to _buildWidgets.
@override
Widget build(BuildContext context) {
return StreamBuilder<GameModel>(
stream: _modelStream,
builder: (context, snapshot) {
return _buildWidgets(
context,
snapshot.hasData ? snapshot.data! : GameModel(board: GameBoard()),
);
},
);
}
// Called when the user taps on the game's board display. If it's the player's
// turn, this method will attempt to make the move, creating a new GameModel
// in the process.
void _attemptUserMove(GameModel model, int x, int y) {
if (model.player == PieceType.black && model.board.isLegalMove(x, y, model.player)) {
_userMovesController.add(model.updateForMove(x, y));
}
}
Widget _buildScoreBox(PieceType player, GameModel model) {
var assetImageCode = player == PieceType.black ? 'black' : 'white';
String assetImageName =
'assets/skins/' + model.skin + '_tile_' + assetImageCode + '.png';
var scoreText =
player == PieceType.black ? '${model.blackScore}' : '${model.whiteScore}';
return Container(
padding: const EdgeInsets.symmetric(
vertical: 3.0,
),
decoration: (model.player == player)
? Styling.activePlayerIndicator
: Styling.inactivePlayerIndicator,
child: Row(
children: <Widget>[
SizedBox(
child: Image(
image: AssetImage(assetImageName),
fit: BoxFit.fill,
),
SizedBox(
width: 10.0,
),
Text(
scoreText,
textAlign: TextAlign.center,
style: TextStyle(
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
color: Color(0xff000000),
),
),
],
),
);
}
Widget _buildScoreBoxes(GameModel model) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Spacer(flex: 1),
_buildScoreBox(PieceType.black, model),
Spacer(flex: 4),
_buildScoreBox(PieceType.white, model),
Spacer(flex: 1),
],
);
}
Table _buildGameBoardDisplay(BuildContext context, GameModel model) {
final rows = <TableRow>[];
for (var y = 0; y < GameBoard.height; y++) {
final cells = <Column>[];
for (var x = 0; x < GameBoard.width; x++) {
PieceType pieceType = model.board.getPieceAtLocation(x, y);
String? assetImageCode = Styling.assetImageCodes[pieceType];
String assetImageName = 'assets/skins/' +
model.skin +
'_tile_' +
(assetImageCode != null ? assetImageCode : 'empty') +
'.png';
Column cell = Column(
children: [
Container(
decoration: Styling.boardCellDecoration,
child: SizedBox(
child: GestureDetector(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(child: child, scale: animation);
},
child: Image(
image: AssetImage(assetImageName),
fit: BoxFit.fill,
key: ValueKey<int>(pieceType == PieceType.empty
? 0
: (pieceType == PieceType.black ? 1 : 2)),
),
),
onTap: () {
_attemptUserMove(model, x, y);
},
),
),
)
cells.add(cell);
}
rows.add(TableRow(children: cells));
}
return Table(defaultColumnWidth: IntrinsicColumnWidth(), children: rows);
}
Widget _buildThinkingIndicator(GameModel model) {
return ThinkingIndicator(
color: Styling.thinkingColor,
height: Styling.thinkingSize,
visible: model.player == PieceType.white,
);
}
Widget _buildRestartGameWidget() {
return Container(
padding: const EdgeInsets.symmetric(
vertical: 5.0,
horizontal: 15.0,
),
child: Image(
image: AssetImage('assets/icons/button_restart.png'),
fit: BoxFit.fill,
),
);
}
Widget _buildEndGameWidget(GameModel model) {
Image decorationImage = Image(
image: AssetImage(model.gameResultString == 'black'
fit: BoxFit.fill,
);
return Container(
margin: EdgeInsets.all(2),
padding: EdgeInsets.all(2),
child: Table(defaultColumnWidth: IntrinsicColumnWidth(), children: [
Column(children: [decorationImage]),
Column(children: [decorationImage]),
Column(children: [
GestureDetector(
onTap: () {
_restartController.add(
GameModel(board: GameBoard()),
);
},
child: _buildRestartGameWidget(),
)
]),
Column(children: [decorationImage]),
Column(children: [decorationImage]),
}
// Builds out the Widget tree using the most recent GameModel from the stream.
Widget _buildWidgets(BuildContext context, GameModel model) {
return Scaffold(
appBar: AppBar(
actions: [
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Colors.blue,
width: 4,
),
),
margin: EdgeInsets.all(8),
child: Image(
image: AssetImage('assets/icons/button_restart.png'),
),
),
onPressed: () => _restartController.add(GameModel(board: GameBoard())),
)
],
),
body: Container(
padding: EdgeInsets.only(
top: 5.0,
left: 5.0,
right: 5.0,
),
decoration: Styling.mainWidgetDecoration,
child: SafeArea(
child: Column(
children: [
_buildScoreBoxes(model),
SizedBox(height: 5),
_buildGameBoardDisplay(context, model),
SizedBox(height: 5),
_buildThinkingIndicator(model),
if (model.gameIsOver) _buildEndGameWidget(model),
],
),