While the operation of the data entry interface was largely in place by the end of the last chapter, there are some things that remain to be done to insure its efficient operation as well as a few elements that I have added to the page to provide functional capability.
The first item I addressed was automation of the inning/half-inning change when the number of outs in a given half-inning reaches 3. This one is easy, which makes it a good place to start<grin>. The underlying logic here is not complex: if the game has been in the top half of an inning I want to change to the bottom half, and if it has been in the bottom half I want to change to the top half of the next inning. From a purely functional standpoint it would be acceptable to place the code that implements this logic within the event_proc() subroutine, because any submission that will trigger its execution will be processed within that subroutine and it should not be necessary to execute that logic at any point other than this, where any other state modifications attendant to the submission have already been made. From the standpoint of compartmentalization and maintainability, however, I thought it appropriate to include the logic in a seperate subroutine. So I did.
sub end_inning {
##automate the end of a team's at bat in an inning
my $state=$_[0];
$$state{'outs'}=0;
$$state{'strikes'}=0;
$$state{'balls'}=0;
$$state{'first_event'}='';
$$state{'second_event'}='';
$$state{'third_event'}='';
##if this is the bottom of the inning, increment the inning, packing it into a string, and reset the half-inning value to A
if ($$state{'half_inning'}=~/B/) {
my $t=$$state{'inning'};
$t++;
my $i_len=2-length($t);
my $new_inn=pack("a$i_len a*","00",$t);
$$state{'inning'}=$new_inn;
$$state{'half_inning'}='A';
}
##if this is the top of the inning, make it the bottom
elsif ($$state{'half_inning'}=~/A/) {
$$state{'half_inning'}='B';
}
}
Regardless of whether the entry session is moving from the top to the bottom half of one inning, or from the bottom half of one inning to the top of the next, the various slices in %state that track the condition of a team's at bat must be cleared. The balls, strikes, and outs counters must be reset to 0, and the slices that represent base occupation must be reset to an empty value. Once that has been accomplished, the inning/half-inning values must be set. The simplicity of the if()-elsif() construct here belies the amount of time I spent testing versions of the structure, making sure that modification of $state{'half_inning'} within the blocks will not interfere with the operation of the conditions. That is, I wanted to verify that changing the value of $state{'half_inning'} to 'A' in the first block when changing from the bottom half of the inning would not result in the elsif condition being met, with the value of $state{'half_inning'} being changed back to 'B' as a result. On the basis of that experimentation it is clear that perl evaluates the if()-elsif() conditions before executing the blocks, thus rendering the concern moot. Within the block executed if the half-inning that has ended was the bottom half of an inning ($state{'half_inning'}=~/B/), I increment the value of $state{'inning'} using the method described in the last chapter. The subroutine is invoked under the appropriate circumstances in the last line of event_proc()if ($$state{'outs'}==3) {end_inning($state);}The next enhancement was the implemention of a mechanism through which the current batter would automatically be established. This is a relatively important enhancement, because insuring that the individual batter is appropriately identified will be an important step in maintaining the quality of the data stored in the database. Manual selection of the current batter, as in the current state of the interface, simply provides too much opportunity for the inadvertent submission of the wrong value.
The first step in this process was the creation of some mechanism through which the current batter can be tracked across multiple submissions, which of course implies modification of the state and state_history tables,
alter table state add column home_batter int; alter table state add column away_batter int; alter table state_history add column home_batter int; alter table state_history add column away_batter int;and appropriate modifications to the conditions that defined the values to be stored in store_state(), with 1 stored when the entry session is initialized and whatever is held in the pertinent hash slice otherwise. With those modifications, %state will now have the slices $state{'away_batter'} and $state{'home_batter'} that will hold integer values representing the place of the current batter in the lineup, with both values initialized to 1. Progress through the batting order can now be represented by incrementing those values.
That is easy enough, but now I have to be able to get from that value to the name and id of the individual in that spot in the batting order. I have available hashes with the concantenation of the batting order position and the player name as the key and the player id as its associated value, created by the execution of get_order() in BB_STACKED and references to which have been stored in $params{'away_order'} and $params{'home_order'}. The only use to this point for these hashes has been the population of the batter select box on the entry page, and as the methodology being developed here will replace that select box I have the freedom to completely drop the statements that create these structures and use something new, perhaps a hash of arrays with the batting order position as the key and the batter name and id as elements in the array. I did not do that, largely because I was too lazy to create the requisite structures, but also because I liked the challenge of adapting a data structure not specifically designed for the purpose.
My first step here was to move storage of the references to the batting order hashes from the %params hash to %state. This move was just housekeeping, it makes a lot more sense conceptually to have that information in %state even though it will not be stored between submissions. I am going to have to undertake some gymnastics to access the specific hash slice that represents the batter, however, because perl requires the full hash key to access the value stored in the slice. The strategy I employed here was to use the keys function to return all of the keys iin the pertinent hash as a list, and then search that list for elements that have the integer representing the batting order position of the current batter stored at the beginning of the string that represents the hash key. In effect I am avoiding traversing the entire hash by flattening the keys of the hash into a simple list, which can be searched much more quickly that the hash itself.
While I could step through the elements of that list, attempting a pattern match against each in turn, a far more efficient method is to employ perl's grep function. Perl's grep is analogous tto the grep function implemented in *nix environments, which performs a pattern match against each of the items in a list. Perl's grep implementation, in list context, returns a list of the items that match the pattern. The following statement, for example, searches the strings of the keys in a hash referenced by $players for those which hold the integer in $state{'away_batter'} at the beginning of the string and stores the result to the array @batter.
@batter=grep /^$$state{'away_batter'}/, keys %$players;
In this application, of course, that array will have only one element, the key for the slice in the pertinent batting order hash that is associated with the current batter. Exactly which hash that is, and which %state slice holds the pertinent batting order position, is determined by the current half_inning. The full subroutine that locates the batter information is thus
sub find_batter {
my $state=$_[0];
my ($players,@batter);
if ($$state{'half_inning'}=~/A/) {
$players=$$state{'away_order'};
@batter=grep /^$$state{'away_batter'}/, keys %$players;
}
elsif ($$state{'half_inning'}=~/B/) {
$players=$$state{'home_order'};
@batter=grep /^$$state{'home_batter'}/, keys %$players;
}
$$state{'ab_batter'}=$$players{$batter[0]};
$$state{'batter_name'}=$batter[0];
}
I store the pertinent hashref to $players simply to trim a statement or two from the subroutine. Beyond the use of the grep function, ths subroutine is straightforward. Note that I store the id of the batter to $state{'ab_batter'} and the batting order poosition - name concantenation to $state{'batter_name'}.With this in place I can drop the batter select box, print the batter name in the at bat form, and the first batter for the away team will be displayed when an entry session for a game is initialized. While I could remove the batting order position froom the string that is displayed, it struck me that the display of the batting order position does provide a bit of pertinent information to the user, sufficient to justify its presence.
What I have done to this point, however, does nothing to step through the lineup. Next I have to put together a mechanism to increment the integer representing the lineup position for the current batter. This is just a little tougher than it might seem on first consideration, because not only must the half-inning be determined but also whether the current batter is at the bottom of the lineup, which would mean that the value should be reset to 1. Rather than explicitly coding the potential conditions, I created a temporary hash, %t, in which 'A' and 'B' are stored as the keys and 'away_batter' and 'home_batter' and the values. The value in $t{$$state{'half_inning'}}} will now represent the key for the hash slice holding the position of the current batter in the order. The subroutine to increment the current batter position follows:
sub increment_batter {
my $state=$_[0];
my %t=('A'=>'away_batter',
'B'=>'home_batter');
if ($$state{$t{$$state{'half_inning'}}}==9)
{$$state{$t{$$state{'half_inning'}}}=1;}
else {$$state{$t{$$state{'half_inning'}}}++;}
}
In the version of the interface that used a select box to move from batter to batter I triggered the generation of a new at bat key when the parameter for the batter id established by that selection differed from the id previously embedded in the at bat key. Here the construction of the new at bat key must be triggered when the new batter is assigned, and in other circumstances as I shall discuss shortly. As the construction of the new key will be required in more than one circumstance, I wrapped it into a subroutine of its own.
sub construct_ab_key {
my $state=$_[0];
my $ctr=substr($$state{'at_bat_key'},10,5);
$ctr++;
my $len=5-length($ctr);
$ctr=pack("a$len a*","00000",$ctr);
$$state{'at_bat_key'}=$$state{'game_id'}.$$state{'ab_batter'}.$$state{'inning'}.$ctr;
}
This is, of course, the same logic as implemented previously.I can now use these three subroutines to move to a new batter. I have added the scalar $new_batter to event_proc(), and use it as a flag, storing the value "1" in any context in which the nature of the submission being processed indicates that the current at bat has ended. Among the circumstances that lead to this flag being set are a third strike, a fourth ball, or the presence of any at bat result parameter value, because if an at bat had a result it is clearly over. With the $new_batter switch set, the small block that I have now included after the final switch construct in event_proc() will make the changes appropriate to a batter change.
if ($new_batter) {
$$state{'strikes'}=0;
$$state{'balls'}=0;
increment_batter($state);
find_batter($state);
construct_ab_key($state);
}
That block is only the most obvious place from which one or more of the new subroutines must be called. I now also execute find_batter() and construct_ab_key() as the final statements in end_inning(), retrieving the appropriate batter for the team now coming to bat and constructing a new at bat key.Also worthy of note is the order in which the $new_batter block and the call to the end_inning() subroutine are implemented at the end of event_proc(), and their independence from each other. The $new_batter block must come before end_inning(), because the pertinent batting order values, when appropriate, must be changed before the inning/half-inning values.
if ($new_batter) {
$$state{'strikes'}=0;
$$state{'balls'}=0;
increment_batter($state);
find_batter($state);
construct_ab_key($state);
}
if ($$state{'outs'}==3) {end_inning($state);}
Two examples should serve to illustrate the relevance of this order. Consider first a runner who has struck out to end the inning. As the $new_batter flag is set, the batting order value for the team with which the batter was associated will be incremented, and then the information for the other team's order will be pulled forth to generate the next entry page. On the other hand, if the third out occurred because a runner was picked off base the $new_batter block will not execute because the flag was not set, and the same batter will be at the plate when next the team bats. In this current structure construct_ab_key() will be executed twice at the end of an inning in which the batter makes the final out, but as end_inning() is executed after the $new_batter block the value in $state{'at_bat_key'} will be appropriate for the team coming to bat.One additional modification remains to complete the implementation of automatic movement through the batting order, and that is initializing the at bat key that will be current when any given entry session is begun. In effect, in this version that action becomes part of state initialization. While I could have included the steps that generate the initial key in store_state(), I had some reservations about cluttering up that subroutine with statements that do not directly pertain to state storage. Therefore, I modified the statement that assigns the initial at bat key value to be stored in %state to the string "Not Set" and added a block to BB_STACKED that traps that stored value and initializes the at bat information immediately after the batting orders have been retrieved.
##get hashrefs to the orders for the teams in the selected game, as well as the ids for the home and away teams.
($home_players,$away_players)=get_order($r,$db,$state);
$$state{'away_order'}=$away_players;
$$state{'home_order'}=$home_players;
##set the at bat key if the stored value is "Not Set", which indicates that this is the initial entry for this game
if ($$state{'at_bat_key'}=~/^Not/) {
find_batter($state);
$$state{'at_bat_key'}=$$state{'game_id'}.$$state{'ab_batter'}.$$state{'inning'}.'00001';
store_state('B',$state,$db,$r);
}
This involves an extra call to store_state() than would have been involved had I incorporated the logic into store_state() itself, but as this context will exist only upon entry session initialization I considered the tradeoff to be worthwhile.With these modifications in place, the application will now automatically move through the lineup, switch between teams as half-innings end, and remember the next batter due at the plate.
The next little interface tweak I implemented was the prevention of the submission of an on base event that would result in the runner occupying either the same base they occupy before the play or a base preceeding that base on the base path. In other words, I want to prevent the user from inadvertently submitting an event for a base runner on third base that involves that runner advancing to second base. Providing a basic level of protection against such a submission is relatively simple.
The key thing to remember here is that the events I wish to trap are only pertinent to the on base forms, and therefore whatever modifications I employ need only to be represented in dosel_onbase() in the embedded javascript. As in the implementation of check_base(), I define a variable, back_base, in the root scope of the script that will serve as a flag. Within the dosel_onbase() function itself I create the variable dest_base that is an integer representation of the last two characters of the value of the selected option that is being evaluated by the function. Obviously, what is held in that variable will have no meaning at all in most of the function, but its value will be perfectly appropriate in the locations in which it is used.
var dest_base=parseInt(code.substr(code_len-2,2));The base currently occupied by the runner is already available in the function as the variable c_base. Therefore, I have only to compare the values stored in the two variables to determine whether to set the flag, and the final two cases in dosel_onbase() now include the following line.
if (dest_base <= c_base) {back_base='1';}
All that remains is the incorporation of an appropriate condition into the valid_entry() function.
if (back_base=='1') {
alert ('The selected base is behind or the same as the one currently occupied');
return false;}
With these elements in place, the attempted submission of such an event will fail and an alert will be displayed.
The next item I added to the entry page was the display of the name of the runner occupying a base in the on-base form that is generated as the result of that occupation. Given that the strings stored in %state to represent base occupation (e.g., $$state{'first_event'}) have the id of the runner embedded within them, all I need is a hash with that id as the key and the player name as the value. At this point the system does not have a hash in this format. Rather than creating one, I decided that the most efficient manner in which to handle this was to simply reverse the hashes created in get_order(). I therefore added these lines to BB_STACKED()
##store a reference to a hash of ids and names for the players in the current order
my %temp=reverse %$away_players if $$state{'half_inning'}=~/A/;
%temp=reverse %$home_players if $$state{'half_inning'}=~/B/;
$$state{'players'}= \%temp;
Note that I only store the lineup pertinent to the current half-inning. I can now print the runner's name by accessing the key that is the pertinent substring of the relevant %state hash value, as in
$$r->print("Runner: $$state{'players'}{substr($$state{'first_event'},5,3)}<br>");
which is now in the html that prints the first base on base form. With these elements in place, a typical entry page might look like this.
While I have earlier discussed the limitations on automating the movement of baserunners in advance of the hitter, there are a couple of circumstances in which that movement can be automated. Those are the at bat results of home runs and four base errors. (The latter, while rare, do happen. I have tried to envision a context in which a runner in advance of the batter would not score on a four-base error, and have come to the conclusion that by definition they would score, simply because the circumstance would not be recorded as a four-base error otherwise. If I think of an exception at some point I will revise the code accordingly.)
The subroutine that I have written to score any runners in advance of the batter is named clear_bases().
sub clear_bases {
##clear the bases on a homer or four base error
my ($params,$state,$db)=@_;
if ($$state{'third_event'}=~/\S/) {
$$params{'base'}='3';
$$params{'insert_key'}=$$state{'third_event'};
obe_insert($params,$state,$db);
record_score($params,$state,$db);
$$state{'third_event'}='';
}
if ($$state{'second_event'}=~/\S/) {
$$params{'base'}='2';
$$params{'insert_key'}=$$state{'second_event'};
obe_insert($params,$state,$db);
record_score($params,$state,$db);
$$state{'second_event'}='';
}
if ($$state{'first_event'}=~/\S/) {
$$params{'base'}='1';
$$params{'insert_key'}=$$state{'first_event'};
obe_insert($params,$state,$db);
record_score($params,$state,$db);
$$state{'first_event'}='';
}
}
At this point the subroutine is very literal: this is another of the circumstances in which the items specific to each context could be stored in a structure and iterated over, but with only three bases I think the ease a maintaining a literal block like this outweighs the relatively small reduction of code resulting from the implementation of such a structure. Note that I start with any runner occupying third base and work backward along the basepath. As a result the value of $state{'sequence'} stored in the ob_events table will accurately preserve the order in which the runners score.The context within which clear_bases() is called is within the block that is executed if the submission contains an at bat result code that includes the string "04" in the final segment. The statements within that block score the batter and any runners on the basepath.
elsif ($r_destination =~ /04/) {
if ($r_result=~/02/) {$$params{'rbi'}='1',$$params{'notes'}[0]='scored on home run';$$params{'obe_code'}[0]='OBE:02:05:NA:04';}
elsif ($r_result=~/03|04/) {$$params{'rbi'}='0',$$params{'notes'}[0]='scored on error';$$params{'obe_code'}[0]='OBE:02:04:'.$r_position.':04';}
clear_bases($params,$state,$db);
$$params{'base'}=0;
$$params{'insert_key'}=$$state{'at_bat_key'};
record_score($params,$state,$db );
}
First, note that the batter is scored after clear_bases() scores any runners. Again, this is to make sure that scores are recorded in the appropriate sequence. The blocks included within the if() - elsif() construct at the beginning of the snippet store values pertinent to the character of the at bat result (i.e., hit or error) to the hash slices from which they will be used to populate columns in the ob_events and score tables.Once these modifications are made and the server reloaded, submission of a home run or four base error with runners on base will trigger the scoring of those runners and the batter, with attendant insertion of appropriate records in the ob_events and score tables, which is pretty cool.
These changes have substantially smoothed the operation of the entry session, but a moment's reflection may reveal something missing. How are substitutions to be made? Given that I anticipate that this will be a reasonably substantial subject, I think I will end this chapter here and address it, and one or two smaller issues, in the next.