12

I'm developing a card game but I need to have a function that stops the program until the player hasn't clicked in the PictureBox of his card to discard it. The algorithm of my game is this:

int nextDrawer = 0; // the players which will discard a card are determinated in counterclockwise starting from the human player
for (int i = 0; i < players; i++) // untill all the players hasn't drawed a card
{
    if (i == 0) .... // the human player has to click on a picture box to discard a card
    else .... // an AI player will discard a card which is selected randomly from the 3 cards which AI has got in its hand
}

The problem is that when a mance ends, the first who will discard a card could change. If the players are numerated with 0 (human player), 1 (first AI player), 2 (second AI player) and 3 (third AI player), at the first mance the first to discard a card is the human player, but at the second mance the first to discard could be the 2 AI player and the human player has to wait until all the AI players before him discard a card (in this case, the round would be 2-3-0-1).

How can I cancel the click event if the AI players hasn't discarded a card yet?

UPDATE

I don't always need to wait that all AI players had drawed a card: if the winner of the mance is the number 2, the round would be 2-3-0-1: that means the player has to wait the AI players 2 and 3 drawed, then the player has to click one PictureBox, and the loop will return back to the AI players and then the AI player 1 is allowed to discard its card.

UPDATE 2

I've thought something like that:

int leader = 0; // who is going to discard first
int nextDiscarder = leader; // next player who's going to discard
for (int i = 0; i < nPlayers; i++) // until all the players hasn't discarded
{
    if (nextDiscarder == 0) // the human has to discard
    {
        enablePictureBoxClickEvent;
        // now before the loop continue the program has to wait the event click on a picture box
    }
    else
    {
        AI[nextDiscarder].discard(); // the ai player will discard
    }
    if (nextDiscarder == players - 1) // if nextDiscarder has reached the end of the table
        nextDiscarder = 0; // return to the begin until all player has discarded a card
    else
        ++nextDiscarder; // continue to discard with the next player
}

and in my event click I'd do something like this:

private myEventClick(object sender, EventArgs e)
{
    .... // do the instructions needed to discard a card
    disableMyEventClick;
    returnToLoop;
}

but the main problem is that I don't know how to write in code my instruction returnToLoop.

  • 1
    Have you tried setting `bool` flags to help you? Ex: Set a flag to false when human has played, and then set it back to true only if 3 more have discarded cards (using a counter perhaps). Then ensure that the human can only discard their card when the flag is true. I hope that helps! – andeart Feb 19 '16 at 20:55
  • Tried this? https://msdn.microsoft.com/en-us/library/system.componentmodel.canceleventargs.cancel(v=vs.110).aspx – Zuzlx Feb 19 '16 at 21:05
  • I think the "returnToLoop" is not needed, because the event did not terminate the loop, so the return to the loop is automatic since the event did not take you out of the thread. – gridtrak Feb 20 '16 at 02:24
  • I think that you may not be happy when you block the loop thread at the " // now before the loop continue the program has to wait" point in your code. Your GUI will freeze! I think you will be happier using a State Machine instead of a single threaded loop. – gridtrak Feb 20 '16 at 02:49
  • Using an event driven approach instead of a loop may be more elegant. – rollsch Oct 26 '16 at 05:27
  • You know, this is probably not too clean, but: a really easy thing to do is have a `while(true) {//checks for flag change and breaks loop} ` This will of course, disrupt the thread so anything else on that thread will be paused also. Another option is recursion `checkFlag(flag) { if(flag==false) checkFlag(); else //something}` – Ashwin Gupta Oct 29 '16 at 01:13

4 Answers4

26

I know most of the people will argue that you should use event-driven approach, but async/await feature can be used for easily implementing things like this w/o the need of implementing manually state machines.

I already posted similar approach in Force loop to wait for an event and A Better Way to Implement a WaitForMouseUp() Function?, so basically this is the same helper as in the former with Button replaced with Control:

public static class Utils
{
    public static Task WhenClicked(this Control target)
    {
        var tcs = new TaskCompletionSource<object>();
        EventHandler onClick = null;
        onClick = (sender, e) =>
        {
            target.Click -= onClick;
            tcs.TrySetResult(null);
        };
        target.Click += onClick;
        return tcs.Task;
    }
}

Now all you need is to mark your method as async and use await:

// ...
if (nextDiscarder == 0) // the human has to discard
{
    // now before the loop continue the program has to wait the event click on a picture box
    await pictureBox.WhenClicked();
    // you get here after the picture box has been clicked
}
// ...
Community
  • 1
  • 1
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • 1
    +1. For more info the same exact idea is described in [Tip 3: Wrap events up in Task-returning APIs and await them](https://channel9.msdn.com/Series/Three-Essential-Tips-for-Async/Lucian03-TipsForAsyncThreadsAndDatabinding) video by Lucian Wischik in channel9.msdn. – YuvShap Oct 22 '16 at 16:46
2

I like Ivan solution, because it looks good, and is reusable easily anywhere else you need to wait for a control.

However, I wanted to provide another solution, because I feel like the way are doing this is far more complicated that it could be.

So let's resume this :

  • At some point in the game, you need players to select a card they don't want to throw it away
  • There is one human player, which is number 0 in your array
  • The human player is not always the first to decide which card to throw away.
  • To decide which card to throw away, you display a picturebox to the player and you wait for him to click on it.

I believe a simple solution could be :

  1. You start by removing the card for the AI players before the human (if human is first to discard, this will do nothing, if human is last, all AI will discard here)
  2. You enable the PictureBox and you end your function
  3. In the click event of the PictureBox, you remove the user card, then you remove the card for the remaining AI players that are after the human (if human is first, all AI will remove a card here, if human is last, you do nothing)

Done...

So this would look like this :

//We need an instance variable, to keep track of the first player
int _firstPlayerToDiscard = 0;

private void StartDiscardingProcess(int FirstToDiscard)
{
    _firstPlayerToDiscard = FirstToDiscard;
    if (FirstToDiscard != 0) //If the player is the first, we do nothing
    {
        //We discard for every other AI player after the human player
        for (int i = FirstToDiscard; i < nPlayers; i++)
        {
            AI[i].Discard(); 
        }
    }
    //Now we fill the PictureBox with the cards and we display it to the player
    DiscardPictureBox.Enabled = true;
    //or DiscardPictureBox.Visible = true;
    //and we are done here, we know basically wait for the player to click on the PictureBox.
}

private void pictureBox_click(Object sender, EventArgs e)
{
    //Now we remove the card selected by the player
    // ...
    //And we remove the cards from the other AI players
    //Note that if the player was first to discard, we need to change the instance variable
    if (_firstPlayerToDiscard == 0) { _firstPlayerToDiscard = nbPlayers; }
    for (int i = 1; i < _firstPlayerToDiscard; i++)
    {
        AI[i].Discard();
    }
}

And you're pretty much done...

NB: Sorry if syntax is bad or unusual, I usually code in VB .Net... Feel free to edit syntax issues...

Martin Verjans
  • 4,675
  • 1
  • 21
  • 48
1

The following code demonstrates a simple timer based state machine. In this case, the machine's state is the current Player's Turn. This example lets each Play decide when to let the next player have her turn by setting the state to the next player. Add additional states for other things the program should check for. This program architecture runs relatively smoothly because the program threads are not blocked in tight loops. The "faster" each player can complete and exit the turn, the better - even if the player's turn repeats 10000 times without doing anything before letting the next player play.

In the example below, the Click event handler advances the machine state from the Human's turn to the AI's turn. This effectively pauses the game until the Human Clicks. Since the Turn is not blocked in a tight loop, you can have other buttons for the Human to click on like "Pass", "Start Over", and "Quit".

using System;
using System.Windows.Forms;
using System.Timers;

namespace WindowsFormsApplication1
{
  public partial class Form1 : Form
  {
    private System.Timers.Timer machineTimer = new System.Timers.Timer();

    // These are our Machine States
    private const int BEGIN_PLAY = 0;
    private const int HUMAN_PLAYER_TURN = 1;
    private const int AI_PLAYER_TURN = 2;

    // This is the Current Machine State
    private int currentPlayer = BEGIN_PLAY;

    // Flag that lets us know that the Click Event Handler is Enabled
    private bool waitForClick = false;

    // The AI members, for example 100 of them
    private const int AIcount = 100;
    private object[] AIplayer = new object[AIcount];
    private int AIcurrentIndex = 0;    // values will be 0 to 99


    public Form1()
    {
        InitializeComponent();
        this.Show();

        // The Timer Interval sets the pace of the state machine. 
        // For example if you have a lot of AIs, then make it shorter
        //   100 milliseconds * 100 AIs will take a minimum of 10 seconds of stepping time to process the AIs
        machineTimer.Interval = 100;  
        machineTimer.Elapsed += MachineTimer_Elapsed;

        MessageBox.Show("Start the Game!");
        machineTimer.Start();
    }


    private void MachineTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
        // Stop the Timer
        machineTimer.Stop();
        try
        {
            // Execute the State Machine
            State_Machine();

            // If no problems, then Restart the Timer
            machineTimer.Start();
        }
        catch (Exception stateMachineException)
        {
            // There was an Error in the State Machine, display the message
            // The Timer is Stopped, so the game will not continue
            if (currentPlayer == HUMAN_PLAYER_TURN)
            {
                MessageBox.Show("Player Error: " + stateMachineException.Message, "HUMAN ERROR!",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            else if (currentPlayer == AI_PLAYER_TURN)
            {
                MessageBox.Show("Player Error: " + stateMachineException.Message, "AI ERROR!",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
            else
            {
                MessageBox.Show("Machine Error: " + stateMachineException.Message, "Machine ERROR!",
                                MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    }



    private void State_Machine()
    {
        // This routine is executing in the Timer.Elapsed Event's Thread, not the Main Form's Thread
        switch (currentPlayer)
        {
            case HUMAN_PLAYER_TURN:
                Play_Human();
                break;

            case AI_PLAYER_TURN:
                Play_AI();
                break;

            default:
                Play_Begin();
                break;
        }
    }


    private void Play_Human()
    {
        // This routine is executing in the Timer.Elapsed Event's Thread, not the Main Form's Thread
        // My Turn!
        if (!waitForClick)
        {
            // Please Wait until I take a card...
            // I am using this.Invoke here because I am not in the same thread as the main form GUI
            // If we do not wrap the code that accesses the GUI, we may get threading errors.
            this.Invoke((MethodInvoker)delegate
            {
                pictureBox1.Click += PictureBox1_Click;
            });

            // set this flag so we do not re-enable the click event until we are ready next time
            waitForClick = true;
        }
    }


    private void PictureBox1_Click(object sender, EventArgs e)
    {
        // This routine is executing in the Main Form's Thread, not the Timer's Thread

        // Stop the game for a little bit so we can process the Human's turn
        machineTimer.Stop();

        // Disable the Click Event, we don't need it until next time
        pictureBox1.Click -= PictureBox1_Click;
        waitForClick = false;

        // To Do:  Human's Turn code...

        // Let the AI Play now
        currentPlayer = AI_PLAYER_TURN;
        machineTimer.Start();
    }


    private void Play_AI()
    {
        // This routine is executing in the Timer.Elapsed Event's Thread, not the Main Form's Thread
        if (AIcurrentIndex < AIcount)
        {
            // If we do not wrap the code that accesses the GUI, we may get threading errors.
            this.Invoke((MethodInvoker)delegate
            {
                // To Do:  AI Player's Turn code...
            });

            // Advance to the next AI
            AIcurrentIndex++;
        }
        else
        {
            // Reset to the beginning
            AIcurrentIndex = 0;
            currentPlayer = BEGIN_PLAY;
        }
    }


    private void Play_Begin()
    {
        // This routine is executing in the Timer.Elapsed Event's Thread, not the Main Form's Thread
        // If we do not wrap the code that accesses the GUI, we may get threading errors.
        this.Invoke((MethodInvoker)delegate
        {
            // ... do stuff to setup the game ...
        });

        // Now let the Human Play on the next Timer.Elapsed event
        currentPlayer = HUMAN_PLAYER_TURN;

        // After the Human is done, start with the first AI index
        AIcurrentIndex = 0;
    }

  }
}
gridtrak
  • 731
  • 7
  • 20
1

i would have design the process in a different way based on events without loop, but following your way you should use an autoreset event to notify your loop myEvent have been fired.

AutoResetEvent clickEventFired = new AutoResetEvent(false); // instanciate event with nonsignaled state
AutoResetEvent clickEventFired = new AutoResetEvent(true); // instanciate event with signaled state

clickEventFired.Reset(); // set state to nonsignaled
clickEventFired.Set();  // set state to signaled
clickEventFirect.WaitOne(); // wait state to be signaled

https://msdn.microsoft.com/en-us/library/system.threading.autoresetevent(v=vs.110).aspx

public static void yourLoop()
{
    int leader = 0; // who is going to discard first
    int nextDiscarder = leader; // next player who's going to discard

    // instanciate auto reset event with signaled state
    AutoResetEvent clickEventFired = new AutoResetEvent(true);

    for (int i = 0; i < nPlayers; i++) // until all the players hasn't discarded
    {
        if (nextDiscarder == 0) // the human has to discard
        {
            enablePictureBoxClickEvent;
            clickEventFired.WaitOne(); // wait for event to be signaled
        }
        else
        {
            AI[nextDiscarder].discard(); // the ai player will discard
            clickEventFired.Reset(); // set event state to unsignaled
        }

        if (nextDiscarder == players - 1) // if nextDiscarder has reached the end of the table
            nextDiscarder = 0; // return to the begin until all player has discarded a card
        else
            ++nextDiscarder; // continue to discard with the next player
    }
}

private myEventClick(object sender, EventArgs e)
{
    .... // do the instructions needed to discard a card
    disableMyEventClick;
    clickEventFired.Set(); // signal event
}
freakydinde
  • 1,070
  • 1
  • 9
  • 10
  • Problem might be that if you are going into `yourLoop` with the UI thread, you will `WaitOne()` and therefore block the UI thread until the user clicks, which will cause a deadlock... Unless WaitOne() does not block the UI thread, but I don't know how that is possible... – Martin Verjans Oct 27 '16 at 14:27
  • I'm asking this because in the [docs](https://msdn.microsoft.com/en-us/library/58195swd(v=vs.110).aspx) is stated : "Blocks the current thread until the current WaitHandle receives a signal." and they never talk about it being full async. – Martin Verjans Oct 27 '16 at 14:41
  • it is not really async but it feel the same, the waitHandle wait for current thread without locking the other like UI Thread. – freakydinde Oct 27 '16 at 14:56
  • That's what I said, if you enter the `yourLoop()` method with the UI thread, you create a Deadlock... – Martin Verjans Oct 27 '16 at 15:04
  • to launch the yourLoop method with the UI thread you would have to use the UI Dispatcher, why would you use it for anything except updating the UI ? – freakydinde Oct 27 '16 at 15:10