This is a very specific, but I think rather intriguing, programming problem I've come across, and I'm curious how people here would solve it. You can scroll to the end of this post for the basic programming question, but I feel it's necessary to provide a little background and context for how this question arose in the first place.
Background:
A group of my friends likes to play a lot of tabble tennis (ping pong). Recently, on a lark, some of us have decided to start collecting "box scores" on all of our games, and then calculating some analytics on who is best at certain elements of the game, and so forth. Just for fun. As part of this, I've been interested in simulating a table tennis game. That is, given a simplified set of parameters related to a player's performance (e.g. percentage of serves a player aces, the percentage of rallies a player scores a point, the percentage of rallies a player hits it off the table, etc.), I want to be able to simulate 100 games between those two players. Eventually, we want to use this to calcluate a sort of "Wins Above Replacement" stat, where we see how many games a person would be expected to win given their past statistical performance if they are playing an "average" player. (Obviously this is a simplified way of doing things, but we are doing this just for fun!).
Example
For the most part, this simulation is pretty simple to set up in a SAS Data step, using DO and IF loops. For example, the shell of the code might look someting like this:
DATA game;
player1_score= 0;
player2_score= 0;
do until ( (abs(player1_score-player2_score)>=2) & (player1_score>= 11 | player2_score>= 11) );
/* SERVE */
do until (score = 1);
/* DID THEY SERVE OFF THE TABLE? */
player_1_serve_UFE = rand('BERNOULLI',&UFE_rate_serve);
/* If served off table, other player gets point */
if player_1_serve_UFE= 1 then
do;
score = 1;
player2_score= player2_score+1;
end;
else do;
end;
end;
This is a shortened version of the code: this, for example, only evaluates whether or not player 1 serves the ball off the table, and if they do the other player gets the point. The real code is more elaborate, but follows this basic structure, with the DO UNTIL loop being used to satisfy the rules of a table tennis game - play to 11 but have to win by 2, and various IF and DO loops within evaluating our statistics to see whether an event happens and who earns the point. For example, the macro variable &UFE_rate_serve might be the proportion of times a player has served the ball off the table in the past, and is thus being used as an estimator for their error rate. And so on and so forth.
Problem / Question
The one element of the game I am having a difficult time coding is how the service switches in a table tennis game. If you aren't familiar, in table tennis, you serve two points in a row, and then switch; if the score reaches 10-10, you switch serves after every earned point until somebody can win by 2. However, I'm finding that this is frightfully hard to automate in my code.
The part of the program that works well is (like the snippet I gave above) the one that essentially determines what happens after a player serves (and I have similar code for the rally after the serve, all of which works as planned/expected). However, the way the code is currently written, as you can see, is that "player 1" is ALWAYS serving, and player 2 is ALWAYS receiving, which of course doesn't reflect what happens in the game.
So, what I want to be able to do is allocate the serves according to the store. So, I'd use a coin flip to decide which player serves first. Then, after every two earned points, the serve switches. However, I cannot figure out a way to really program this efficiently. I can do something horribly annoying like a really large "select" statement, as follows:
select;
when (player1_score+player2_score<=2)
do;
serve = "player 1";
{code to determine what happens on serve and who scores}
end;
when (2<player1_score+player2_score<=4)
do;
serve = "player 2";
{code to determine what happens on serve and who scores}
end;
(etc.)
But this is a pretty terrible solution, because I basically need to directly enumerate every possible score combination. Further, it is super redundant, because I need to keep repeating the rather complicated series of DO and IF loops within each WHEN statement to actually calculate the score. I know I can replace some of that with a macro to execute, but again I'm not sure if that will even work the way I want it to (after testing similar code a few times it never quite works right).
So, basically, what I want eventually is something along the lines of this:
server = player_1;
do until ( (abs(player1_score-player2_score)>=2) & (player1_score>= 11 | player2_score>= 11) );
(code that switches who the "server" is, every 2 points scored)
do until (score = 1);
(code to determine what happens on serve, etc.);
end;
end;
Does anybody have any idea how to accomplish something like this in SAS? Did I explain my problem clearly enough?
I'd write the code in terms of players 0 and 1 and use arrays to hold scores and other stats.
Every time you want to switch servers, assign
server = mod(server+1, 2);
receiver = mod(server+1, 2);
I think this will prevent dupkication of code; all code will be in terms of the server and receiver, rather than player1 and player2.
I'd write the code in terms of players 0 and 1 and use arrays to hold scores and other stats.
Every time you want to switch servers, assign
server = mod(server+1, 2);
receiver = mod(server+1, 2);
I think this will prevent dupkication of code; all code will be in terms of the server and receiver, rather than player1 and player2.
I accepted this as my solution because the use of the "mod" function in that way was helpful, even if I feel it didn't get at the real heart of my question.
My full solution ended up looking like this:
if team1_score = 10 & team2_score = 10 then overtime = 1;
else overtime = 0;
select;
when (overtime = 0 & mod(team1_score+team2_score,2)=0)
do;
team1_serve = mod(team1_serve+1,2);
team2_serve = mod(team2_serve+1,2);
team1_receive = 1-team1_serve;
team2_receive = 1-team2_serve;
end;
when (overtime = 1)
do;
team1_serve = mod(team1_serve+1,2);
team2_serve = mod(team2_serve+1,2);
team1_receive = 1-team1_serve;
team2_receive = 1-team2_serve;
end;
otherwise;
end;
team1_score = team1_score + team1_serve*server_score + team1_receive*receiver_score;
team2_score = team2_score + team2_serve*server_score + team2_receive*receiver_score;
This was the only way I could really get the simulation to work as expected. I couldn't quite figure out a solution using arrays due to the number of conditionals I had to deal with, though perhaps a more clever programmer would. In any case, this code does work as intended.
Are you ready for the spotlight? We're accepting content ideas for SAS Innovate 2025 to be held May 6-9 in Orlando, FL. The call is open until September 25. Read more here about why you should contribute and what is in it for you!
Learn how use the CAT functions in SAS to join values from multiple variables into a single value.
Find more tutorials on the SAS Users YouTube channel.
Select SAS Training centers are offering in-person courses. View upcoming courses for: