Как сгенерировать значение, чтобы оно отражалось как элемент другого сгенерированного значения?

Как сгенерировать значение, чтобы оно отражалось как элемент другого сгенерированного значения?

Например, возьмите следующий код:

type Space =
    | Occupied  of Piece
    | Available of Coordinate

// Setup
let pieceGen =       Arb.generate<Piece> 
let destinationGen = Arb.generate<Space>
let positionsGen =   Arb.generate<Space list>

Я хочу, чтобы positionGen включал значения, полученные от pieceGen и spaceGen. Однако я понятия не имею, как это сделать.

Чтобы добавить контекст к моему вопросу, мой список позиций (иначе шахматная доска) должен содержать как сгенерированную фигуру, так и сгенерированный пункт назначения в своем списке.

Вот мой тест:

[<Property(QuietOnSuccess = true, MaxTest=10000)>]
let ``moving checker retains set count`` () =

    // Setup
    let pieceGen =       Arb.generate<Piece> 
    let destinationGen = Arb.generate<Space>
    let positionsGen =   Arb.generate<Space list>
    let statusGen =      Arb.generate<Status>

    // Test
    Gen.map4 (fun a b c d -> a,b,c,d) pieceGen destinationGen positionsGen statusGen
    |> Arb.fromGen
    |> Prop.forAll 
    <| fun (piece , destination , positions , status) -> (positions, status) 
                                                         |> move piece destination
                                                         |> getPositions
                                                         |> List.length = positions.Length

Приложение:

(* Types *)
type Black = BlackKing | BlackSoldier
type Red =   RedKing   | RedSoldier

type Coordinate = int * int

type Piece =
    | Black of Black * Coordinate
    | Red   of Red   * Coordinate

type Space =
    | Occupied  of Piece
    | Available of Coordinate

type Status =
    | BlacksTurn | RedsTurn
    | BlackWins  | RedWins

(* Private *)
let private black coordinate = Occupied (Black (BlackSoldier , coordinate))
let private red   coordinate = Occupied (Red   (RedSoldier   , coordinate))

let private getPositions (positions:Space list, status:Status) = positions

let private yDirection = function
    | Black _ -> -1
    | Red   _ ->  1

let private toAvailable = function
    | Available pos -> true
    | _             -> false

let private available positions = positions |> List.filter toAvailable

let private availableSelection = function
    | Available pos -> Some pos
    | Occupied _   -> None

let private availablePositions positions = 
    positions |> List.filter toAvailable
              |> List.choose availableSelection

let private getCoordinate = function
    | Available xy -> Some xy
    | _            -> None

let private coordinateOf = function
    | Black (checker , pos) -> pos
    | Red   (checker , pos) -> pos

let private optionsForSoldier piece = 

    let (sourceX , sourceY) = coordinateOf piece

    (fun pos -> pos = ((sourceX - 1) , (sourceY + (piece |> yDirection) )) ||
                pos = ((sourceX + 1) , (sourceY + (piece |> yDirection) )))

let private optionsForKing piece = 

    let (sourceX , sourceY) = coordinateOf piece

    (fun pos -> pos = ((sourceX - 1) , (sourceY + 1 )) ||
                pos = ((sourceX + 1) , (sourceY + 1 )) ||
                pos = ((sourceX - 1) , (sourceY - 1 )) ||
                pos = ((sourceX + 1) , (sourceY - 1 )))

let private jumpOptions (sourceX , sourceY) space =
    match space with
    | Occupied p -> match p with
                     | Red   (ch,xy) -> xy = (sourceX + 1, sourceY - 1) ||
                                        xy = (sourceX - 1, sourceY - 1)

                     | Black (ch,xy) -> xy = (sourceX + 1, sourceY + 1) ||
                                        xy = (sourceX - 1, sourceY + 1)
    | _ -> false

