Skip to content
Snippets Groups Projects
game_engine.dart 7.17 KiB
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:flutter_custom_toolbox/flutter_toolbox.dart';

import 'package:reversi/common/cubit/nav/nav_cubit_pages.dart';

import 'package:reversi/ai/move_finder.dart';
import 'package:reversi/config/styling.dart';
import 'package:reversi/cubit/activity/activity_cubit.dart';
import 'package:reversi/models/activity/game_board.dart';
import 'package:reversi/models/activity/game_model.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) {
    if (1 == 1) {
      if (1 == 1) {
        Image decorationImage = Image(
          image: AssetImage(model.gameResultString == 'black'
              ? 'assets/ui/game_win.png'
              : 'assets/ui/placeholder.png'),
          fit: BoxFit.fill,
        );
        final double width = MediaQuery.of(context).size.width;

        return Container(
          margin: const EdgeInsets.all(2),
          padding: const EdgeInsets.all(2),
          child: Table(
            defaultColumnWidth: FixedColumnWidth(width / 3.1),
            defaultVerticalAlignment: TableCellVerticalAlignment.bottom,
            children: [
              TableRow(
                children: [
                  Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [decorationImage],
                  ),
                  Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      ActivityButtonQuit(
                        onPressed: () {
                          BlocProvider.of<ActivityCubit>(context).quitActivity();
                          BlocProvider.of<NavCubitPage>(context).goToPageHome();
                        },
                      )
                    ],
                  ),
                  Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [decorationImage],
                  ),
                ],