Читать книгу: «Справочник Жаркова по проектированию и программированию искусственного интеллекта. Том 3: Программирование на Visual C# искусственного интеллекта (продолжение 2)», страница 2

Шрифт:

1.7. Класс CardShoe для представления карт в колоде случайным образом и тестирования игры

Заключительный класс, который управляет картами в движке игры CardEngine. cs, – класс CardShoe. Мы используем этот класс, чтобы обеспечить вывод карт случайным образом (при помощи генератора случайных чисел – г.с.ч.). Игорное казино данного приложения имеет специальное устройство, названное shoe (колода) или deck (колода), которое содержит карты. В начале игры карты перетасованы (shuffle) много раз и помещены в колоду. В процессе многократной перетасовки приложение использует г. с. ч. для размещения карт в виде элементов массива. Класс CardShoe содержит этот массив и заполняет его в начале игры. Все карты вводятся в массив от первой до последней, а затем массив перетасовывается снова, и так несколько раз.

Когда мы проектируем любую систему, мы должны также думать, как мы собираемся её проверять (тестировать). Было бы трудным для нас проверить игру, если бы мы должны были запустить игру 50 раз только для того, чтобы удостовериться, что игра работает правильно, когда игрок получает счёт карт, равный 21. Поэтому класс CardShoe снабжен дополнительной особенностью. В дополнение к конструктору этого класса, который позволяет разработчику использовать класс, чтобы выбрать число перетасовок в колоде, имеется ещё перегрузка конструктора, который принимает массив числовых значений типа byte и представляет «расположенную в стеке» колоду. Такая колода не перетасована, и вместо этого располагает карты в специфической заранее предопределённой последовательности. Расположенная в стеке колода даёт возможность разработчику проверить поведение карт в различных ситуациях игры, предоставляя приложению специфическую последовательность значений карт.

Чтобы гарантировать, что расположенная в стеке колода карт не может использоваться в низменных целях (для подтасовок в игре), флажок сообщает пользователю объекта класса CardShoe, действительно ли в данный момент используется расположенная в стеке колода карт. Разработчик, который использует объект этого класса, может проверить этот флажок и гарантировать, что игра не использует расположенную заранее определённым образом в стеке колоду карт.

Проверяя приложение, мы можем создавать сравнительно редкие сочетания карт, типа blackjacks, просто обеспечивая расположенную в стеке колоду, как показано в следующем коде:

 
public CardShoe (byte [] stackedDeck)
{
decks = stackedDeck;
testShoe = true;
 

}

Расположенная в стеке колода даст в игре Блэк Джека обоим игрокам. Последовательность карты 1 представляет первую карту первой масти в колоде, которая является тузом. Поскольку каждая масть содержит 13 карт, 14-я карта представляет первую карту второй масти, которая является также тузом. В полной игре между игроком (player) и банкомётом (banker) наличие с начала колоды последовательностей карт 1 и 14 приводит и к игроку, и к банкомёту, первоначально начавшему игру, туза. Последовательности карт 11 и 25 представляют Джек (валета) от первых и вторых мастей соответственно, таким образом, каждый игрок получает Джек (валета) как их вторая карта. В игре и игрок (player), и банкомёт (banker) получают по тузу, комбинация Джека (валета) приходит к обоим игрокам, имеющим Блэк Джеки.

1.8. Схема запуска игры

Когда начинается игра, приложение очищает (Clear) «руку игрока» (player’s hand) и добавляет две колоды карт следующим образом:

CardHand playerHand = new CardHand ();

 
playerHand.Clear ();
dealerHand.Clear ();
// deal the face down hole card
dealerHoleCard = shoe.DealCard ();
dealerHoleCard. FaceUp = false;
dealerHand.Add (dealerHoleCard);
// deal the first player card
playerHand.Add(shoe.DealCard ());
// deal the second dealer card (face up)
dealerHand.Add(shoe.DealCard ());
// deal the second player card
 
playerHand.Add(shoe.DealCard ());

В этом коде, для простоты, не принимается во внимание чередование раздачи карт между игроком и банкомётом. Это будет учтено далее в полной программе игры.

Далее в проекте имеется команда «Hit Me» меню maneMenu1. После выбора этой команды, компьютер выдаёт игроку дополнительную карту, если набранное им количество очков меньше 21, как показано в следующем коде:

 
void playerHits ()
{
if (playerHand. BlackJackScoreHand () <21)
{
playerHand.Add(shoe.DealCard ());
if (playerHand. BlackJackScoreHand ()> 21)
{
//We write in the original:
pot.DoPlaceBet ();
pot. HouseWins ();
showPot ();
mode = GameMode.PlayerBust;
}
this.Invalidate ();
 

}

}