let private jumpsForSoldier piece positions =
    match piece with
    | Black (ch,pos) -> positions |> List.filter (jumpOptions (coordinateOf piece))
    | Red   (ch,pos) -> positions |> List.filter (jumpOptions (coordinateOf piece))

let private isKing piece = 
    match piece with
    | Black (checker , _) -> match checker with
                             | BlackSoldier -> false
                             | BlackKing    -> true

    | Red   (checker , _) -> match checker with
                             | RedSoldier   -> false
                             | RedKing      -> true

let private filterOut a b positions =
    positions |> List.filter(fun x -> x <> a && x <> b)

let private movePiece destination positions piece =

    let destinationXY = 
        match destination with
        | Available xy -> xy
        | Occupied p  -> coordinateOf p

    let yValueMin , yValueMax = 0 , 7

    let canCrown =
        let yValue = snd destinationXY
        (yValue = yValueMin || 
         yValue = yValueMax) && 
         not (isKing piece)

    match positions |> List.find (fun space -> space = Occupied piece) with
    | Occupied (Black (ch, xy)) -> 
        let checkerType = if canCrown then BlackKing else BlackSoldier
        Available(xy) :: (Occupied(Black(checkerType, destinationXY)))
                      :: (positions |> filterOut (Occupied (Black(ch, xy))) destination)     

    | Occupied (Red   (ch, xy)) -> 
        let checkerType = if canCrown then RedKing else RedSoldier
        Available(xy) :: (Occupied(Red(checkerType, destinationXY)))
                      :: (positions |> filterOut (Occupied (Red(ch, xy))) destination) 
    | _ -> positions

(* Public *)
let startGame () =
    [ red (0,0);  red (2,0);  red (4,0);  red (6,0)
      red (1,1);  red (3,1);  red (5,1);  red (7,1)
      red (0,2);  red (2,2);  red (4,2);  red (6,2)

      Available (1,3); Available (3,3); Available (5,3); Available (7,3)
      Available (0,4); Available (2,4); Available (4,4); Available (6,4)

      black (1,5);  black (3,5);  black (5,5);  black (7,5)
      black (0,6);  black (2,6);  black (4,6);  black (6,6)
      black (1,7);  black (3,7);  black (5,7);  black (7,7) ] , BlacksTurn

let optionsFor piece positions =

    let sourceX , sourceY = coordinateOf piece

    match piece |> isKing with
    | false -> positions |> availablePositions 
                         |> List.filter (optionsForSoldier piece)

    | true ->  positions |> availablePositions 
                         |> List.filter (optionsForKing piece)

let move piece destination (positions,status) =

    let currentStatus = match status with
                        | BlacksTurn -> RedsTurn
                        | RedsTurn   -> BlacksTurn
                        | BlackWins  -> BlackWins
                        | RedWins    -> RedWins

    let canProceed =  match piece with
                      | Red   _ -> currentStatus = RedsTurn  
                      | Black _ -> currentStatus = BlacksTurn

    if not canProceed then (positions , currentStatus)
    else let options   = optionsFor piece positions
         let canMoveTo = (fun target -> options |> List.exists (fun xy -> xy = target))

         match getCoordinate destination with
         | Some target -> if canMoveTo target then
                             let updatedBoard = ((positions , piece) ||> movePiece destination)
                             (updatedBoard , currentStatus)

                          else (positions , currentStatus)
         | None -> (positions , currentStatus)

