Skip to content
Snippets Groups Projects
main.dart 10.8 KiB
Newer Older
Benoît Harrault's avatar
Benoît Harrault committed
// 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.
Benoît Harrault's avatar
Benoît Harrault committed
import 'dart:async';
Benoît Harrault's avatar
Benoît Harrault committed
import 'package:async/async.dart';
import 'package:flutter/material.dart';
Benoît Harrault's avatar
Benoît Harrault committed
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]);
Benoît Harrault's avatar
Benoît Harrault committed

  runApp(FlutterFlipApp());
Benoît Harrault's avatar
Benoît Harrault committed
/// 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(),
Benoît Harrault's avatar
Benoît Harrault committed
    );
  }
}

/// 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>();
Benoît Harrault's avatar
Benoît Harrault committed
  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) {
Benoît Harrault's avatar
Benoît Harrault committed
    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)) {
Benoît Harrault's avatar
Benoît Harrault committed
      _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}';
Benoît Harrault's avatar
Benoît Harrault committed

    return Container(
      padding: const EdgeInsets.symmetric(
        vertical: 3.0,
        horizontal: 25.0,
Benoît Harrault's avatar
Benoît Harrault committed
      ),
      decoration: (model.player == player)
          ? Styling.activePlayerIndicator
          : Styling.inactivePlayerIndicator,
      child: Row(
        children: <Widget>[
          SizedBox(
            width: 25.0,
            height: 25.0,
Benoît Harrault's avatar
Benoît Harrault committed
            child: Image(
              image: AssetImage(assetImageName),
              fit: BoxFit.fill,
            ),
Benoît Harrault's avatar
Benoît Harrault committed
          SizedBox(
            width: 10.0,
          ),
          Text(
            scoreText,
            textAlign: TextAlign.center,
            style: TextStyle(
              fontSize: 35.0,
Benoît Harrault's avatar
Benoît Harrault committed
              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';
Benoît Harrault's avatar
Benoît Harrault committed

        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)),
Benoît Harrault's avatar
Benoît Harrault committed
                    ),
                  ),
                  onTap: () {
                    _attemptUserMove(model, x, y);
                  },
                ),
              ),
            )
Benoît Harrault's avatar
Benoît Harrault committed

        cells.add(cell);
      }

      rows.add(TableRow(children: cells));
    }

    return Table(defaultColumnWidth: IntrinsicColumnWidth(), children: rows);
Benoît Harrault's avatar
Benoît Harrault committed
  }

  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'
Benoît Harrault's avatar
Benoît Harrault committed
          ? 'assets/icons/game_win.png'
          : 'assets/icons/empty.png'),
Benoît Harrault's avatar
Benoît Harrault committed
      fit: BoxFit.fill,
    );

    return Container(
        margin: EdgeInsets.all(2),
        padding: EdgeInsets.all(2),
        child: Table(defaultColumnWidth: IntrinsicColumnWidth(), children: [
Benoît Harrault's avatar
Benoît Harrault committed
          TableRow(
            children: [
              Column(children: [decorationImage]),
              Column(children: [decorationImage]),
Benoît Harrault's avatar
Benoît Harrault committed
              Column(children: [
                GestureDetector(
                  onTap: () {
                    _restartController.add(
                      GameModel(board: GameBoard()),
                    );
                  },
                  child: _buildRestartGameWidget(),
                )
              ]),
              Column(children: [decorationImage]),
              Column(children: [decorationImage]),
Benoît Harrault's avatar
Benoît Harrault committed
  }

  // Builds out the Widget tree using the most recent GameModel from the stream.
  Widget _buildWidgets(BuildContext context, GameModel model) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          TextButton(
            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'),
                fit: BoxFit.fill,
              ),
            ),
            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),
            ],
          ),