1. |
The Marienbad game. |
|
Nim is a mathematical game of strategy in which two players take turns removing (or "nimming") objects from distinct heaps or piles.
On each turn, a player must remove at least one object, and may remove any number of objects provided they all come from the same heap. Depending on the
version being played, the goal of the game is either to avoid taking the last object or to take the last object.
|
There are lots of different versions of Nim, one of the best known is the Marienbad game, so called because having been played in the
French New Wave film Last Year at Marienbad (1961). In this variant, there are 4 heaps (rows) with 1, 3, 5 and 7 objects (actually
matches). The players may take any number of matches they want (at least one) but all from the same row. The player, who takes the last match, looses. Game
variants, where the last object looses, are sometimes called Misere variants. If you are interested in details about these games,
here the Wikipedia links for Nim and Marienbad (German Wiki).
|
|
2. |
My Marienbad online application. |
|
My Marienbad online application is a Perl CGI implementation of my Marienbad desktop
application that you may freely download from the Lazarus/Free Pascal Programming section of my site. The application is for 1
human player, who plays against the computer. You may choose the type of game (Nim play: Last match wins, or Misere play: Last match looses), the player who
begins (the computer, or you), as well as the computer playing strength. There are three strength levels available:
- Novice: The computer makes all random moves, including at the end of the game (may be seen as if it did not remember if the last match wins or looses).
- Intermediate: At the first half of the game (i.e. until half of the initial number of matches has been taken), the computer plays randomly or not (1 random
move out of 3); in the second half, it plays at full strength.
- Expert: The computer plays at full strength, i.e. if there is a possibility to win the game, it will always win.
|
To play the game, set the wanted game parameters and push the Start button. If the computer has to make the first move, it does it.
Now it's your turn. Enter the number of matches, that you want to nim in the input field at the right of the heap, from which you want to nim, then push
Play. The computer moves automatically after you have done so. If the game is over, push New to prepare
for a new one, change the game parameters, if you want so, and push Start to begin the new game.
|
Use the following link to start the online application.
|
|
3. |
The Marienbad Perl script. |
|
Marienbad is a Perl CGI application, user input validity check, input field focusing, and display of the
winner message being done by Javascript. The validity check is done by a function called when the submit button of the HTML form is
pushed. This function, displaying a message if the input isn't ok, returns a Boolean: If it is true, the Perl script is called,
otherwise, nothing happens (concerning the Javascript code of this function, cf. below). The code for the field focusing and the winning message display is
directly included into the website, generated by the Perl script. Use the following links to download
the Marienbad application (HTML, Javascript and Perl sources) resp. to show the
Marienbad Perl source code.
|
use strict; use warnings;
use CGI;
use CGI::Carp "fatalsToBrowser";
my $cgi = new CGI;
print "Content-Type: text/html\n\n";
print '<!DOCTYPE html>', "\n\n";
my $app_dir = "C:\\Programs\\Apache24\\htdocs\\computing\\website\\applications";
my $lastmatch; my $firstmove; my $level;
my @initialmatches; my @matches; my @playerrowmatches;
$initialmatches[0] = 1; $initialmatches[1] = 3; $initialmatches[2] = 5; $initialmatches[3] = 7;
my $playerrow = -1; my $playermatches = 0; my $computerrow = -1; my $computermatches = 0;
my %params = $cgi->Vars;
my $action = lc($params{'action'});
$action = 'init' unless ($action);
if ($action eq 'init' or $action eq 'new' or $action eq 'start' or $action eq 'play') {
my $nextaction;
if ($action eq 'init' or $action eq 'new') {
if ($action eq 'init') {
$lastmatch = 'winner'; $firstmove = 'player'; $level = 1;
}
else {
$lastmatch = $params{'hlastmatch'}; $firstmove = $params{'hfirstmove'}; $level = $params{'hlevel'};
}
@matches = @initialmatches;
$nextaction = 'start';
}
else {
if ($action eq 'start') {
$lastmatch = $params{'lastmatch'}; $firstmove = $params{'firstmove'}; $level = $params{'level'};
@matches = @initialmatches;
}
else {
$lastmatch = $params{'hlastmatch'}; $firstmove = $params{'hfirstmove'}; $level = $params{'hlevel'};
$matches[0] = $params{'hmatches1'}; $matches[1] = $params{'hmatches2'};
$matches[2] = $params{'hmatches3'}; $matches[3] = $params{'hmatches4'};
$playerrowmatches[0] = $params{'player1'}; $playerrowmatches[1] = $params{'player2'};
$playerrowmatches[2] = $params{'player3'}; $playerrowmatches[3] = $params{'player4'};
for (my $i=0; $i < 4; $i++) {
if ($playerrowmatches[$i]) {
$playerrow = $i; $playermatches = $playerrowmatches[$i];
}
}
}
$nextaction = 'play';
}
$lastmatch = 'winner' unless ($lastmatch eq 'looser');
$firstmove = 'player' unless ($firstmove eq 'computer');
$level = 1 unless ($level == 2 or $level == 3);
for (my $i=0; $i < 4; $i++) {
$matches[$i] =~ s/[^0-7]//g;
$playerrowmatches[$i] =~ s/[^1-7]//g;
}
$playerrow =~ s/[^1-4]//g;
$playermatches =~ s/[^1-7]//g;
my $winner = '';
if ($action eq 'start' or $action eq 'play') {
if ($action eq 'start') {
if ($firstmove eq 'computer') {
($computerrow, $computermatches) = computerMove(\@matches, $lastmatch, $level);
$matches[$computerrow] -= $computermatches;
}
}
else {
$matches[$playerrow] -= $playermatches;
my $remaining = $matches[0] + $matches[1] + $matches[2] + $matches[3];
if ($remaining == 0) {
if ($lastmatch eq 'winner') {
$winner = 'The player takes the last match and wins!';
}
else {
$winner = 'The player takes the last match and looses!';
}
}
else {
($computerrow, $computermatches) = computerMove(\@matches, $lastmatch, $level);
$matches[$computerrow] -= $computermatches;
my $remaining = $matches[0] + $matches[1] + $matches[2] + $matches[3];
if ($remaining == 0) {
if ($lastmatch eq 'winner') {
$winner = 'The computer takes the last match and wins!';
}
else {
$winner = 'The computer takes the last match and looses!';
}
}
}
}
if ($winner) {
$nextaction = 'new';
}
}
my $template = "$app_dir\\marienbad.template.html";
open(my $input, "<", $template)
or die "Can't open template file $template: $!";
my @lines = <$input>;
close($input);
my $matchcount = 0;
foreach my $line (@lines) {
chomp($line);
if ($line) {
if (substr($line, 0, 1) eq '#') {
$line = substr($line, 1);
if ($line =~ /#checked1a#/) {
if ($lastmatch eq 'winner') {
$line =~ s/#checked1a#/checked="checked"/;
}
else {
$line =~ s/#checked1a#//;
}
}
elsif ($line =~ /#checked1b#/) {
if ($lastmatch eq 'looser') {
$line =~ s/#checked1b#/checked="checked"/;
}
else {
$line =~ s/#checked1b#//;
}
}
elsif ($line =~ /#checked2a#/) {
if ($firstmove eq 'computer') {
$line =~ s/#checked2a#/checked="checked"/;
}
else {
$line =~ s/#checked2a#//;
}
}
elsif ($line =~ /#checked2b#/) {
if ($firstmove eq 'player') {
$line =~ s/#checked2b#/checked="checked"/;
}
else {
$line =~ s/#checked2b#//;
}
}
elsif ($line =~ /#selected1#/) {
if ($level == 1) {
$line =~ s/#selected1#/selected="selected"/;
}
else {
$line =~ s/#selected1#//;
}
}
elsif ($line =~ /#selected2#/) {
if ($level == 2) {
$line =~ s/#selected2#/selected="selected"/;
}
else {
$line =~ s/#selected2#//;
}
}
elsif ($line =~ /#selected3#/) {
if ($level == 3) {
$line =~ s/#selected3#/selected="selected"/;
}
else {
$line =~ s/#selected3#//;
}
}
elsif ($line =~ /#visible\d\d#/) {
for (my $i=1; $i <= 4; $i++) {
for (my $j=1; $j <= $initialmatches[$i - 1]; $j++) {
my $visible = 'style="visibility:';
my $matchvisible = '#visible' . $i . $j . '#';
if ($line =~ /$matchvisible/) {
if ($j <= $matches[$i - 1]) {
$visible .= 'visible"';
}
else {
$visible .= 'hidden"';
}
$line =~ s/$matchvisible/$visible/;
}
}
}
}
elsif ($line =~ /#computer\d#/) {
for (my $i=1; $i <= 4; $i++) {
my $rowcomputer = '#computer' . $i . '#';
if ($line =~ /$rowcomputer/) {
if ($computerrow == $i - 1) {
$line =~ s/$rowcomputer/$computermatches/;
}
else {
$line =~ s/$rowcomputer//;
}
}
}
}
elsif ($line =~ /#player\d#/) {
for (my $i=1; $i <= 4; $i++) {
my $rowplayer = '#player' . $i . '#';
if ($line =~ /$rowplayer/) {
$line =~ s/$rowplayer//;
}
}
if ($nextaction eq 'play') {
$line =~ s/#readonly#//;
$line =~ s/#bold#/bold/;
}
else {
$line =~ s/#readonly#/readonly="readonly"/;
$line =~ s/#bold#//;
}
}
elsif ($line =~ /#button#/) {
my $button = ucfirst($nextaction);
$line =~ s/#button#/$button/;
}
elsif ($line =~ /#hlastmatch#/) {
$line =~ s/#hlastmatch#/$lastmatch/;
}
elsif ($line =~ /#hfirstmove#/) {
$line =~ s/#hfirstmove#/$firstmove/;
}
elsif ($line =~ /#hlevel#/) {
$line =~ s/#hlevel#/$level/;
}
elsif ($line =~ /#hmatches\d#/) {
for (my $i=1; $i <= 4; $i++) {
my $rowremaining = '#hmatches' . $i . '#';
if ($line =~ /$rowremaining/) {
$line =~ s/$rowremaining/$matches[$i - 1]/;
}
}
}
elsif ($line =~ /#playerhrow#/) {
$line =~ s/#hplayerhrow#/$playerrow/;
}
elsif ($line =~ /#hplayermatches#/) {
$line =~ s/#hplayermatches#/$playermatches/;
}
elsif ($line =~ /#js#/) {
my $js;
if ($nextaction eq 'play' and $winner eq '') {
my $first = 1;
while ($matches[$first - 1] == 0) {
$first++;
}
my $playerfiled = 'player' . $first;
$js = "<script>document.getElementById('$playerfiled').focus();</script>";
}
elsif ($winner ne '') {
$js = "<script>alert('$winner');</script>";
}
else {
$js = '';
}
$line =~ s/#js#/$js/;
}
}
unless ($line =~ /#/) {
print "$line\n";
}
}
}
}
sub computerMove {
my ($ref_matches, $lastmatch, $level) = @_;
my @matches = @$ref_matches;
my $computerrow; my $computermatches;
if ($level == 3) {
($computerrow, $computermatches) = computerWinningMove(\@matches, $lastmatch);
}
else {
my $matchrows = 0; my $onematchrows = 0;
for (my $i=0; $i < 4; $i++) {
unless ($matches[$i] == 0) {
$matchrows++;
if ($matches[$i] == 1) {
$onematchrows++;
}
}
}
if ($level == 1) {
if ($matchrows == 1) {
($computerrow, $computermatches) = computerWinningMove(\@matches, $lastmatch);
}
else {
($computerrow, $computermatches) = computerRandomMove(\@matches);
}
}
else {
if ($matchrows <= 2 or $matchrows == $onematchrows) {
($computerrow, $computermatches) = computerWinningMove(\@matches, $lastmatch);
}
else {
if (int(rand(2)) == 0) {
($computerrow, $computermatches) = computerRandomMove(\@matches);
}
else {
($computerrow, $computermatches) = computerWinningMove(\@matches, $lastmatch);
}
}
}
}
return ($computerrow, $computermatches);
}
sub computerRandomMove {
my ($ref_matches) = @_;
my @matches = @$ref_matches;
my $row; my $taken;
do {
$row = int(rand(4));
} while ($matches[$row] == 0);
my $r = int(rand(3));
if ($r == 0) {
$taken = 1;
}
elsif ($r == 1) {
if ($matches[$row] < 2) {
$taken = 1
}
else {
$taken = 2;
}
}
else {
do {
$taken = int(rand(5)) + 1;
} while ($taken > $matches[$row]);
}
return ($row, $taken);
}
sub computerWinningMove {
my ($ref_matches, $last) = @_;
my @matches = @$ref_matches;
my $row; my $taken;
my $endGame = 0; my $moreThanOnes = 0; my $ones = 0;
if ($last eq 'looser') {
for (my $i = 0; $i < 4; $i++) {
if ($matches[$i] > 1) {
$moreThanOnes++;
}
if ($matches[$i] == 1) {
$ones++;
}
}
if ($moreThanOnes <= 1) {
$endGame = 1;
}
}
if ($endGame) {
$row = -1;
for (my $i = 0; $i < 4; $i++) {
if ($matches[$i] > 1) {
$row = $i;
}
}
if ($row == -1) {
do {
$row = int(rand(4));
} while ($matches[$row] == 0);
$taken = 1;
}
else {
if ($ones % 2 == 0) {
$taken = $matches[$row] - 1;
}
else {
$taken = $matches[$row];
}
}
}
else {
my $win = 0; my $loop = 0;
do {
$loop++;
my @matches2 = @matches;
my @array = (0, 0, 0, 0);
my @matchesBin = (\@array, \@array, \@array);
my @nimSums = (0, 0, 0);
do {
$row = int(rand(4));
} while ($matches2[$row] == 0);
$taken = int(rand($matches2[$row])) + 1;
$matches2[$row] -= $taken;
for (my $i = 0; $i < 4; $i++) {
my $n = $matches2[$i];
if ($n >= 4) {
$matchesBin[$i][0] = 1;
$n -= 4;
}
if ($n >= 2) {
$matchesBin[$i][1] = 1;
$n -= 2;
}
if ($n == 1) {
$matchesBin[$i][2] = 1;
}
}
for (my $j = 0; $j < 3; $j++) {
for (my $i = 0; $i < 4; $i++) {
if ($matchesBin[$i][$j] == 1) {
$nimSums[$j]++;
}
}
}
$win = 1;
for (my $j = 0; $j < 3; $j++) {
if ($nimSums[$j] % 2 == 1) {
$win = 0;
}
}
} while (!$win and $loop < 1000);
if ($loop >= 1000) {
($row, $taken) = computerRandomMove(\@matches);
}
}
return ($row, $taken);
}
|
|
|
4. |
Using Javascript to check user input. |
|
The function checkinput() checks the number of matches taken by the player being a valid number that is less than or equal to the
number of matches actually available on the heap from which the player wants to take some match(es). If the user input is valid, the function returns true and the Perl script is called. Please, note that my knowlege of Javascript is more than limited, thus my code might look strange
to "real Javascript programmers". The only thing that matters to me, is that it works as I want it to work... Use the following link to
show the Marienbad - checkinput() Javascript code.
|
function checkinput () {
var ok = false;
if (document.getElementById) {
if (document.getElementById("action").value == 'Play') {
var row = 0; var taken = 0; var singlerow = true;
for (i = 1; i <= 4; i++) {
rowplayer = 'player' + i;
rowtaken = document.getElementById(rowplayer).value;
pattern = /^([0123456789])$/g;
res = pattern.test(rowtaken);
if (res) {
if (row == 0) {
row = i;
taken = rowtaken;
}
else {
singlerow = false;
}
}
}
if (row == 0 || taken == 0) {
alert('You must enter a valid number of matches!');
}
else if (!singlerow) {
alert('You may only remove matches from a single heap!');
}
else {
rowremaining = 'hmatches' + row;
remaining = document.getElementById(rowremaining).value;
if (taken > remaining) {
alert('You cannot take more matches than there are left on the heap!');
}
else {
ok = true;
}
}
}
else {
ok = true;
}
}
return ok;
}
|
Note: The call to the Javascript function checkinput() is implemented in the HTML file
(i.e. the template used by Perl to generate the game website) by adding an onSubmit method to the HTML form definition. The
effect of this is that the Perl script (coded as value of the action attribute), will only be called if the function
result is true. Here the HTML describing the form object:
|
<form id="form1" name="form1" method="post" action="/cgi-bin/marienbad.pl" onSubmit="return(checkinput());">
|
|
|