let jump target positions source =

    let canJump = 
        positions |> jumpsForSoldier source
                  |> List.exists (fun s -> match s with
                                           | Occupied target -> true
                                           | _                -> false)

    let (|NorthEast|NorthWest|SouthEast|SouthWest|Origin|) (origin , barrier) =

        let (sourceX  , sourceY) =  origin
        let (barrierX , barrierY) = barrier

        if   barrierY = sourceY + 1 &&
             barrierX = sourceX - 1
        then SouthWest

        elif barrierY = sourceY + 1 &&
             barrierX = sourceX + 1 
        then SouthEast

        elif barrierY = sourceY - 1 &&
             barrierX = sourceX - 1
        then NorthWest

        elif barrierY = sourceY - 1 &&
             barrierX = sourceX + 1
        then NorthEast

        else Origin

    let jumpToPostion origin barrier =

        let (sourceX  , sourceY) =  origin
        let (barrierX , barrierY) = barrier

        match (origin , barrier) with
        | SouthWest -> (barrierX + 1, barrierY - 1)
        | SouthEast -> (barrierX + 1, barrierY + 1)
        | NorthWest -> (barrierX - 1, barrierY - 1)
        | NorthEast -> (barrierX - 1, barrierY + 1)
        | Origin    -> origin

    if canJump then
        let destination = Available (jumpToPostion (coordinateOf source) (coordinateOf target))
        let result = (positions, source) ||> movePiece destination
                                          |> List.filter (fun s -> s <> Occupied target)
        Available (coordinateOf target)::result
    else positions

person Scott Nimrod    schedule 09.08.2016    source источник


Ответы (1)


Как объяснялось в предыдущем ответе, вы можете использовать выражение вычисления gen для выражения более сложных генераторов.

В этом конкретном примере вы заявляете, что вам нужно positionsGen, чтобы включить значения, созданные pieceGen и spaceGen. Вы можете сделать это следующим образом:

[<Property(QuietOnSuccess = true, MaxTest=10000)>]
let ``moving checker retains set count`` () =
    gen {
        let! piece = Arb.generate<Piece>
        let! destination = Arb.generate<Space>

        let! otherPositions = Arb.generate<Space list>
        let! positions =
            Occupied piece :: destination :: otherPositions |> Gen.shuffle

        let! status = Arb.generate<Status>
        return piece, destination, positions |> Array.toList, status }
    |> Arb.fromGen
    |> Prop.forAll
    // ... the rest of the test goes here...

Выражение вычисления начинается с создания piece и destination. Из-за использования let! в вычислительном выражении, внутри этого контекста, они являются нормальными значениями Piece и Space и могут рассматриваться как таковые.

Затем выражение использует let! для «генерации» значения Space list, которое будет содержать другие значения (если таковые имеются; сгенерированный список может быть пустым).

Это дает вам все строительные блоки, необходимые для создания списка, содержащего как минимум два нужных значения, а также другие значения. Чтобы создать такой список, вы можете добавить (::) два «известных» значения в список, а затем перемешать результат для большей точности.

Последнее выражение в вычислительном выражении gen возвращает четырехэлементный кортеж. Тип этого выражения — Gen<Piece * Space * Space list * Status>. Его можно превратить в Arbitrary<Piece * Space * Space list * Status> с помощью Arb.fromGen, а затем передать в Prop.forAll.

Это устраняет проблему, связанную с тем, что свойство moving checker retains set count создает внутренние исключения.


Это, кстати, демонстрирует, что свойство фальсифицируемо:

Test 'Ploeh.StackOverflow.Q38857462.Properties.moving checker retains set count' failed: FsCheck.Xunit.PropertyFailedException : 
Falsifiable, after 70 tests (0 shrinks) (StdGen (1318556550,296190265)):
Original:
<null>
(Black (BlackKing,(-1, 1)), Available (0, 0),
 [Occupied (Red (RedSoldier,(-1, 0))); Available (0, 0);
  Occupied (Black (BlackKing,(-1, 1))); Available (0, 0)], RedsTurn)

Другой вопрос, проблема ли это в тесте или в реализации...

person Mark Seemann    schedule 09.08.2016
comment
Это тестирование, основанное на свойствах, и раздражает, и восхищает одновременно. FsCheck находит пробелы в моей логике, о которых я никогда не знал. Большое спасибо! - person Scott Nimrod; 09.08.2016
comment
@ScottNimrod Я надеюсь, что в какой-то момент вы преодолеете раздражение - я просто нахожу это потрясающим :) - person Mark Seemann; 09.08.2016