0

I'm writing a program to play Chess. My Game class has the async function play. This function returns when the game ends, returning a Winner enum, which is either a stalemate or a win for some colour.

In my main fn, I want to run game.play, then run game.render() repeatedly, until game.play returns a Winner, at which point the loop should stop.

Having attempted to read the docs, I don't really understand why Pinning or Boxing is neccesary in order to poll the future, and I haven't been able to get it working.

The desired code for main should look something like this:

    let textures = piece::Piece::load_all_textures().await;

    let mut game = game::Game::new(player::Player::User, player::Player::User);
    let future = game.play();

    while None = future.poll() {
        game.render(textures).await;
        next_frame().await;
    }

    // Do something now that the game has ended

I also understand that I will run into issues with borrow checking with this code, but that's a problem for me to deal with once I figure this one out.

What is the easiest way of attempting what I am trying to do?

2 Answers2

0

You can simply use loop

let textures = piece::Piece::load_all_textures().await;

let mut game = game::Game::new(player::Player::User, player::Player::User);
let future = game.play();

loop {
    game.render(textures).await;
    next_frame().await;
}

This will make the game run at the highest frame rate possible though, which is usually over kill, so you might want to add a sleep

loop {
    game.render(textures).await;
    next_frame().await;
    async_std::task::sleep(Duration::from_millis(15)).await;
}

This will run it at around 60 frames/second. If you want to be very accurate you can use Instant

loop {
    let now = Instant::now();
    game.render(textures).await;
    next_frame().await;
    let elapsed_ms = now.elapsed().as_millis();
    // rendering could take more than 17ms in that case substraction can 
    // overflow
    let sleep_ms = 17.checked_sub(elapsed_ms).unwrap_or_default();
    async_std::task::sleep(Duration::from_millis(sleep_ms)).await;
}

This would result in a 58.8 frames per second (you can use nanoseconds for 60frames/second which needs 16.66 ms per frame.)

Oussama Gammoudi
  • 685
  • 7
  • 13
  • This doesn't show how to stop the loop once `game.play()` returns a winner, which is the main point of the question. – Brian Bowman Oct 31 '22 at 01:41
  • @BrianBowman in general for game programming you don't want to stop the loop as you always want to render your game, all the logic should be inside of the next_frame, if you want to stop the loop you can use white let instead or simply use break; You are abusing futures, they are not intended to hold state like that, futures are only intended for async IO mostly that you don't want to check on manually, so you use some async runtime. In your case it is not an IO operation, and more than that you're going to check on it manually every frame, so no need to use a future, better to just use a var – Oussama Gammoudi Oct 31 '22 at 07:56
  • To be clear, I'm not the asker. I agree that using async for this particular use case is not common, necessary, or very helpful. But the question is specifically about async code. – Brian Bowman Oct 31 '22 at 12:05
0

The code depends a bit on what async runtime you're using. Using the popular Tokio runtime you can do something like this (the code should be very similar for async-std):

#[tokio::main]
async fn main() {
    let textures = Piece::load_all_textures().await;
    let game = Arc::new(Game::new(Player::User, Player::User));

    let game_ref = game.clone();
    let play_handle = tokio::spawn(async move { game_ref.play().await });

    let render_handle = tokio::spawn(async move {
        loop {
            game.render(&textures).await;
            next_frame().await;

            // Naive fixed delay; in reality you'd want to delay based on how long rendering took.
            tokio::time::sleep(Duration::from_millis(10)).await;
        }
    });

    let winner = play_handle.await.unwrap();

    // Do something now that the game has ended

    render_handle.abort();
    println!("{winner:?}");
}

Playground link with full code

We use tokio::spawn() to run a play task and render task concurrently. It returns a handle that you can .await to get the task's return value. The handle also has an abort() method that we use to end the render task once the play task finishes.

The Game struct is stored in an Arc so we can share it between the play and render tasks. The tokio::spawn() function requires a 'static future, so we can't just use plain references to share Game.

Brian Bowman
  • 892
  • 6
  • 9
  • Unfortunately, game.play() requires a mutable ref, so Arc is not usable, and trying to use a Mutex means that the borrow checker doesn't allow it either. I need play_handle to get a mutable ref and render_handle to get an immutable ref. – Ed Jackson Dec 11 '22 at 17:55
  • It is fundamentally impossible to have `&T` and `&mut T` references to the same object simultaneously. Maybe you can change `game.play()` to take `&self`, and then wrap any game state you need to mutate in a `Mutex`. – Brian Bowman Dec 12 '22 at 02:21