Отметим, что метод BlackJackScoreHand каждый раз возвращает счёт «руки игрока» (playerHand). После этого метод Invalidate перерисовывает форму Form1, точнее, перерисовывает карты и обновляет счёт обоих игроков. Аналогично в меню maneMenu1 имеется команда «Себе», по которой банкомёт набирает карты себе (после игрока).

1.9. Рисование очков игроков

Методы Windows для рисования текстов хороши для простых сообщений, но для игры пользователь ожидает что-то более красивое. Например, мы можем рисовать текст на фоне какого-либо рисунка, чтобы выделить текст. Мы можем осуществить это, неоднократно рисуя текст в множестве позиций вокруг его желательного местоположения, перед размещением реального текста на самом верху фона. Чтобы сделать это, был написан ряд утилит, как показано в следующем коде:

 
static private SolidBrush messageBrush =
new SolidBrush (Color. Black);
public static void BigText (string message, int x, int y,
Color back, Color fore,
Font messageFont, Graphics g)
{
int i;
messageBrush.Color = back;
for (i = 1; i <3; i++)
{
g. DrawString (message, messageFont, messageBrush,
x – i, y – i);
g. DrawString (message, messageFont, messageBrush,
x – i, y + i);
g. DrawString (message, messageFont, messageBrush,
x + i, y – i);
g. DrawString (message, messageFont, messageBrush,
x + i, y + i);
}
messageBrush.Color = fore;
g. DrawString (message, messageFont, messageBrush, x, y);
 

}

Этот метод BigText снабжен ссылкой на объект графики, чтобы использовать её для рисования текста (message) соответствующим шрифтом (messageFont) в соответствующей позиции. Задаётся также цвет для приоритетных и фоновых версий текста. Метод рисует множество фоновых версий текста перед помещением приоритетной версии на вершине. Метод является статическим, поэтому для вызова не нуждается в объекте класса Utilities, а вызывается напрямую после имени класса, как показано в следующем коде:

Utilities.BigText («Dealer Bust»,

20, 80, Color. Black, Color. Yellow, messageFont, g);

В этом коде сообщение «Dealer Bust!» означает «Банкомёт перебрал карты».

Объект messageFont класса Font создан в начале приложения и используется для всего рисунка сообщения.

Далее при разработке программы игры мы сначала стандартно создадим шаблон метода Paint (после двойного щелчка по имени события Paint в панели Properties для формы Form1), затем в тело этого шаблона запишем наш код и будем вызывать этот метод каждый раз, когда экран должен быть перерисован. С точки зрения проектирования, считается не очень хорошей практикой выполнять прикладные функции непосредственно в обработчике события Paint. Поэтому для рисования изображений, наше приложение в шаблоне метода Form1_Paint будет вызывать специальный метод paintForm, как показано в следующем коде:

private void Form1_Paint (object sender, PaintEventArgs e)

{

paintForm(e.Graphics);

}

1.10. Управление игрой

Теперь мы можем использовать вышеупомянутые классы, чтобы осуществить большинство видов игры в карты. Рассмотрим, как в целом осуществляется игра, давая возможность сначала игроку (player) сделать первые ходы (набрать карты), а затем – банкомёту (или дилеру – dealer) сделать ответные ходы (набрать свои карты).

Игра в очко может иметь одно из следующих состояний в течение всей игры:

игрок выдаёт карты себе или Компьютеру (the player is making his or her moves);

игрок перебрал карты (the player busted);

игрок выиграл (the player has won);

банкомёт (dealer) осуществляет набор карт (the dealer is making his or her moves.);

банкомёт перебрал карты (the dealer busted);

банкомёт выиграл (the dealer has won);

счёт равный (the score is tied, known as a push).

В каждом из этих состояний рисунок экрана будет различным, как ответ игры на события. Например, запрос банкомёта следующей карты допускается только тогда, когда игрок закончил набор своих карт. Мы можем представить эти состояния или режимы игры (mode) посредством перечисления enum следующим образом:

 
public enum GameMode
{
LoadingDisplay,
PlacingBets,
PlayerActive,
PlayerWon,
PlayerBust,
PocketJack,
DealerActive,
DealerWon,
DealerBust,
Push
 

}

