视频扑克(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/video-poker-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 15 分钟阅读 - 7289 个词 阅读量 0视频扑克(译文)
原文地址:https://www.codeproject.com/Articles/1187548/Video-Poker
原文作者:Robert Welliever
译文由本站 robot-v1.0 翻译
前言
Classic, Arcade-Style Video Poker
经典的街机风格视频扑克
介绍(Introduction)
从过去的LCD手持设备到当今赌场的街机游戏,我都喜欢简单的扑克游戏.找不到在CodeProject上代表的可访问视频扑克,我想与您,我的技术朋友分享我最喜欢的经典版本的实现.(I’ve enjoyed the simple gameplay of poker from the LCD handhelds of yesteryear to the arcade machines in casinos of today. Not finding an accessible Video Poker represented on CodeProject I wanted to share an implementation of my favorite classic version with you, my technology friends.)
使用(To Use)
开始,(To begin,) 下载VideoPoker.zip(Download VideoPoker.zip) 然后解压缩.在提取的Video Poker目录中,您将找到三个代码文件; VideoPoker.js,VideoPoker.css.和VideoPoker.html,以及两个包含音频和图像文件的资源目录.如果您具有丰富的编程经验,则可以轻松地跳至修改代码.它相对简短,易读且评论丰富,有助于理解.只需在文本编辑器中打开代码文件以进行读取或修改,或在任何浏览器中打开VideoPoker.html即可运行.(and unzip. Within extracted directory Video Poker you will find three code files; VideoPoker.js, VideoPoker.css. and VideoPoker.html, and two resource directories containing audio and image files. If you have depth of programming experience, you may comfortably skip to modifying code. It is relatively short, readable and heavily commented to aid comprehension. Just open code files in a text editor to read or modify, or open VideoPoker.html in any browser to run.)
建筑(Architecture)
和其他许多人一样,我同时使用不同的硬件堆栈在大型和大型显示器上玩游戏.为了促进这种分辨率和设备多样性,我选择了全天然Javascript,HTML和CSS.(As many others, I play games on both small and large displays while riding different hardware stacks. In order to facilitate this resolution and device diversity, I selected all-natural Javascript, HTML and CSS.) 为了以不同的分辨率渲染,HTML和CSS提供了合格的帮助.尽管许多游戏使用设置为窗口大小的单个绘制表面,但是这样做可能需要费力的代码才能以不同的分辨率保持绘制对象的位置和布局状态.取而代之的是,该游戏将绘制的区域限制在下注的手中,然后利用HTML的内置布局技巧和CSS来处理其余的界面.结果,该实现方案在窄至480像素的窗口或宽得多的窗口(例如1200像素)的窗口中都能很好地运行,同时保留了较轻的代码.(To render at different resolutions, HTML and CSS provide qualified help. While many games use a single draw surface set to the window size, doing so may require arduous code to maintain positional and layout state of drawn objects at different resolutions. Instead, this game limits the drawn area to the dealt hand, and then leverages the baked-in layout skill of HTML with CSS to handle the remaining interface. As a result, this implementation plays well in a window as narrow as 480 pixels, or in much wider windows (e.g. 1200px) while retaining lighter-weight code.) 为了促进不同设备之间的均等性能,屏幕更新是事件驱动的,而不是连续渲染的.连续渲染要求游戏以无限循环执行,在计算状态和渲染帧(通常以每秒帧数为单位)之间快速切换.或者,该游戏仅响应交互而呈现更新.这种事件驱动的设计对硬件的压力较小,因为UI仅根据用户的响应进行计算和绘制.(To facilitate equal performance between differently-abled devices, screen updates are event-driven rather than rendered continuously. Continuous rendering requires games execute in infinite loop, rapidly switching between calculating state and rendering frames (often measured in frames-per-second). Alternatively, this game renders updates only in response to interaction. This event-driven design puts less stress on hardware because the UI calculates and draws only in response to the user.) 因此,通过使用HTML进行结构化并使用事件驱动的UI更新,可以说我们在VideoPoker.html,VideoPoker.js和VideoPoker.css之间舒适地分配了高性能,响应式代码.顺便说一句,此模型在游戏应用程序之外还可以很好地工作.如果我们不包含较重的音频和图像文件,则可以在页面中运行此游戏作为交互式广告(例如赌场联属营销),或者作为启动/加载屏幕在更密集的应用程序加载时吸引用户.(So by structuring with HTML and using event-driven UI updates we arguably have performant, responsive code split comfortably between VideoPoker.html, VideoPoker.js, and VideoPoker.css. On a side note, this model works well beyond gaming applications. If we didn’t include our heavier audio and image files, we might run this game in a page as interactive advertising (e.g. casino affiliate marketing), or as a splash/loading screen to engage a user while a more intensive application loads.)
码(Code)
打开Javascript文件VideoPoker.js后,我们首先遇到一个自我描述的枚举(Upon opening the Javascript file, VideoPoker.js, we first meet a self-describing enumeration)游戏状态(GameStates)列出游戏的状态:(listing the game’s states:)
var GameStates = { // Game state enumeration
Uninitialized: 0,
FirstDeal: 1,
SecondDeal: 2,
HandLost: 3,
HandWon: 4,
GameOver: 5
}
该应用程序开始于(The application begins in)游戏状态(GameStates).(.)未初始化(Uninitialized),最高(, with a topmost)<(<)div(div)>(>)覆盖游戏窗口的元素.这个最上方的加载屏幕会掩盖界面,直到资产初始化为止.每个加载的资源(图像或音频)都会触发一个加载完成处理程序,该处理程序会减少加载资源的全局计数.最后加载的资源删除了(element covering the game window. This topmost loading screen masks the interface until assets are initialized. Each loaded resource (image or audio) fires a load-complete handler that decrements a global count of loading resources. The last loaded resource drops the)<(<)div(div)>(>)允许玩.(to allow play.) 向下移动Javascript,我们发现了一些定义游戏玩法的全局常量和属性,其中一些看起来像:(Moving down the Javascript, we find a block of global constants and properties defining gameplay, some of which look like:)
var _GameState = GameStates.Uninitialized; // Initial game state
var _StartCredits = 100; // Number of starting credits
var _Credits = _StartCredits; // Number of current credits
var _CurrentBet = 1; // Amount of bet
...
在此之下,我们有三个对象组成了我们的主要数据结构,(And below that, we have a trio of objects that comprise our main data structures,)甲板(Deck),(,)手(Hand)和(, and)卡(Card).一种(. A)甲板(Deck)对象代表标准的52张扑克牌组,并包含预期的功能,例如(object represents a standard fifty-two card poker deck, and contains expected functions such as)随机播放(Shuffle)和(and)成交(Deal).(.)
var Deck = { // Deck Object - A 52 card poker deck
Cards: null, // Array of Card objects representing one deck
Shuffled: null, // Array of Card objects representing a shuffled deck
SpriteSheet: null, // Image object of uncut card deck
SpriteWidth: 230, // pixel width of card in source image
SpriteHeight: 300, // pixel height of card in source image
Initialize: function () {...},
Shuffle: function () {...},
Deal: function (numCards) {...}
...
的(The)甲板(Deck)还包含对卡片图像的引用,这些图像作为单个图像资产(称为"(also contains a reference to the card images, which are chunked as a single image asset called a)精灵表(sprite sheet).从本质上讲,我们使用Sprite工作表是因为(. Essentially, we use a sprite sheet because one)图片(Image)对象需要的处理程序代码少于52个单独的处理程序代码(object requires less handler code than fifty-two separate)图片(Image)对象.为了可视化,我们的卡片精灵看起来像这样:(objects. For visualization, our card sprites look something like this:)
发出的卡片(Cards dealt from the)甲板(Deck)去(go to the)手(Hand)保持玩家五个活动状态的对象(object which holds the player’s five active)卡(Card)对象.(objects.)
function Hand(cards) { // Hand object - The player's active Card objects
this.Cards = cards; // Array of Card objects
this.Evaluate = function () {...} // Return ID of winning hand type, or -1 if losing hand
this.IsRoyal = function () {...}
this.IsFullHouse = function () {...}
this.IsFourOfAKind = function () {...}
this.IsFlush = function () {...}
this.IsStraight = function () {...}
this.IsThreeOfAKind = function () {...}
this.IsTwoPair = function () {...}
this.IsJacksOrBetter = function () {...}
...
的(The)手(Hand)对象还提供了用于检查获胜情况的实例例程(例如同花顺,满堂红,平直).需要注意的一点是,检查获胜手的总体过程尚未优化.例如,(object additionally serves instance routines for checking winning-ness (e.g. flush, full-house, straight). A point to note is that the overall procedure for checking a winning hand is not optimized. For example, the)**满屋(IsFullHouse)**和(and)**是直的(IsStraight)**例行程序都需要排序卡片作为评估的一部分.而不是通过排序(routines both require sorted cards as part of their evaluation. Instead of passing a sorted)卡(Card)数组,每个例程将对卡片进行冗余排序.这种低效率是有目的的,因此每个例程都在逻辑上进行了封装,希望可以使初学者更容易修改它.同样,围绕扑克规则的解释可能存在争议.例如,应该(array, each routine instead sorts the cards redundantly. This inefficiency was taken purposefully so that each routine is logically encapsulated, hopefully making it more modifiable for beginners. Also, there may be points of controversy surrounding interpretation of poker rules. For example, should the)对偶(IsTwoPair)如果一手包含四个King,则函数返回true?从技术上讲,是的,所以我用这种方式进行了编码.其他人可能会选择不同的方式.举手的优先权使这成为实际代码执行中的争议点,但我想我会指出存在主观性.(function return true if a hand contains four Kings? Technically, yes, so I coded it that way. Others may choose differently. The precedence of winning hands makes this a moot point in actual code execution, but thought I would note that subjectivity exists.) 前进,(Moving along, the)卡(Card)对象是纯粹的结构,看起来很像这样:(object is purely structural, looking a lot like this:)
function Card(id, suit, rank, x, y, width, height) { // Represents a standard playing card.
this.ID = id; // Card ID: 1-52
this.Suit = suit; // Card Suit: 1-4 {Club, Diamond, Heart, Spade}
this.Rank = rank; // Card Rank: 1-13 {Ace, Two, ..King}
this.X = x; // Horizontal coordinate position of card image on sprite sheet
this.Y = y; // Vertical coordinate position of card image on sprite sheet
this.Width = width; // Pixel width of card sprite
this.Height = height; // Pixel height of card sprite
this.Locked = false; // true if Card is Locked/Held
this.FlipState = 0; // The flip state of card: 0 or 1 (Back Showing or Face Showing)
}
现在,警报编码人员可能已经注意到(Now alert coders may have already noticed that the)甲板(Deck),(,)手(Hand)和(, and)卡(Card)在适当的面向对象环境中,对象将是它们自己的类文件.在这样的小型应用程序中,为了方便理解架构,我选择将所有Javascript混搭到单个文件中.您可以通过进一步开发将其分解.(objects would be their own class files in a proper object-oriented environment. In a small application like this, I have chosen to mash all Javascript to a single file for expedience in understanding architecture. You might break it apart with further development.) 继续,我们发现像(Continuing on, we find a spattering of handlers like)_DealClick(_DealClick)和(and)_赌注(_Bet)在玩家互动时执行.由于代码应提供足够的上下文,因此我不会在本文中完整描述这些例程,但是我们现在来看一看.的(that execute on player interaction. I will not fully describe these routines in this article as the code should provide sufficient context, but we might look at one now. The)_赌注(_Bet)例行程序通过调整玩家的信用,播放相关的声音效果,然后更新UI来响应"下注"或"下注"动作.希望这些步骤在例程本身中很简单,即:(routine responds to a “bet up” or “bet down” action by adjusting the player’s credits, playing a related sound effect, and then updating the UI. These steps are hopefully plain within the routine itself, i.e.:)
function _Bet(action) {
if (_GameState !== GameStates.FirstDeal &&
_GameState !== GameStates.HandWon &&
_GameState !== GameStates.HandLost)
return; // Only allow bet before being dealt
if (action === '-') { // Bet down requested
if (_CurrentBet > 1) { // Govern minimum bet
_CurrentBet -= 1; // Decrement bet
GameAudio.Play('BetDown');
}
}
else if (action === '+') { // Bet up requested
if (_CurrentBet < 5 && _CurrentBet < _Credits) { // Govern maximum bet
_CurrentBet += 1; // Increment bet
GameAudio.Play('BetUp');
}
}
_UpdateBetLabel();
_UpdateCreditsLabel();
}
此外,在代码中我们找到了与绘制元素相关的功能.玩家的五个(Also in the code we find functions related to drawn elements. The player’s five)卡(Card)填充对象(objects filling the)手(Hand)是界面的绘制部分,由HTML的(are a drawn part of the interface, handled by HTML’s)帆布(Canvas)目的.通过从我们的实例获取图形上下文(object. By obtaining the graphics context from our instance of)帆布(Canvas),我们可以用它渲染.这是我们最外面的绘制例程:(, we may render with it. Here is our outermost draw routine:)
function _DrawScreen() { // Render UI update
if (_GameState == GameStates.Uninitialized) // Redrawn only if loading screen is down
return;
var g = _Canvas.getContext('2d'); // Graphics context
g.clearRect(0, 0, _Canvas.width, _Canvas.height); // Wipe frame clean
for (var i = 0; i < _Hand.Cards.length; i++) { // for each Card in Hand
if (_Hand.Cards[i].FlipState === 1)
_DrawCardFace(g, i); // FlipState == 1
else
_DrawCardBack(g, i); // FlipState == 0
if (_GameState === GameStates.SecondDeal && _Hand.Cards[i].Locked) // Second deal
_DrawCardHold(g, i); // Card is locked by player
}
_UpdateBetLabel(); // Refresh html bet elements
_UpdateCreditsLabel(); // Refresh html credits elements
if (_GameState == GameStates.HandLost || _GameState == GameStates.HandWon)
_DrawHandOverMessage(g);
}
检查完我们处于允许绘图的状态后,我们调用(After checking we are in a state where drawing is allowed, we call)getContext(getContext)得到我们的图形上下文(to get our graphics context)G(g).用(. With)G(g)我们首先通过调用其本机函数来清洁绘图表面(we first clean the draw surface by calling its native function)clearRect(clearRect)并提供清晰的几何边界.然后我们调用自己的绘制例程(and supplying the geometric bounds to clear. We then call our own draw routines)_DrawCardFace(_DrawCardFace)要么(or)_DrawCardBack(_DrawCardBack)取决于(depending on the)翻转状态(FlipState)的(of the)卡(Card).我们以绘制辅助效果和更新HTML元素结束.如果我们看一下常规(. We finish with drawing ancillary effects and updating HTML elements. If we look at the routine)_DrawCardBack(_DrawCardBack)我们可能了解一些实际的渲染:(we may understand some actual rendering:)
function _DrawCardBack(g, cardIndex) {
g.save(); // Push styling context
g.fillStyle = '#300'; // Set dark red card back
var cardX = _HandX + (cardIndex * (_CardWidth + 4) + 4); // Card x position (4px buffer)
g.fillRect(cardX, 0, _CardWidth, _CardHeight); // Render card back
g.restore(); // Pop styling context
}
我们称之为(We call)救(save)将当前的样式上下文推入内存,然后调用(to push the current styling context into memory, and call)恢复(restore)弹出来.在这种情况下,我们仅设置(to pop it back out. In this case, we are only setting the)fillStyle(fillStyle)属性,因此我们可以只重置该特定属性并删除对(property, so we might just reset that specific property and remove any need to)救(save)和(and)恢复(restore)整个样式背景,但我发现总是在打电话(the entire styling context, but I have found always calling)救(save)和(and)恢复(restore)在设置样式属性之前,可以提供一个标准化的标准,以防止错误并增强可读性.(before setting style properties provides a standardization that prevents bugs and enhances readability.) 除了手动绘制之外,我们还利用内置功能进行UI渲染.当前有一个特殊的线程(Beyond manual drawing, we also utilize an in-built function for UI rendering. There is a special thread from the current)窗口(Window)每次经过一段时间后都会执行特定的代码块.我们可以使用此间隔功能来处理每次获胜时出现的奖选框上的闪烁效果.每次执行该功能时,我们交替上蜡和下蜡,切换奖品行的CSS:(that executes a specific block of code each time a span of time has elapsed. We can use this interval function to handle a blinking effect on the prize marquee that occurs with each win. With each execution of the function, we alternate wax on and wax off, toggling the prize row’s CSS:)
function _PrizeWinBlink() // Handles marquee blink on winning prize row
{
_BlinkOn = !_BlinkOn; // Toggle the effect
var rowStyle = document.getElementById('row' + _WinID).style; // Winning prize row's style property
rowStyle.color = _BlinkOn ? '#fff' : '#fc5'; // white to yellow
rowStyle.textShadow = _BlinkOn ? '0 0 1px #fff' : '0 0 10px #a70'; // Toggle white to yellow shadow
}
我们通过调用设置此间隔函数(We set this interval function by calling)setInterval(setInterval)并提供处理程序和两次执行之间的时间间隔(即(and supplying a handler and the interval of time between executions (i.e.)setInterval(_PrizeWinBlink,400)(setInterval(_PrizeWinBlink, 400)),每隔400毫秒执行一次以上代码).当终止当前(, which executes the above code after each four hundred millisecond span). While termination of the current)窗口(Window)理论上也可以终止任何运行间隔函数,我注意到并非总是如此.尽管广泛支持(theoretically also terminates any running interval functions, I have noticed this is not always the case. Despite widespread support for the)窗口(Window)对象,但尚未完全标准化.我可以在多个浏览器窗口的条件下重现孤立的区间函数的实例.我们可以通过保留对每个间隔函数的引用来缓解这种情况,然后在(object, it is not fully standardized. I can reproduce instances of orphaned interval functions under conditions of multiple browser windows. We can mitigate this by keeping a reference to each interval function, and then forcing shut down when the)窗口(Window)尝试正常卸载:(attempts to unload normally:)
window.onbeforeunload = function () {
if (_PrizeWinThread != null) // If marquee blinking effect is running when app is closing
clearInterval(_PrizeWinThread); // Terminate
};
跳转到Javascript的底部是我要解决的最后一小段代码,(Jumping to the bottom of the Javascript is the last bit of code I want to address, the)游戏音频(GameAudio)对象,用于处理游戏的不同(object, which handles the game’s different)音讯(Audio)对象.(objects.) 的(The)音讯(Audio)object是每个供应商实现的本地Javascript对象.这等同于摇摆不定的音频支持.为了确保我们可以在不同的配置中听到音频效果,我们需要同一文件的多种编码.对于游戏中的每种音效,都会创建三个版本; OGG,MP3和WAV.为了帮助选择我们需要的版本,每个供应商都必须回答该问题,您可以播放此媒体类型(通常称为MIME类型)吗?有趣的是,Firefox(和其他)一起报告(object is a native Javascript object implemented per vendor. This equates to wavering audio support. To ensure we have audio effects that we can hear across different configurations we need multiple encodings of the same file. For each sound effect in the game, three versions are created; OGG, MP3, and WAV. To help select which version we need, each vendor is required to respond to the question, can you play this media type (often called a MIME type)? Interestingly, Firefox (as others), reports back)**大概(probably)**如果指定的媒体类型似乎可播放,(if the specified media type appears to be playable,)**也许(maybe)**如果无法确定媒体类型是否可以播放而不播放它,或者如果指定的媒体类型绝对不能播放,则返回一个空字符串((if it cannot tell if the media type is playable without playing it, or an empty string if the specified media type definitely cannot be played () https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/canPlayType(https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType) ).我发现非空响应有点古怪,我做了一些跨浏览器的实验,发现空字符串是一个合理的指标.通过使用以下顺序选择媒体类型,我们可以说具有很好的兼容性,同时可以考虑质量/尺寸比:(). Finding the non-empty responses a bit wonky, I did some cross-browser experimentation and found the empty string to be a reasonable indicator. By picking the media type using the following order we arguably have good compatibility while factoring quality/size ratio:)
var audio = new Audio();
var oggCapable = audio.canPlayType('audio/ogg') !== ''; // Check for OGG Media type
var mp3Capable = audio.canPlayType('audio/mpeg') !== ''; // Check for MPEG Media type
var wavCapable = audio.canPlayType('audio/wav') !== ''; // Check for WAV Media type
var extension = oggCapable ? 'ogg' : mp3Capable ? 'mp3' : wavCapable ? 'wav' : '';
...
因为(Because an)音讯(Audio)对象包装一个声音,我们需要多个声音(object wraps one sound, we require multiple)音讯(Audio)相同声音的对象可以实现重叠,异步播放的特定效果.例如,用户可以在一秒钟内单击两次" Bet Up"按钮.如果单(objects of the same sound to achieve overlapping, asynchronous play of a particular effect. For example, a user may click the “Bet Up” button twice in one second. If a single)音讯(Audio)物体持续播放一秒钟,那么播放器将不会在第二次点击时听到第二声.我们必须使用多个(object one second in duration played, then the player would not hear a second sound on the second click. We must use multiple)音讯(Audio)每个效果的对象,使我们能够缓冲声音并实现重叠播放.我们只是检索一个效果的缓冲区,增加其缓冲区索引(或者在缓冲区末尾设置为开始),然后播放它:(objects per effect, allowing us to buffer sound and achieve overlapping play. We just retrieve an effect’s buffer, increment its buffer index (or set to beginning if at end of buffer), and then play it:)
var buffer = this._SoundEffects[soundName]; // Get the buffer per sound effect
var bufferIndex = this._SoundEffects[soundName + "I"]; // Get buffer's current index
bufferIndex = bufferIndex === buffer.length - 1 ? 0 : bufferIndex + 1; // Increment or reset if at end
this._SoundEffects[soundName + "I"] = bufferIndex; // Set buffer index
buffer[bufferIndex].play(); // Play sound effect at buffer index
...
基本上就是这样.混合一些数据结构,事件处理程序,绘制例程以及弹出视频扑克.在本文中,我假设Java代码和大多数HTML/CSS的内容都比假设的说明容易阅读.如果我错了并且您有紧迫的问题,请在下面的评论中回答.否则,感谢您的关注和愉快的编码!(So that’s largely it. Mix a few data structures, event handlers, draw routines and out pops Video Poker. I have glossed over sections of Javascript and most of the HTML/CSS in this article assuming code reads easier than explanations of code. If I am wrong and you have pressing questions, I might answer them in the comments below. Otherwise, thanks for your attention and happy coding!)
历史(History)
- 2017年5月20日-初始发布(May 20, 2017 - Initial publish)
- 2017年5月21日-修复文章中损坏的图片链接(May 21, 2017 - Fixed broken image links in article)
- 2017年5月24日-更新了Javascript文件和文章以准确反映DSAlCoda在以下评论中建议的扑克惯例(May 24, 2017 - Updated Javascript file and article to accurately reflect poker conventions as suggested by DSAlCoda in comments below)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
CSS HTML Javascript game 新闻 翻译