F# 中的 RogueLike 非常简单,使其更加“实用”

发布于 2024-10-08 17:44:07 字数 5541 浏览 7 评论 0原文

我有一些现有的 C# 代码,用于一个非常非常简单的 RogueLike 引擎。这是故意天真的,因为我试图尽可能简单地做最少的事情。它所做的只是使用箭头键和 System.Console 在硬编码地图上移动 @ 符号:

//define the map
var map = new List<string>{
  "                                        ",
  "                                        ",
  "                                        ",
  "                                        ",
  "    ###############################     ",
  "    #                             #     ",
  "    #         ######              #     ",
  "    #         #    #              #     ",
  "    #### #### #    #              #     ",
  "       # #  # #    #              #     ",
  "       # #  # #    #              #     ",
  "    #### #### ######              #     ",
  "    #              =              #     ",
  "    #              =              #     ",
  "    ###############################     ",
  "                                        ",
  "                                        ",
  "                                        ",
  "                                        ",
  "                                        "
};

//set initial player position on the map
var playerX = 8;
var playerY = 6;

//clear the console
Console.Clear();

//send each row of the map to the Console
map.ForEach( Console.WriteLine );

//create an empty ConsoleKeyInfo for storing the last key pressed
var keyInfo = new ConsoleKeyInfo( );

//keep processing key presses until the player wants to quit
while ( keyInfo.Key != ConsoleKey.Q ) {
  //store the player's current location
  var oldX = playerX;
  var oldY = playerY;

  //change the player's location if they pressed an arrow key
  switch ( keyInfo.Key ) {
    case ConsoleKey.UpArrow:
      playerY--;
      break;
    case ConsoleKey.DownArrow:
      playerY++;
      break;
    case ConsoleKey.LeftArrow:
      playerX--;
      break;
    case ConsoleKey.RightArrow:
      playerX++;
      break;
  }

  //check if the square that the player is trying to move to is empty
  if( map[ playerY ][ playerX ] == ' ' ) {
    //ok it was empty, clear the square they were standing on before
    Console.SetCursorPosition( oldX, oldY );
    Console.Write( ' ' );
    //now draw them at the new square
    Console.SetCursorPosition( playerX, playerY );
    Console.Write( '@' );
  } else {
    //they can't move there, change their location back to the old location
    playerX = oldX;
    playerY = oldY;
  }

  //wait for them to press a key and store it in keyInfo
  keyInfo = Console.ReadKey( true );
}

我在 F# 中尝试这样做,最初我尝试使用函数概念来编写它,但结果证明我有点过度了我的头脑,所以我几乎做了一个直接移植 - 它不是真正一个F#程序(尽管它编译并运行)它是一个用F#语法编写的程序程序:

open System

//define the map
let map = [ "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "    ###############################     ";
            "    #                             #     ";
            "    #         ######              #     ";
            "    #         #    #              #     ";
            "    #### #### #    #              #     ";
            "       # #  # #    #              #     ";
            "       # #  # #    #              #     ";
            "    #### #### ######              #     ";
            "    #              =              #     ";
            "    #              =              #     ";
            "    ###############################     ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        " ]

//set initial player position on the map
let mutable playerX = 8
let mutable playerY = 6

//clear the console
Console.Clear()

//send each row of the map to the Console
map |> Seq.iter (printfn "%s")

//create an empty ConsoleKeyInfo for storing the last key pressed
let mutable keyInfo = ConsoleKeyInfo()