Переменная типа GameMode сохраняет состояние игры. Эта переменная управляет перерисовкой экрана. Когда состояние игры изменяется, должен произойти ряд действий. Лучший способ получить это поведение состоит в том, чтобы осуществить управление посредством свойства следующим образом:

 
GameMode modeValue;
GameMode mode
{
get
{
return modeValue;
}
set
{
switch (value)
{
case GameMode. LoadingDisplay:
BetMinusToolStripMenuItem1.Enabled = false;
BetPlusToolStripMenuItem. Enabled = false;
HitMeToolStripMenuItem. Enabled = false;
StayToolStripMenuItem. Text = «Point»;
StayToolStripMenuItem. Enabled = false;
MenuToolStripMenuItem. Text = «21»;
MenuToolStripMenuItem. Enabled = false;
break;

modeValue = value;
this.Invalidate ();
 

}

}

Когда свойству задано значение value, выполняется часть набора свойства после ключевого слова set. Когда код набора set выполнен, переключатель switch устанавливает приложение нужным способом. Например, когда состояние свойства изменено в состояние PlayerActive, активизированы команды меню Hit и Stay. Везде, где происходит изменение состояния в основном приложении, пользовательский интерфейс находится всегда в нужном состоянии. Это также означает, что мы должны изменить конфигурацию игры только в одном месте кода. Отметим, что, когда состояние игры изменено, вызывается метод Invalidate для обновления экрана.

Состояние игры (game state) также управляет рисованием, когда вызывается событие Paint, как показано в следующем коде:

 
void paintForm (Graphics g)
{
switch (mode)
{

case GameMode.PlayerActive:
dealerHand. DrawHand (g, 10, 30, 80, 25);
playerHand. DrawHand (g, 10, 135, 20, 25);
Utilities.BigText (playerHand. BlackJackScoreHand ().
ToString (), 140, 150, Color. Black,
Color. Yellow, messageFont, g);
 
break;

Этот код с методом Paint показывает, как приложение управляет выводом сообщений и карт. Очки банкомёта не появляются, когда игрок набирает карты.

1.11. Набор карт банкомётом

Банкомёт (dealer) должен получить две карты, одна из которых появляется лицевой стороной вниз (face down). Мы достигаем этого при использовании следующего кода:

 
// clear the hands
playerHand.Clear ();
dealerHand.Clear ();
// deal the face down hole card
dealerHoleCard = shoe.DealCard ();
dealerHoleCard. FaceUp = false;
dealerHand.Add (dealerHoleCard);
// deal the first player card
playerHand.Add(shoe.DealCard ());
// deal the second dealer card (face up)
dealerHand.Add(shoe.DealCard ());
// deal the second player card
playerHand.Add(shoe.DealCard ());

mode = GameMode.PlayerActive;
 

Приложение сохраняет ссылку на «тайную – hole» карту банкомёта, которая инициализируется лицевой стороной вниз, когда начинается набор карт. Это достигается заданием свойству FaceUp (Лицевая сторона вверх) значения, равное False. Когда экран будет перерисовываться, будет нарисована также и обратная сторона «тайной – hole» карты банкомёта. Когда банкомёт начинает набор карт, свойству FaceUp (Лицевая сторона вверх) задаётся значение True, и изображение на лицевой стороне карты становится видимым. Отметим, что изменение режима (mode) внизу приведённого кода переводит игру в активное состояние, когда игрок готов принять участие в игре.

Приложение сдаёт карты в том же самом порядке, как в реальной игре с банкомётом и игроком, по очереди берущими карты. Нет никакой программируемой причины для замены между банкомётом и игроком, но в реальной игре проще «расположить в стеке» колоду, если сдача карт не чередуется.

1.12. Набор карт игроком

Приложение содержит метод, который вызывается, когда игрок хочет набрать себе карты. Он получает дополнительную карту, только если счёт – меньше 21, как показано в следующем коде:

 
void playerHits ()
{
if (playerHand. BlackJackScoreHand () <21)
{
playerHand.Add(shoe.DealCard ());
if (playerHand. BlackJackScoreHand ()> 21)
{
//We write in the original:
pot.DoPlaceBet ();
pot. HouseWins ();
showPot ();
mode = GameMode.PlayerBust;
}
this.Invalidate ();
 

}

}

Если счёт игрока превышает 21, игрок совершил перебор карт (the player busts), и состояние игры изменяется, чтобы отобразить это. Иначе, экран обновляется, что вызывает перерисовку и добавление на экран новой карты.

Когда игрок достиг счёта, которым он доволен, игрок может приостановить (stay) набор новой карты, как показано в следующем коде:

 
void playerStays ()
{
dealerHoleCard. FaceUp = true;
mode = GameMode.DealerActive;
this.Refresh ();
System.Threading.Thread.Sleep (750);
while (dealerHand. BlackJackScoreHand () <17)
{
dealerHand.Add(shoe.DealCard ());
this.Refresh ();
System.Threading.Thread.Sleep (750);
}
if (dealerHand. BlackJackScoreHand ()> 21)
{
mode = GameMode.DealerBust;
pot.PlayerWins ();
showPot ();
return;
}
if (playerHand. BlackJackScoreHand ()>
dealerHand. BlackJackScoreHand ())
{
mode = GameMode.PlayerWon;
pot.PlayerWins ();
showPot ();
return;
}
if (playerHand. BlackJackScoreHand () <
dealerHand. BlackJackScoreHand ())
{
mode = GameMode.DealerWon;
//Мы дописываем в оригинале:
pot.DoPlaceBet ();
pot. HouseWins ();
showPot ();
return;
}
if (playerHand. BlackJackScoreHand () ==
dealerHand. BlackJackScoreHand ())
{
mode = GameMode. Push;
pot.DoPushBet ();
showPot ();
return;
 

}

}

Этот метод должен изменить игровое состояние на DealerActive и затем закончить набор карт банкомётом. Он также переворачивает тайную карту банкомёта лицом вверх, чтобы эту карту можно было увидеть. Игра банкомёта организована по циклу, который неоднократно даёт новые карты банкомёту, если счёт банкомёта меньше 17. Банкомёт обязан запускать игру этим механистическим способом. Приложение содержит паузу на 750 миллисекунд между каждой картой банкомёта, чтобы добавить волнение в игру (в этой паузе также звучит тревожная музыка). Вызов метода Refresh гарантирует, что игрок информирован относительно каждой последующей карты банкомёта.

Если банкомёт получает счёт, больше 21, состояние изменено на DealerBust (Банкомёт перебрал карты), и выполнение метода заканчивается. Иначе, метод решает, кто выиграл и устанавливает соответствующее состояние игры. Тогда игрок может выбрать команду «new game» для начала новой игры, которая снова устанавливает «руки» игрока и банкомёта и соответствующее состояние игры.

1.13. Контекстно-зависимый пользовательский интерфейс

Даже настольный компьютер ограничен доступными средствами управления пользовательским интерфейсом. Он спроектирован так, чтобы во многих случаях можно было пользоваться только одной рукой, что означает, что управления должны быть удобными. В данном приложении есть ограниченное число действий, которые игрок может выполнить. Чтобы сделать игру проще, необходимо отобразить эти действия на клавишах клавиатуры и кнопках мыши, которыми игрок наиболее часто пользуется, как показано в следующем коде:

 
void doEnter ()
{
switch (mode)
{
case GameMode. LoadingDisplay:
break;
case GameMode.PlacingBets:
startPlay ();
break;
case GameMode.PlayerActive:
playerHits ();
break;
case GameMode. PocketJack:
case GameMode.PlayerWon:
case GameMode.PlayerBust:
case GameMode.DealerActive:
case GameMode.DealerWon:
case GameMode.DealerBust:
case GameMode. Push:
startHand ();
break;
 

}

}

Этот код означает программируемые команды Меню на основе элемента управления MenuStrip.

Когда игрок нажимает клавиши на клавиатуре, предыдущий код решает важные для приложения задачи. В режиме PlayerActive, нажимая клавишу Enter, программа сдаёт игроку следующую карту. В любом другом режиме программа сдаёт карты для новой игры. Используя команду Stay (Приостановка) элемента управления MenuStrip, пользователь может приостановить и запустить игру.

Мы должны рассмотреть эту проблему, проектируя наши карточные и другие игры. Мы должна попробовать использовать игру на мобильном устройстве (о чем описано в наших предыдущих книгах), которое мы держим в одной, например, левой руке, в то время как в другой руке мы держим чашку кофе. Это должно быть возможным. Отметим, что команды элемента управления MenuStrip могут также выполнять все эти контекстно-зависимые действия; например, по команде Hit программа сдаёт игроку следующую карту. Игрок никогда не обязан предполагать, а должен точно знать, как выполнить специфическое действие.

1.14. Банк и ставка

Большинство игроков хочет иметь возможность взять банк (Pot). Чтобы сделать это, игра должна иметь ставку (Bet), которую игрок может выиграть или проиграть на каждой сдаче карт. В начале игры игроку показывают размеры доступного банка и ставки, которые он может увеличить или уменьшить. Если капитал игрока накапливается плохо, приложение должно дать игроку опцию сброса размера банка назад к оригинальным значениям начала игры. Банк осуществлен как отдельный класс Pot, чтобы он мог использоваться в других играх (если требуется). Перед каждой сдачей карт игрок может решить, сколько держать пари (какую назначить ставку), до количества денег в банке. Если игрок попробует держать пари больше, чем размер банка, приложение предложит сбросить банк до начального значения.

Игрок легко управляет размером ставки, нажимая клавиши со стрелками вверх или вниз. Эти команды также доступны на элементе управления MenuStrip.

После выбора размера ставки игрок может запустить игру при помощи команды на элементе управления MenuStrip и мыши или клавиш. Режим банка и ставки фактически управляется посредством дополнительного игрового состояния, которое также рисует размеры банка и ставки на фоне игры, как будет показано далее.

1.15. Добавление справочной формы

Приложение теперь имеет все основные составляющие, позволяющие запустить игру. Однако несколько добавлений придадут приложению более современный вид. Эти добавления включают:

справочную форму,

рисунок с сообщением Loading, появляющийся на экране во время длительной загрузки и вывода на экран какого-либо изображения

и звуковое сопровождение.

Справочная форма – это вторая форма Form2 (вслед за главной формой Form1 с интерфейсом игры), которая содержит один или несколько элементов управления с панели инструментов Toolbox, например, окно текста TextBox со справочной информацией для игрока. Когда при помощи мыши игрок раскрывает Меню элемента управления MenuStrip, а затем в этом Меню выбирает команду Помощь (рис. 1.7), рядом с первой формой Form1 появляется эта справочная форма Ащкь2 (рис. 1.8). Напомним, что Меню элемента управления MenuStrip закрывается после его повторного выбора мышью. Эта форма Form2 закрывается щелчком по значку Close, и на экране остаётся одна игровая форма Form1.



Рис. 1.7. Команда Помощь в Меню. Рис. 1.8. Справочная форма.

1.16. Добавление рисунка загрузки

Одним из заключительных этапов разработки игры является добавление в проект и вывод на экран рисунка загрузки (a loading screen) в виде файла loading.gif. Этот рисунок появляется на экране в режиме (mode = GameMode. LoadingDisplay;) при помощи метода Paint в начале загрузки в программу игровых компонентов (например, в виде графических и звуковых файлов) и построения формы, а затем исчезает. Здесь основная проблема – обеспечение видимости фона формы в процессе загрузки. Чтобы сделать игровую фон формы видимым, мы устанавливаем для формы значение свойства Visible, как показано в следующем коде:

 
System.Reflection.Assembly asm =
System.Reflection.Assembly.GetExecutingAssembly ();
loadingImage = new Bitmap(asm.GetManifestResourceStream (
"PocketJack.images.loading.gif»));
bankImage = new Bitmap(asm.GetManifestResourceStream (
"PocketJack.images.bank.jpg»));
mode = GameMode. LoadingDisplay;
this.Visible = true;
 
 
В этом коде в строке
 
 
PocketJack.images.loading.gif
 

переменная PocketJack означает имя проекта (или пространства имён),

а переменная images – имя папки, в которой имеется графический файл loading.gif.

После запуска игры, рисунок загрузки loading.gif появляется в середине экрана и находится на экране все время загрузки игровых компонентов, как определено в следующем коде:

 
void paintForm (Graphics g)
{
switch (mode)
{
case GameMode. LoadingDisplay:
//We draw all images below the menu:
g. DrawImage (
bankImage, 0, StayToolStripMenuItem. Height);
g. DrawImage (
loadingImage, 0, StayToolStripMenuItem. Height +60);
 
break;
 
case GameMode.PlacingBets:
g. DrawImage (bankImage, 0, StayToolStripMenuItem. Height);
Utilities.BigText («Bank: " + pot.PotValue.ToString (),
10, 40, Color. Black,
Color. Yellow, messageFont, g);
Utilities.BigText («Bet: " +
pot.BetValue.ToString (), 10, 80, Color. Black,
Color. Yellow, messageFont, g);
 
break;


На рис. 1.9 показан узкий слева – направо рисунок загрузки loading.gif с надписью «POCKET JACK LOADING» поверх фона игры в виде файла bank.jpg. Этот рисунок держится на экране всего несколько секунд, пока не загрузятся все графические и звуковые файлы, а затем исчезает, уступая место информации о размерах Банка и Ставки, показанных на следующем рисунке.



Рис. 1.9. Рисунок загрузки loading.gif


Разработку звукового сопровождения игры мы опишем далее при написании программы, а сейчас перейдем к правилам игры.

Возрастное ограничение:
12+
Дата выхода на Литрес:
19 октября 2022
Объем:
351 стр. 103 иллюстрации
ISBN:
9785005910844
Правообладатель:
Издательские решения
Формат скачивания:
epub, fb2, fb3, ios.epub, mobi, pdf, txt, zip

С этой книгой читают

Новинка
Черновик
4,9
167