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 'dart:async';
import 'package:async/async.dart';
import 'package:flutter/material.dart';
import 'package:reversi/ai/move_finder.dart';
import 'package:reversi/config/styling.dart';
import 'package:reversi/models/activity/game_board.dart';
import 'package:reversi/models/activity/game_model.dart';
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import 'package:reversi/ui/widgets/actions/button_game_quit.dart';
import 'package:reversi/ui/widgets/game/game_board_display.dart';
import 'package:reversi/ui/widgets/game/score_box.dart';
import 'package:reversi/ui/widgets/game/thinking_indicator.dart';
/// The [GameEngine] Widget represents the entire game
/// display, from scores to board state and everything in between.
class GameEngine extends StatefulWidget {
const GameEngine({super.key});
@override
State createState() => _GameEngineState();
}
/// State class for [GameEngine].
///
/// 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. [GameEngine] uses a [StreamBuilder] wired up to that stream
/// of models to build out its [Widget] tree.
class _GameEngineState extends State<GameEngine> {
final StreamController<GameModel> _userMovesController = StreamController<GameModel>();
final StreamController<GameModel> _restartController = StreamController<GameModel>();
Stream<GameModel>? _modelStream;
_GameEngineState() {
// 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 Future.delayed(const Duration(milliseconds: 2000), () {
return 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(
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));
}
}
// Builds out the Widget tree using the most recent GameModel from the stream.
Widget _buildWidgets(GameModel model) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Spacer(flex: 1),
ScoreBox(player: PieceType.black, model: model),
const Spacer(flex: 4),
ScoreBox(player: PieceType.white, model: model),
const Spacer(flex: 1),
],
),
const SizedBox(height: 8),
GameBoardDisplay(
context: context,
model: model,
callback: _attemptUserMove,
),
const SizedBox(height: 8),
ThinkingIndicator(
color: Styling.thinkingColor,
height: Styling.thinkingSize,
visible: model.player == PieceType.white,
),
model.gameIsOver ? _buildGameEnd(model) : SizedBox.shrink()
],
);
}
Widget _buildGameEnd(GameModel model) {
Image decorationImage = Image(
image: AssetImage(model.gameResultString == 'black'
? 'assets/ui/game_win.png'
: 'assets/ui/placeholder.png'),
fit: BoxFit.fill,
);
return Container(
margin: const EdgeInsets.all(2),
padding: const EdgeInsets.all(2),
child: Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
defaultVerticalAlignment: TableCellVerticalAlignment.bottom,
children: [
TableRow(
children: [
Column(
children: [decorationImage],
),
Column(
children: [QuitGameButton()],
),
Column(
children: [decorationImage],
),
],
),
],
),
);
}
}