//keep processing key presses until the player wants to quit
while not ( keyInfo.Key = ConsoleKey.Q ) do
    //store the player's current location
    let mutable oldX = playerX
    let mutable oldY = playerY

    //change the player's location if they pressed an arrow key
    if keyInfo.Key = ConsoleKey.UpArrow then
        playerY <- playerY - 1
    else if keyInfo.Key = ConsoleKey.DownArrow then
        playerY <- playerY + 1
    else if keyInfo.Key = ConsoleKey.LeftArrow then
        playerX <- playerX - 1
    else if keyInfo.Key = ConsoleKey.RightArrow then
        playerX <- playerX + 1

    //check if the square that the player is trying to move to is empty
    if map.Item( playerY ).Chars( playerX ) = ' ' then
        //ok it was empty, clear the square they were standing on
        Console.SetCursorPosition( oldX, oldY )
        Console.Write( ' ' )
        //now draw them at the new square 
        Console.SetCursorPosition( playerX, playerY )
        Console.Write( '@' )
    else
        //they can't move there, change their location back to the old location
        playerX <- oldX
        playerY <- oldY

    //wait for them to press a key and store it in keyInfo
    keyInfo <- Console.ReadKey( true )

所以我的问题是,我需要什么学习以便更实用地重写这个,你能给我一些提示,一个模糊的概述,诸如此类的事情吗?

我更喜欢朝着正确的方向前进,而不是仅仅看到一些代码,但如果这是你向我解释它的最简单的方法,那么很好,但在这种情况下,你能否也解释一下“为什么”而不是“如何” ” 的?

I have some existing C# code for a very, very simple RogueLike engine. It is deliberately naive in that I was trying to do the minimum amount as simply as possible. All it does is move an @ symbol around a hardcoded map using the arrow keys and System.Console:

//define the map
var map = new List<string>{
  "                                        ",
  "                                        ",
  "                                        ",
  "                                        ",
  "    ###############################     ",
  "    #                             #     ",
  "    #         ######              #     ",
  "    #         #    #              #     ",
  "    #### #### #    #              #     ",
  "       # #  # #    #              #     ",
  "       # #  # #    #              #     ",
  "    #### #### ######              #     ",
  "    #              =              #     ",
  "    #              =              #     ",
  "    ###############################     ",
  "                                        ",
  "                                        ",
  "                                        ",
  "                                        ",
  "                                        "
};

//set initial player position on the map
var playerX = 8;
var playerY = 6;

//clear the console
Console.Clear();

//send each row of the map to the Console
map.ForEach( Console.WriteLine );

//create an empty ConsoleKeyInfo for storing the last key pressed
var keyInfo = new ConsoleKeyInfo( );

//keep processing key presses until the player wants to quit
while ( keyInfo.Key != ConsoleKey.Q ) {
  //store the player's current location
  var oldX = playerX;
  var oldY = playerY;

  //change the player's location if they pressed an arrow key
  switch ( keyInfo.Key ) {
    case ConsoleKey.UpArrow:
      playerY--;
      break;
    case ConsoleKey.DownArrow:
      playerY++;
      break;
    case ConsoleKey.LeftArrow:
      playerX--;
      break;
    case ConsoleKey.RightArrow:
      playerX++;
      break;
  }

  //check if the square that the player is trying to move to is empty
  if( map[ playerY ][ playerX ] == ' ' ) {
    //ok it was empty, clear the square they were standing on before
    Console.SetCursorPosition( oldX, oldY );
    Console.Write( ' ' );
    //now draw them at the new square
    Console.SetCursorPosition( playerX, playerY );
    Console.Write( '@' );
  } else {
    //they can't move there, change their location back to the old location
    playerX = oldX;
    playerY = oldY;
  }

  //wait for them to press a key and store it in keyInfo
  keyInfo = Console.ReadKey( true );
}

I was playing around with doing it in F#, initially I was trying to write it using functional concepts, but turned out I was a bit over my head, so I did pretty much a straight port - it's not really an F# program (though it compiles and runs) it's a procedural program written in F# syntax:

open System

//define the map
let map = [ "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "    ###############################     ";
            "    #                             #     ";
            "    #         ######              #     ";
            "    #         #    #              #     ";
            "    #### #### #    #              #     ";
            "       # #  # #    #              #     ";
            "       # #  # #    #              #     ";
            "    #### #### ######              #     ";
            "    #              =              #     ";
            "    #              =              #     ";
            "    ###############################     ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        " ]

//set initial player position on the map
let mutable playerX = 8
let mutable playerY = 6

