packages feed

bishbosh-0.1.0.0: src-exe/BishBosh/UI/Raw.hs

{-# LANGUAGE CPP, FlexibleContexts, ScopedTypeVariables #-}
{-
	Copyright (C) 2018 Dr. Alistair Ward

	This file is part of BishBosh.

	BishBosh is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.

	BishBosh is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with BishBosh.  If not, see <http://www.gnu.org/licenses/>.
-}
{- |
 [@AUTHOR@]	Dr. Alistair Ward

 [@DESCRIPTION@]	Interacts with the user by receiving commands & displaying the game on a raw text-based interface.
-}

module BishBosh.UI.Raw(
-- * Functions
--	readMove,
	takeTurns
 ) where

import			Control.Arrow((&&&))
import			Control.Monad((>=>), (<=<))
import qualified	BishBosh.Attribute.WeightedMeanAndCriterionValues		as Attribute.WeightedMeanAndCriterionValues
import qualified	BishBosh.Component.Move						as Component.Move
import qualified	BishBosh.Component.QualifiedMove				as Component.QualifiedMove
import qualified	BishBosh.Component.Turn						as Component.Turn
import qualified	BishBosh.Concurrent.Pondering					as Concurrent.Pondering
import qualified	BishBosh.ContextualNotation.PGN					as ContextualNotation.PGN
import qualified	BishBosh.ContextualNotation.PositionHashQualifiedMoveTree	as ContextualNotation.PositionHashQualifiedMoveTree
import qualified	BishBosh.ContextualNotation.QualifiedMoveForest			as ContextualNotation.QualifiedMoveForest
import qualified	BishBosh.Data.Exception						as Data.Exception
import qualified	BishBosh.Data.Time						as Data.Time
import qualified	BishBosh.Evaluation.Fitness					as Evaluation.Fitness
import qualified	BishBosh.Evaluation.PositionHashQuantifiedGameTree		as Evaluation.PositionHashQuantifiedGameTree
import qualified	BishBosh.Evaluation.QuantifiedGame				as Evaluation.QuantifiedGame
import qualified	BishBosh.Input.IOOptions					as Input.IOOptions
import qualified	BishBosh.Input.NativeUIOptions					as Input.NativeUIOptions
import qualified	BishBosh.Input.Options						as Input.Options
import qualified	BishBosh.Input.SearchOptions					as Input.SearchOptions
import qualified	BishBosh.Input.StandardOpeningOptions				as Input.StandardOpeningOptions
import qualified	BishBosh.Input.UIOptions					as Input.UIOptions
import qualified	BishBosh.Model.Game						as Model.Game
import qualified	BishBosh.Notation.MoveNotation					as Notation.MoveNotation
import qualified	BishBosh.Property.ExtendedPositionDescription			as Property.ExtendedPositionDescription
import qualified	BishBosh.Property.ForsythEdwards				as Property.ForsythEdwards
import qualified	BishBosh.Property.ShowFloat					as Property.ShowFloat
import qualified	BishBosh.Search.Search						as Search.Search
import qualified	BishBosh.Search.SearchState					as Search.SearchState
import qualified	BishBosh.State.ApplicationTerminationReason			as State.ApplicationTerminationReason
import qualified	BishBosh.State.Board						as State.Board
import qualified	BishBosh.State.InstancesByPosition				as State.InstancesByPosition
import qualified	BishBosh.State.MaybePieceByCoordinates				as State.MaybePieceByCoordinates
import qualified	BishBosh.State.PlayState					as State.PlayState
import qualified	BishBosh.Text.ShowColouredPrefix				as Text.ShowColouredPrefix
import qualified	BishBosh.Text.ShowList						as Text.ShowList
import qualified	BishBosh.Text.ShowPrefix					as Text.ShowPrefix
import qualified	BishBosh.Types							as T
import qualified	BishBosh.UI.Command						as UI.Command
import qualified	BishBosh.UI.PrintObject						as UI.PrintObject
import qualified	BishBosh.UI.ReportObject					as UI.ReportObject
import qualified	BishBosh.UI.SetObject						as UI.SetObject
import qualified	Control.Concurrent
import qualified	Control.DeepSeq
import qualified	Control.Exception
import qualified	Control.Monad
import qualified	Control.Monad.Reader
import qualified	Data.Array.IArray
import qualified	Data.Bits
import qualified	Data.Default
import qualified	Data.List.Extra
import qualified	Data.Map
import qualified	Data.Maybe
import qualified	Data.Time.Clock
import qualified	Numeric
import qualified	System.IO
import qualified	System.Random
import qualified	ToolShed.System.Random

#ifdef USE_UNBOXED_ARRAYS
import qualified	Data.Array.Unboxed
#endif

{- |
	* Reads a command-sequence from the user, terminating in either a request to move or to exit the game.

	* Since the user can also request roll-back to an earlier game before then requesting a new move, a new game is returned rather than just the requested move.
-}
readMove :: forall column criterionValue criterionWeight pieceSquareValue positionHash randomGen rankValue row weightedMean x y. (
	Control.DeepSeq.NFData					column,
#ifdef USE_PARALLEL
	Control.DeepSeq.NFData					criterionValue,
#endif
	Control.DeepSeq.NFData					criterionWeight,
	Control.DeepSeq.NFData					pieceSquareValue,
	Control.DeepSeq.NFData					rankValue,
	Control.DeepSeq.NFData					row,
	Control.DeepSeq.NFData					x,
	Control.DeepSeq.NFData					y,
	Data.Array.IArray.Ix					x,
#ifdef USE_UNBOXED_ARRAYS
	Data.Array.Unboxed.IArray Data.Array.Unboxed.UArray	pieceSquareValue,	-- Requires 'FlexibleContexts'. The unboxed representation of the array-element must be defined (& therefore must be of fixed size).
#endif
	Data.Bits.Bits						positionHash,
	Fractional						criterionValue,
	Fractional						pieceSquareValue,
	Fractional						rankValue,
	Fractional						weightedMean,
	Integral						column,
	Integral						x,
	Integral						y,
	Ord							positionHash,
	Read							x,
	Read							y,
	Real							criterionValue,
	Real							criterionWeight,
	Real							pieceSquareValue,
	Real							rankValue,
	Real							weightedMean,
	Show							column,
	Show							pieceSquareValue,
	Show							row,
	Show							x,
	Show							y,
	System.Random.RandomGen					randomGen
 )
	=> ContextualNotation.PositionHashQualifiedMoveTree.PositionHashQualifiedMoveTree x y positionHash
	-> randomGen
	-> Data.Time.Clock.UTCTime
	-> State.PlayState.PlayState column criterionValue criterionWeight pieceSquareValue positionHash rankValue row weightedMean x y
	-> IO (State.PlayState.PlayState column criterionValue criterionWeight pieceSquareValue positionHash rankValue row weightedMean x y)
{-# SPECIALISE readMove :: (
	Control.DeepSeq.NFData	column,
	Control.DeepSeq.NFData	row,
	Integral		column,
	Show			column,
	Show			row,
	System.Random.RandomGen	randomGen
 )
	=> ContextualNotation.PositionHashQualifiedMoveTree.PositionHashQualifiedMoveTree T.X T.Y T.PositionHash
	-> randomGen
	-> Data.Time.Clock.UTCTime
	-> State.PlayState.PlayState column T.CriterionValue T.CriterionWeight T.PieceSquareValue T.PositionHash T.RankValue row T.WeightedMean T.X T.Y
	-> IO (State.PlayState.PlayState column T.CriterionValue T.CriterionWeight T.PieceSquareValue T.PositionHash T.RankValue row T.WeightedMean T.X T.Y)
 #-}
readMove positionHashQualifiedMoveTree randomGen startUTCTime playState	= let
	(game, options)			= State.PlayState.getGame &&& State.PlayState.getOptions $ playState
	(searchOptions, ioOptions)	= Input.Options.getSearchOptions &&& Input.Options.getIOOptions $ options

	searchDepthByLogicalColour	= Input.SearchOptions.getSearchDepthByLogicalColour searchOptions
	fullyManual			= Data.Map.null searchDepthByLogicalColour

	(uiOptions, maybeMaximumPGNNames)	= Input.IOOptions.getUIOptions &&& Input.IOOptions.getMaybeMaximumPGNNames $ ioOptions

	moveNotation	= Input.UIOptions.getMoveNotation uiOptions
	nDecimalDigits	= Input.UIOptions.getNDecimalDigits uiOptions
	verbosity	= Input.UIOptions.getVerbosity uiOptions
 in (
	\nativeUIOptions -> let
		show2D :: Model.Game.Game x y -> String
		show2D	= uncurry State.MaybePieceByCoordinates.show2D (
			snd {-columns-} . Input.NativeUIOptions.getBoardMagnification &&& Input.NativeUIOptions.getColourScheme $ nativeUIOptions
		 ) (
			Notation.MoveNotation.getOrigin moveNotation
		 ) . State.Board.getMaybePieceByCoordinates . Model.Game.getBoard

		onCommand :: UI.Command.Command x y -> IO (State.PlayState.PlayState column criterionValue criterionWeight pieceSquareValue positionHash rankValue row weightedMean x y)
		onCommand UI.Command.Hint	= do
			Control.Monad.unless (Model.Game.isTerminated game) . Data.Maybe.maybe (
				do
					Control.Monad.when (verbosity > Data.Default.def && not (ContextualNotation.PositionHashQualifiedMoveTree.isTerminal positionHashQualifiedMoveTree)) . System.IO.hPutStrLn System.IO.stderr $ Text.ShowColouredPrefix.showsPrefixInfo "failed to find any suitable archived move."

					let
						searchResult	= Control.Monad.Reader.runReader (
							Search.Search.search (Input.SearchOptions.getSearchDepth searchOptions) $ State.PlayState.getSearchState playState
						 ) searchOptions

					case Search.Search.getQuantifiedGames searchResult of
						quantifiedGame : _	-> putStrLn . Text.ShowColouredPrefix.showsPrefixInfo . showString "Try " $ if verbosity == maxBound
							then Notation.MoveNotation.showsNotationFloatToNDecimals moveNotation nDecimalDigits searchResult ""
							else Data.Maybe.maybe (
								Control.Exception.throw $ Data.Exception.mkNullDatum "BishBosh.UI.Raw.readMove.onCommand:\tzero turns have been made."
							) (
								showChar '"' . flip (Notation.MoveNotation.showsNotation moveNotation) "\"."
							) . Model.Game.maybeLastTurn $ Evaluation.QuantifiedGame.getGame quantifiedGame
						_		-> Control.Exception.throwIO . Data.Exception.mkRequestFailure . showString "BishBosh.UI.Raw.readMove.onCommand:\tunexpectedly failed to find any moves; " $ shows game "."	-- CAVEAT: the game should have terminated.
			 ) (
				\(qualifiedMove, names) -> putStrLn . Text.ShowColouredPrefix.showsPrefixInfo . showString "Try \"" . Notation.MoveNotation.showsNotation moveNotation qualifiedMove . showString "\" from:" $ ContextualNotation.QualifiedMoveForest.showsNames maybeMaximumPGNNames names ""
			 ) $ ContextualNotation.PositionHashQualifiedMoveTree.maybeRandomlySelectOnymousQualifiedMove randomGen (
				Input.StandardOpeningOptions.getMatchSwitches $ Input.SearchOptions.getStandardOpeningOptions searchOptions
			 ) game positionHashQualifiedMoveTree

			return {-to IO-monad-} playState	-- N.B.: though one could merely call "eventLoop", a new random-generator is desirable in case an alternative hint is requested.
		onCommand (UI.Command.Print printObject)	= do
			case printObject of
				UI.PrintObject.Configuration	-> putStrLn $ Property.ShowFloat.showsFloatToN nDecimalDigits options "."
				UI.PrintObject.Help		-> putStrLn . showString "Enter either a move in " . shows moveNotation $ showString "-notation, or:\n" UI.Command.usageMessage

			eventLoop
		onCommand UI.Command.Quit	= do
			Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr $ Text.ShowColouredPrefix.showsPrefixInfo "quitting on request."

			return {-to IO-monad-} playState { State.PlayState.getMaybeApplicationTerminationReason = Just State.ApplicationTerminationReason.byRequest }
		onCommand (UI.Command.Report reportObject)	= do
			($ game) $ case reportObject of
				UI.ReportObject.AvailableMoves	-> putStrLn . ($ ".") . Text.ShowList.showsFormattedList (
					showChar '|'
				 ) (
					Notation.MoveNotation.showsNotation moveNotation
				 ) . Model.Game.findQualifiedMovesAvailableToNextPlayer
				UI.ReportObject.Board			-> putStrLn . show2D
				UI.ReportObject.EPD			-> putStrLn . Property.ExtendedPositionDescription.showEPD
				UI.ReportObject.FEN			-> putStrLn . Property.ForsythEdwards.showFEN
				UI.ReportObject.Game			-> print
				UI.ReportObject.MaxPositionInstances	-> print . State.InstancesByPosition.findMaximumInstances . Model.Game.getInstancesByPosition
				UI.ReportObject.Moves			-> putStrLn . showString (
					showString Component.Move.tag "s"
				 ) . Text.ShowList.showsAssociation . ($ ".") . Text.ShowList.showsFormattedList' (
					Notation.MoveNotation.showsNotation moveNotation
				 ) . Model.Game.listTurnsChronologically
				UI.ReportObject.PGN			-> putStrLn . ($ ".") <=< ContextualNotation.PGN.showsGame
				UI.ReportObject.ReversiblePlyCount	-> print . State.InstancesByPosition.countConsecutiveRepeatablePlies . Model.Game.getInstancesByPosition

			eventLoop
		onCommand UI.Command.Resign	= do
			Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr $ Text.ShowColouredPrefix.showsPrefixInfo "resigning."

			return {-to IO-monad-} $ State.PlayState.resign playState
		onCommand UI.Command.Restart	= do
			Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr $ Text.ShowColouredPrefix.showsPrefixInfo "restarting game."

			Data.Maybe.maybe (
				return {-to IO-monad-} ()
			 ) (
				\(filePath, _) -> Control.Exception.catch (
					do
						System.IO.withFile filePath System.IO.WriteMode (`System.IO.hPrint` (Data.Default.def :: Model.Game.Game T.X T.Y))

						Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixInfo . showString "the initial game-state has been saved in " $ shows filePath "."
				) $ \e -> System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixError $ show (e :: Control.Exception.SomeException)
			 ) $ Input.IOOptions.getMaybePersistence ioOptions

			return {-to IO-monad-} $ State.PlayState.resetPositionHashQuantifiedGameTree playState
		onCommand (UI.Command.RollBack maybeNPlies)	= let
			rollBack :: Component.Move.NPlies -> IO (State.PlayState.PlayState column criterionValue criterionWeight pieceSquareValue positionHash rankValue row weightedMean x y)
			rollBack nPlies
				| (game', _) : _ <- drop (pred nPlies) $ Model.Game.rollBack game	= do
					Control.Monad.when (verbosity == maxBound) . putStrLn $ show2D game'

					return {-to IO-monad-} $ State.PlayState.reconstructPositionHashQuantifiedGameTree game' playState
				| otherwise	= onCommand UI.Command.Restart
		 in Data.Maybe.maybe (
			let
				nPlies	= succ $ Data.Map.size searchDepthByLogicalColour	-- In fully manual play, rollback one ply, in semi-manual play rollback two plies.
			in do
				Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixInfo . showString "rolling-back " $ shows nPlies " plies."

				rollBack nPlies
		 ) rollBack maybeNPlies
		onCommand UI.Command.Save	= do
			Data.Maybe.maybe (
				return {-to IO-monad-} $ Text.ShowColouredPrefix.showsPrefixError "the file-path at which to save the game, hasn't been defined."
			 ) (
				\(filePath, automatic) -> if automatic
					then return {-to IO-monad-} $ Text.ShowColouredPrefix.showsPrefixWarning "the state of the game is, in accordance with configuration, saved automatically."
					else Control.Exception.catch (
						do
							System.IO.withFile filePath System.IO.WriteMode (`System.IO.hPutStrLn` show game)

							return {-to IO-monad-} . Text.ShowColouredPrefix.showsPrefixInfo . showString "the game-state has been saved in " $ shows filePath "."
					 ) $ \e -> return {-to IO-monad-} . Text.ShowColouredPrefix.showsPrefixError $ show (e :: Control.Exception.SomeException)
			 ) (
				Input.IOOptions.getMaybePersistence ioOptions
			 ) >>= System.IO.hPutStrLn System.IO.stderr

			eventLoop
		onCommand (UI.Command.Set setObject)
			| fullyManual	= do
				System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixWarning $ shows UI.Command.setTag " requires an automated opponent."

				return {-to IO-monad-} playState
			| otherwise	= Control.Exception.catchJust (
				\e -> if Data.Exception.isBadData e
					then Just $ show e
					else Nothing
			) (
				do
					Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixInfo . showString "setting " $ shows setObject "."

					readMove positionHashQualifiedMoveTree randomGen startUTCTime playState {
						State.PlayState.getOptions	= Control.DeepSeq.force $ case setObject of
							UI.SetObject.SearchDepth searchDepth	-> options {
								Input.Options.getSearchOptions	= Input.SearchOptions.setSearchDepth searchDepth $ Input.Options.getSearchOptions options
							}
					} -- Recurse.
			) (
				\s -> do
					Control.Monad.unless (verbosity == minBound) . System.IO.hPutStrLn System.IO.stderr $ Text.ShowColouredPrefix.showsPrefixError s

					eventLoop
			)
		onCommand UI.Command.Swap
			| fullyManual	= do
				Control.Monad.when (verbosity >= Data.Default.def) . System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixWarning . showString " there aren't any " $ shows Input.SearchOptions.searchDepthTag " to swap."

				eventLoop
			| otherwise	= do
				Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixInfo . showString "swapping " $ shows Input.SearchOptions.searchDepthTag "."

				return {-to IO-monad-} playState { State.PlayState.getOptions = Input.Options.swapSearchDepth options }

		eventLoop :: IO (State.PlayState.PlayState column criterionValue criterionWeight pieceSquareValue positionHash rankValue row weightedMean x y)
		eventLoop	= getLine >>= \line -> case Data.List.Extra.trim line of
			"?"	-> onCommand $ UI.Command.Print UI.PrintObject.Help
			':' : s	-> either (
				\errorMessage -> do
					System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixError $ showString errorMessage "."

					eventLoop
			 ) (
				\(command, remainder) -> if null $ Data.List.Extra.trimStart remainder
					then onCommand command
					else do
						System.IO.hPutStrLn System.IO.stderr . showString "unexpected trailing text " $ shows remainder "."

						eventLoop
			 ) . UI.Command.readsCommand $ UI.Command.autoComplete s
			s	-> let
				corrections	= State.PlayState.suggestCorrections s playState
			 in case Notation.MoveNotation.readsQualifiedMove moveNotation s of
				[(eitherQualifiedMove, "")]
					| Just errorMessage <- Model.Game.validateEitherQualifiedMove eitherQualifiedMove game	-> do
						System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixError . shows s . showString " is illegal; " $ shows errorMessage "."

						Control.Monad.unless (null corrections) . System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixInfo . showString "did you mean " $ shows corrections " ?"

						eventLoop	-- Recurse.
					| otherwise	-> do
						elapsedTime	<- Data.Time.measureElapsedTime startUTCTime

						let
							game'		= Model.Game.applyEitherQualifiedMove eitherQualifiedMove game
							playState'	= State.PlayState.updateWithManualMove game' playState

						Control.Monad.when (verbosity == maxBound) $ do
							System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixInfo . showString Component.Move.tag . Text.ShowList.showsAssociation . shows (
								Notation.MoveNotation.showNotation moveNotation . Data.Maybe.fromMaybe (
									Control.Exception.throw $ Data.Exception.mkResultUndefined "BishBosh.UI.Raw.readMove.eventLoop:\tModel.Game.maybeLastTurn failed."
								) $ Model.Game.maybeLastTurn game'
							 ) . showString " was requested after " $ Data.Time.showsTimeAsSeconds nDecimalDigits elapsedTime "."

							case ContextualNotation.PositionHashQualifiedMoveTree.findNextOnymousQualifiedMovesForPosition game' positionHashQualifiedMoveTree of
								[]			-> return {-to IO-monad-} ()
								onymousQualifiedMoves	-> System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixInfo . showString "matches archived game(s):" $ ContextualNotation.QualifiedMoveForest.showsNames maybeMaximumPGNNames (
									concatMap (
										map fst {-Name-} . snd {-[OnymousResult]-}
									) onymousQualifiedMoves
								 ) ""

						return {-to IO-monad-} playState'	-- It's now the other player's move.
				[(_, remainder)]		-> do
					System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixError . showString "the specified " . showString Component.Move.tag . showString " was correctly formatted, but was followed by unexpected text" . Text.ShowList.showsAssociation $ shows remainder "."

					eventLoop	-- Recurse.
				_ {-no parse-}			-> do
					Control.Monad.unless (null s) $ do
						System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixError . shows s . showString " /~ " $ Notation.MoveNotation.showsMoveSyntax moveNotation "."	-- CAVEAT: this error also results from source == destination.

						Control.Monad.unless (null corrections) . System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixInfo . showString "did you mean " $ shows corrections " ?"

					eventLoop	-- Recurse.
	 in do
		Control.Monad.unless (verbosity == minBound) . putStrLn . showString "Enter either a move in " . shows moveNotation . showString "-notation, or '?' for " $ showString UI.PrintObject.helpTag ":"

		eventLoop
 ) `either` (
	const . Control.Exception.throwIO $ Data.Exception.mkInvalidDatum "BishBosh.UI.Raw.readMove:\tunexpected CECP-options."
 ) $ Input.UIOptions.getEitherNativeUIOrCECPOptions uiOptions

-- | Plays the game.
takeTurns :: forall column criterionValue criterionWeight pieceSquareValue positionHash randomGen rankValue row weightedMean x y. (
	Control.DeepSeq.NFData					column,
#ifdef USE_PARALLEL
	Control.DeepSeq.NFData					criterionValue,
#endif
	Control.DeepSeq.NFData					criterionWeight,
	Control.DeepSeq.NFData					pieceSquareValue,
	Control.DeepSeq.NFData					rankValue,
	Control.DeepSeq.NFData					row,
	Control.DeepSeq.NFData					weightedMean,
	Control.DeepSeq.NFData					x,
	Control.DeepSeq.NFData					y,
	Data.Array.IArray.Ix					x,
#ifdef USE_UNBOXED_ARRAYS
	Data.Array.Unboxed.IArray Data.Array.Unboxed.UArray	pieceSquareValue,	-- Requires 'FlexibleContexts'. The unboxed representation of the array-element must be defined (& therefore must be of fixed size).
#endif
	Data.Bits.Bits						positionHash,
	Fractional						criterionValue,
	Fractional						pieceSquareValue,
	Fractional						rankValue,
	Fractional						weightedMean,
	Integral						column,
	Integral						x,
	Integral						y,
	Ord							positionHash,
	Read							x,
	Read							y,
	Real							criterionValue,
	Real							criterionWeight,
	Real							pieceSquareValue,
	Real							rankValue,
	Real							weightedMean,
	Show							column,
	Show							pieceSquareValue,
	Show							row,
	Show							x,
	Show							y,
	System.Random.RandomGen					randomGen
 )
	=> ContextualNotation.PositionHashQualifiedMoveTree.PositionHashQualifiedMoveTree x y positionHash
	-> randomGen
	-> State.PlayState.PlayState column criterionValue criterionWeight pieceSquareValue positionHash rankValue row weightedMean x y
	-> IO (State.PlayState.PlayState column criterionValue criterionWeight pieceSquareValue positionHash rankValue row weightedMean x y)
{-# SPECIALISE takeTurns :: (
	Control.DeepSeq.NFData	column,
	Control.DeepSeq.NFData	row,
	Integral		column,
	Show			column,
	Show			row,
	System.Random.RandomGen	randomGen
 )
	=> ContextualNotation.PositionHashQualifiedMoveTree.PositionHashQualifiedMoveTree T.X T.Y T.PositionHash
	-> randomGen
	-> State.PlayState.PlayState column T.CriterionValue T.CriterionWeight T.PieceSquareValue T.PositionHash T.RankValue row T.WeightedMean T.X T.Y
	-> IO (State.PlayState.PlayState column T.CriterionValue T.CriterionWeight T.PieceSquareValue T.PositionHash T.RankValue row T.WeightedMean T.X T.Y)
 #-}
takeTurns positionHashQualifiedMoveTree randomGen playState	= let
	options	= State.PlayState.getOptions playState

	(searchOptions, ioOptions)	= Input.Options.getSearchOptions &&& Input.Options.getIOOptions $ options

	uiOptions	= Input.IOOptions.getUIOptions ioOptions

	moveNotation	= Input.UIOptions.getMoveNotation uiOptions
	nDecimalDigits	= Input.UIOptions.getNDecimalDigits uiOptions
	verbosity	= Input.UIOptions.getVerbosity uiOptions
 in (
	\nativeUIOptions -> do
		mVar	<- Control.Concurrent.newEmptyMVar

		let
			show2D :: Model.Game.Game x y -> String
			show2D	= uncurry State.MaybePieceByCoordinates.show2D (
				snd {-columns-} . Input.NativeUIOptions.getBoardMagnification &&& Input.NativeUIOptions.getColourScheme $ nativeUIOptions
			 ) (
				Notation.MoveNotation.getOrigin moveNotation
			 ) . State.Board.getMaybePieceByCoordinates . Model.Game.getBoard

			slave
				:: Maybe (Concurrent.Pondering.Pondering (Component.Move.Move x y))
				-> Maybe Component.Move.NPlies
				-> [randomGen]
				-> State.PlayState.PlayState column criterionValue criterionWeight pieceSquareValue positionHash rankValue row weightedMean x y
				-> IO (State.PlayState.PlayState column criterionValue criterionWeight pieceSquareValue positionHash rankValue row weightedMean x y)
			slave maybePondering maybeMaximumPlies ~(randomGen' : randomGens) playState'	= let
				(game', searchOptions')		= State.PlayState.getGame &&& Input.Options.getSearchOptions . State.PlayState.getOptions $ playState'	-- Deconstruct.
			 in Data.Maybe.maybe (
				do
					startUTCTime	<- Data.Time.Clock.getCurrentTime

					Data.Maybe.maybe (
						do
							playState''	<- readMove positionHashQualifiedMoveTree randomGen' startUTCTime playState'	-- Block reading a command-sequence terminating in a move from a manual player.

							(,) playState'' `fmap` (
								if playState' `State.PlayState.hasMorePlies` playState''
									then {-rolled-back-} Data.Maybe.maybe (
										return {-to IO-monad-} Nothing
									) $ \pondering -> do
										Concurrent.Pondering.abort mVar pondering >>= Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr . Text.ShowPrefix.showsPrefixInfo . showString "pondering invalidated by roll-back => "

										return {-to IO-monad-} Nothing	-- Pondering has been terminated.
									else return {-to IO-monad-}
							 ) maybePondering
					 ) (
						\searchDepth' -> Data.Maybe.maybe (
							do
								Control.Monad.when (
									verbosity > Data.Default.def && not (
										ContextualNotation.PositionHashQualifiedMoveTree.isTerminal positionHashQualifiedMoveTree
									)
								 ) . System.IO.hPutStrLn System.IO.stderr $ Text.ShowColouredPrefix.showsPrefixInfo "failed to find any suitable archived move."

								let
									search ss	= Control.Monad.Reader.runReader (Search.Search.search searchDepth' ss) searchOptions'
									searchResult	= search $ State.PlayState.getSearchState playState'

								Data.Maybe.maybe (
									return {-to IO-monad-} searchResult	-- Pondering hasn't been configured, so the search must be evaluated.
								 ) (
									\pondering -> if Data.Maybe.maybe False (
										(== Concurrent.Pondering.getPremise pondering) . Component.QualifiedMove.getMove . Component.Turn.getQualifiedMove
									) $ Model.Game.maybeLastTurn game'	-- Confirm whether the pondering initiated at the start of the opponent's turn, was founded on the move they eventually made.
										then do
											Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr $ Text.ShowPrefix.showsPrefixInfo "move-premise validated => waiting."

											Control.Concurrent.takeMVar mVar	-- Blocking read, while the pondering terminates.
										else do
											Concurrent.Pondering.abort mVar pondering >>= Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr . Text.ShowPrefix.showsPrefixInfo . showString "pondering invalidated by incorrect move-premise => "

											return {-to IO-monad-} searchResult	-- Pondering wasn't well-founded, so the search must be evaluated.
								 ) maybePondering >>= (
									\searchResult' -> let
										searchState'	= Search.Search.getSearchState searchResult'
									in case Search.Search.getQuantifiedGames searchResult' of
										quantifiedGame : continuation {-optimal move-sequence-}	-> let
											bestTurn	= Evaluation.QuantifiedGame.getLastTurn quantifiedGame
											showsMove	= Notation.MoveNotation.showsNotation moveNotation bestTurn
										 in do
											elapsedTime	<- bestTurn `seq` Data.Time.measureElapsedTime startUTCTime

											if verbosity == minBound
												then putStrLn $ showsMove ""	-- CAVEAT: potentially machine-interpreted.
												else do
													System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixInfo . (
														if verbosity == maxBound
															then Property.ShowFloat.showsFloatToN nDecimalDigits (
																Control.Monad.Reader.runReader (
																	Evaluation.Fitness.evaluateFitness Nothing game'
																) $ Input.Options.getEvaluationOptions options	:: Attribute.WeightedMeanAndCriterionValues.WeightedMeanAndCriterionValues weightedMean criterionValue
															) . Search.Search.showsSeparator	-- Prepend the fitness of the original game prior to the new result.
															else id
													 ) $ (
														if verbosity > Data.Default.def
															then Notation.MoveNotation.showsNotationFloatToNDecimals moveNotation nDecimalDigits searchResult' . showString " in " . Data.Time.showsTimeAsSeconds nDecimalDigits elapsedTime
															else showsMove
													 ) "."

													putStrLn . show2D $ Evaluation.QuantifiedGame.getGame quantifiedGame

											(,) (
												State.PlayState.updateWithAutomaticMove (
													Attribute.WeightedMeanAndCriterionValues.getCriterionValues $ Evaluation.QuantifiedGame.getWeightedMeanAndCriterionValues quantifiedGame
												) searchState' playState'
											 ) `fmap` if Input.SearchOptions.getUsePondering searchOptions
												then case continuation of
													quantifiedGame' {-1st move after ours in optimal move-sequence-} : _	-> fmap Just . (
														\positionHashQuantifiedGameTree'' -> Concurrent.Pondering.ponder (
															Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr . Text.ShowPrefix.showsPrefixInfo
														) (
															showString "move-premise" . Text.ShowList.showsAssociation . ($ ".") . Notation.MoveNotation.showsNotation moveNotation &&& Component.QualifiedMove.getMove . Component.Turn.getQualifiedMove $ Evaluation.QuantifiedGame.getLastTurn quantifiedGame'
														) (
															search searchState' { Search.SearchState.getPositionHashQuantifiedGameTree = positionHashQuantifiedGameTree'' }
														) mVar
													 ) . Data.Maybe.fromMaybe (
														Control.Exception.throw $ Data.Exception.mkIncompatibleData "BishBosh.UI.Raw.takeTurns.slave:\tData.RoseTree.reduce failed."
													 ) . Evaluation.PositionHashQuantifiedGameTree.reduce (
														(== Evaluation.QuantifiedGame.getLastTurn quantifiedGame') . Evaluation.QuantifiedGame.getLastTurn . Evaluation.PositionHashQuantifiedGameTree.getQuantifiedGame
													 ) $ Search.SearchState.getPositionHashQuantifiedGameTree searchState'
													_		-> return {-to IO-monad-} Nothing
												else return {-to IO-monad-} Nothing
										_	-> Control.Exception.throwIO . Data.Exception.mkRequestFailure . showString "BishBosh.UI.Raw.takeTurns.slave:\tunexpectedly failed to find any future moves; " $ shows game' "."	-- A gameTerminationReason should have been defined.
								 )
						 ) (
							\(qualifiedMove, names) -> do
								elapsedTime	<- Data.Time.measureElapsedTime startUTCTime

								Data.Maybe.maybe (
									return {-to IO-monad-} ()
								 ) (
									Concurrent.Pondering.abort mVar >=> Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr . Text.ShowPrefix.showsPrefixInfo . showString "pondering pre-empted by standard-opening match => "
								 ) maybePondering

								let selectedGame	= Model.Game.applyQualifiedMove qualifiedMove game'

								if verbosity == minBound
									then putStrLn $ Notation.MoveNotation.showsNotation moveNotation qualifiedMove ""	-- CAVEAT: potentially machine-interpreted.

									else do
										Control.Monad.when (verbosity > Data.Default.def) . System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixInfo . showString "selected " . Notation.MoveNotation.showsNotation moveNotation qualifiedMove . showString " from:" . ContextualNotation.QualifiedMoveForest.showsNames (
											Input.IOOptions.getMaybeMaximumPGNNames ioOptions
										 ) names . showString "\n\tin " $ Data.Time.showsTimeAsSeconds nDecimalDigits elapsedTime "."

										putStrLn $ show2D selectedGame

								return {-to IO-monad-} (State.PlayState.updateWithManualMove selectedGame playState', Nothing)	-- N.B.: one could ponder, but would have to construct a game-tree, & the chance of a subsequent standard-opening move is high.
						 ) $ ContextualNotation.PositionHashQualifiedMoveTree.maybeRandomlySelectOnymousQualifiedMove randomGen' (
							Input.StandardOpeningOptions.getMatchSwitches $ Input.SearchOptions.getStandardOpeningOptions searchOptions
						 ) game' positionHashQualifiedMoveTree	-- Determine whether the automated player's move can be decided by a search of recorded games or we must decide ourself.
					 ) (
						Model.Game.getNextLogicalColour game' `Data.Map.lookup` Input.SearchOptions.getSearchDepthByLogicalColour searchOptions'	-- Determinate whether the next player is manual.
					 ) >>= (
						\(playState'', maybePondering') -> do
							Data.Maybe.maybe (
								return {-to IO-monad-} ()
							 ) (
								\(filePath, automatic) -> let
									game''	= State.PlayState.getGame playState''
								in Control.Monad.when automatic . Control.Exception.catch (
									do
										System.IO.withFile filePath System.IO.WriteMode (`System.IO.hPrint` game'')

										Control.Monad.when (verbosity == maxBound) . System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixInfo . showString "the game-state has been saved in " $ shows filePath "."
								) $ \e -> System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixError $ show (e :: Control.Exception.SomeException)
							 ) $ Input.IOOptions.getMaybePersistence ioOptions

							if State.PlayState.hasApplicationTerminationBeenRequested playState''
								then return {-to IO-monad-} playState''
								else if Data.Maybe.maybe False (<= 1) maybeMaximumPlies
									then return {-to IO-monad-} playState'' { State.PlayState.getMaybeApplicationTerminationReason = Just State.ApplicationTerminationReason.maximumPlies }
									else slave maybePondering' (fmap pred maybeMaximumPlies) randomGens playState''	-- Tail recurse.
					 )
			 ) (
				\gameTerminationReason -> if State.PlayState.hasApplicationTerminationBeenRequested playState'
					then return {-to IO-monad-} playState'
					else {-don't terminate the application-} do
						putStrLn $ if verbosity == minBound
							then show gameTerminationReason	-- CAVEAT: potentially machine-interpreted.
							else Text.ShowColouredPrefix.showsPrefixInfo $ shows gameTerminationReason "."

						let
							criterionValueStatistics	= State.PlayState.calculateCriterionValueStatistics playState'

							showsFloat :: Double -> ShowS
							showsFloat	= Numeric.showFFloat $ Just nDecimalDigits

						Control.Monad.when (
							verbosity == maxBound && not (null criterionValueStatistics)
						 ) . System.IO.hPutStrLn System.IO.stderr . Text.ShowColouredPrefix.showsPrefixInfo . showString "mean & standard-deviation of criterion-values" . Text.ShowList.showsAssociation $ Text.ShowList.showsFormattedList' (
							\(mean, standardDeviation) -> showChar '(' . showsFloat mean . Text.ShowList.showsSeparator . showsFloat standardDeviation . showChar ')'
						 ) criterionValueStatistics "."

						readMove positionHashQualifiedMoveTree randomGen' (
							Control.Exception.throw $ Data.Exception.mkInvalidDatum "BishBosh.UI.Raw.takeTurns.slave:\tundefined startUTCTime."
						 ) playState' {-there're zero valid moves, but the user can issue commands-} >>= slave Nothing maybeMaximumPlies randomGens	-- Tail recurse.

			 ) $ Model.Game.getMaybeTerminationReason game'

		Control.Monad.unless (verbosity == minBound) . putStrLn . show2D $ State.PlayState.getGame playState

		slave Nothing (Input.Options.getMaybeMaximumPlies options) (ToolShed.System.Random.randomGens randomGen) playState
 ) `either` (
	const . Control.Exception.throwIO $ Data.Exception.mkInvalidDatum "BishBosh.UI.Raw.takeTurns:\tunexpected CECP-options."
 ) $ Input.UIOptions.getEitherNativeUIOrCECPOptions uiOptions