//clear the console
Console.Clear()

//send each row of the map to the Console
map |> Seq.iter (printfn "%s")

//create an empty ConsoleKeyInfo for storing the last key pressed
let mutable keyInfo = ConsoleKeyInfo()

//keep processing key presses until the player wants to quit
while not ( keyInfo.Key = ConsoleKey.Q ) do
    //store the player's current location
    let mutable oldX = playerX
    let mutable oldY = playerY

    //change the player's location if they pressed an arrow key
    if keyInfo.Key = ConsoleKey.UpArrow then
        playerY <- playerY - 1
    else if keyInfo.Key = ConsoleKey.DownArrow then
        playerY <- playerY + 1
    else if keyInfo.Key = ConsoleKey.LeftArrow then
        playerX <- playerX - 1
    else if keyInfo.Key = ConsoleKey.RightArrow then
        playerX <- playerX + 1

    //check if the square that the player is trying to move to is empty
    if map.Item( playerY ).Chars( playerX ) = ' ' then
        //ok it was empty, clear the square they were standing on
        Console.SetCursorPosition( oldX, oldY )
        Console.Write( ' ' )
        //now draw them at the new square 
        Console.SetCursorPosition( playerX, playerY )
        Console.Write( '@' )
    else
        //they can't move there, change their location back to the old location
        playerX <- oldX
        playerY <- oldY

    //wait for them to press a key and store it in keyInfo
    keyInfo <- Console.ReadKey( true )

So my question is, what do I need to learn in order to rewrite this more functionally, can you give me some hints, a vague overview, that kind of thing.

I'd prefer a shove in the right direction rather than just seeing some code, but if that's the easiest way for you to explain it to me then fine, but in that case can you please also explain the "why" rather the "how" of it?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

调妓 2024-10-15 17:44:08

这是一个不错的小游戏:-)。在函数式编程中,您希望避免使用可变状态(正如其他人指出的那样),并且您还希望将游戏的核心编写为没有任何副作用的函数(例如从控制台读取和写作)。

游戏的关键部分是控制位置的函数。您可以重构代码,使其具有具有类型签名的函数:

val getNextPosition : (int * int) -> ConsoleKey -> option<int * int>

如果游戏退出,该函数将返回 None。否则,它将返回 Some(posX, posY),其中 posXposY@ 符号的新位置。通过进行更改,您将获得一个很好的功能核心,并且函数 getNextPosition 也很容易测试(因为它总是针对相同的输入返回相同的结果)。

要使用该函数,最好的选择是使用递归编写循环。主函数的结构如下所示:

let rec playing pos =
  match getNextPosition pos (Console.ReadKey()) with
  | None -> () // Quit the game
  | Some(newPos) ->
     // This function redraws the screen (this is a side-effect,
     // but it is localized to a single function)
     redrawScreen pos newPos
     playing newPos

That's a nice little game :-). In functional programming, you'd want to avoid using mutable state (as others pointed out) and you'd also want to write the core of your game as a function that doesn't have any side-effects (e.g. reading from console and writing).

The key part of the game is the function that controls the position. You could refactor your code to have a function with the type signature:

val getNextPosition : (int * int) -> ConsoleKey -> option<int * int>

The function returns None if the game should quit. Otherwise it returns Some(posX, posY) where posX and posY are your new locations for the @ symbol. By doing the change, you get a nice functional core and the function getNextPosition is also easy to test (because it always returns the same result for the same inputs).

To use the function, the best option is to write the looping using recursion. The structure of the main function would look like this:

let rec playing pos =
  match getNextPosition pos (Console.ReadKey()) with
  | None -> () // Quit the game
  | Some(newPos) ->
     // This function redraws the screen (this is a side-effect,
     // but it is localized to a single function)
     redrawScreen pos newPos
     playing newPos
终弃我 2024-10-15 17:44:08

作为一款游戏,并使用控制台,这里存在固有的状态和副作用。但您要做的关键事情是消除这些可变因素。使用递归循环而不是 while 循环将帮助您做到这一点,因为这样您就可以将状态作为参数传递给每个递归调用。除此之外,我认为利用 F# 功能的主要方法是使用模式匹配而不是 if/then 语句和开关,尽管这主要是美观方面的改进。

Being a game, and using the Console, there is state and side-effects here which are inherent. But the key thing you'll want to do is eliminate those mutables. Using a recursive loop instead of a while loop will help you do that since then you can pass your state as arguments to each recursive call. Other than that, the main thing I can see to take advantage of F# features here is using pattern matching instead of if/then statements and switches, though that would be a mainly aesthetic improvement.

在梵高的星空下 2024-10-15 17:44:08

我会尽量避免过于具体 - 如果我最终在另一个方向走得太远并且这太模糊,请告诉我,我会尝试稍微改进它。

当制作一个具有某种状态的功能程序时,您想要实现的基本机制是这样的:

(currentState, input) => newState

然后您可以围绕它编写一个小包装器来处理获取输入和绘制输出。

I'll try and avoid being overly specific - if I end up going too far in the other direction and this is too vague, let me know and I'll try improve it a little.

When making a functional program that has some sort of state, the basic mechanism you want to implement is something like:

(currentState, input) => newState

Then you can write a small wrapper around that to handle fetching input and drawing output.

清风无影 2024-10-15 17:44:07

一般来说,游戏编程将测试您管理复杂性的能力。我发现函数式编程鼓励你将解决的问题分解成更小的部分。

您要做的第一件事就是通过分离所有不同的关注点,将脚本变成一堆函数。我知道这听起来很愚蠢,但是这样做的行为本身将使代码更加实用(双关语)。您主要关心的是状态管理。我使用记录来管理位置状态,使用元组来管理运行状态。随着您的代码变得更加高级,您将需要对象来干净地管理状态。

尝试向该游戏添加更多内容,并随着功能的增长不断分解它们。最终您将需要对象来管理所有功能。

在游戏编程笔记中,不要将状态更改为其他内容,然后在某些测试失败时将其更改回来。你想要最小的状态改变。因此,例如下面我计算 newPosition ,然后仅在该未来位置通过时更改 playerPosition

open System

// use a third party vector class for 2D and 3D positions
// or write your own for pratice
type Pos = {x: int; y: int} 
    with
    static member (+) (a, b) =
        {x = a.x + b.x; y = a.y + b.y}

let drawBoard map =
    //clear the console
    Console.Clear()
    //send each row of the map to the Console
    map |> List.iter (printfn "%s")

let movePlayer (keyInfo : ConsoleKeyInfo) =
    match keyInfo.Key with
    | ConsoleKey.UpArrow -> {x = 0; y = -1}
    | ConsoleKey.DownArrow -> {x = 0; y = 1}
    | ConsoleKey.LeftArrow -> {x = -1; y = 0}
    | ConsoleKey.RightArrow  -> {x = 1; y = 0}
    | _ -> {x = 0; y = 0}

let validPosition (map:string list) position =
    map.Item(position.y).Chars(position.x) = ' '

//clear the square player was standing on
let clearPlayer position =
    Console.SetCursorPosition(position.x, position.y)
    Console.Write( ' ' )

//draw the square player is standing on
let drawPlayer position =
    Console.SetCursorPosition(position.x, position.y)
    Console.Write( '@' )

let takeTurn map playerPosition =
    let keyInfo = Console.ReadKey true
    // check to see if player wants to keep playing
    let keepPlaying = keyInfo.Key <> ConsoleKey.Q
    // get player movement from user input
    let movement = movePlayer keyInfo
    // calculate the players new position
    let newPosition = playerPosition + movement
    // check for valid move
    let validMove = newPosition |> validPosition map
    // update drawing if move was valid
    if validMove then
        clearPlayer playerPosition
        drawPlayer newPosition
    // return state
    if validMove then
        keepPlaying, newPosition
    else
        keepPlaying, playerPosition

// main game loop
let rec gameRun map playerPosition =
    let keepPlaying, newPosition = playerPosition |> takeTurn map 
    if keepPlaying then
        gameRun map newPosition

// setup game
let startGame map playerPosition =
    drawBoard map
    drawPlayer playerPosition
    gameRun map playerPosition


//define the map
let map = [ "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "    ###############################     ";
            "    #                             #     ";
            "    #         ######              #     ";
            "    #         #    #              #     ";
            "    #### #### #    #              #     ";
            "       # #  # #    #              #     ";
            "       # #  # #    #              #     ";
            "    #### #### ######              #     ";
            "    #              =              #     ";
            "    #              =              #     ";
            "    ###############################     ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        " ]

//initial player position on the map
let playerPosition = {x = 8; y = 6}

startGame map playerPosition

Game programming in general will test your ability to manage complexity. I find that functional programming encourages you to break problems your solving into smaller pieces.

The first thing you want to do is turn your script into a bunch of functions by separating all the different concerns. I know it sounds silly but the very act of doing this will make the code more functional (pun intended.) Your main concern is going to be state management. I used a record to manage the position state and a tuple to manage the running state. As your code gets more advanced you will need objects to manage state cleanly.

Try adding more to this game and keep breaking the functions apart as they grow. Eventually you will need objects to manage all the functions.

On a game programming note don't change state to something else and then change it back if it fails some test. You want minimal state change. So for instance below I calculate the newPosition and then only change the playerPosition if this future position passes.

open System

// use a third party vector class for 2D and 3D positions
// or write your own for pratice
type Pos = {x: int; y: int} 
    with
    static member (+) (a, b) =
        {x = a.x + b.x; y = a.y + b.y}

let drawBoard map =
    //clear the console
    Console.Clear()
    //send each row of the map to the Console
    map |> List.iter (printfn "%s")

let movePlayer (keyInfo : ConsoleKeyInfo) =
    match keyInfo.Key with
    | ConsoleKey.UpArrow -> {x = 0; y = -1}
    | ConsoleKey.DownArrow -> {x = 0; y = 1}
    | ConsoleKey.LeftArrow -> {x = -1; y = 0}
    | ConsoleKey.RightArrow  -> {x = 1; y = 0}
    | _ -> {x = 0; y = 0}

let validPosition (map:string list) position =
    map.Item(position.y).Chars(position.x) = ' '

//clear the square player was standing on
let clearPlayer position =
    Console.SetCursorPosition(position.x, position.y)
    Console.Write( ' ' )

//draw the square player is standing on
let drawPlayer position =
    Console.SetCursorPosition(position.x, position.y)
    Console.Write( '@' )

let takeTurn map playerPosition =
    let keyInfo = Console.ReadKey true
    // check to see if player wants to keep playing
    let keepPlaying = keyInfo.Key <> ConsoleKey.Q
    // get player movement from user input
    let movement = movePlayer keyInfo
    // calculate the players new position
    let newPosition = playerPosition + movement
    // check for valid move
    let validMove = newPosition |> validPosition map
    // update drawing if move was valid
    if validMove then
        clearPlayer playerPosition
        drawPlayer newPosition
    // return state
    if validMove then
        keepPlaying, newPosition
    else
        keepPlaying, playerPosition

// main game loop
let rec gameRun map playerPosition =
    let keepPlaying, newPosition = playerPosition |> takeTurn map 
    if keepPlaying then
        gameRun map newPosition

// setup game
let startGame map playerPosition =
    drawBoard map
    drawPlayer playerPosition
    gameRun map playerPosition


//define the map
let map = [ "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "    ###############################     ";
            "    #                             #     ";
            "    #         ######              #     ";
            "    #         #    #              #     ";
            "    #### #### #    #              #     ";
            "       # #  # #    #              #     ";
            "       # #  # #    #              #     ";
            "    #### #### ######              #     ";
            "    #              =              #     ";
            "    #              =              #     ";
            "    ###############################     ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        " ]

//initial player position on the map
let playerPosition = {x = 8; y = 6}

startGame map playerPosition
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文