‡ المحتويات‡
لعبة البناء بالقطع هى واحدة من أكثر اللعب شهرة و اللعبة الأصل برمجت و صممت بواسطة مبرمج روسى اسمه Alexey Pajitnov فى عام 1985 و منذ ذلك الحين أصبحت اللعبة موجودة على معظم أنظمة الكمبيوتر فى أشكال عديدة و حتى هاتفى المحمول به نسخة معدلة من اللعبة.
اللعبة عبارة عن لغز بناء بالقطع الساقطة و فيها سعبة أشكال مختلفة تسمى tetrominoes و هم S-shapeو Z-shape و T-shape و L-shape و Line-shape و MirroredL-shape و Square-shape و كل شكل يتكون من أربعة مربعات و تتساقط الأشكال فى لوحة اللعب و مهمة اللعبة هى تحريك و تدوير الأشكال حتى نكون صف كامل و عندها يحذف الصف و نحرز النقاط و نكمل اللعب حتى تمتلأ.
Figure: Tetrominoes
ليست لدينا صور لقطع البناء لذلك سوف نرسمها باستخدام دوال الرسم فى مكتبات WinFormsو بجوار أى لعبة كمبيوتر هناك نموذج حسابى و بالطبع سيكون فى لبعتنا واحد.
بعض الأفكار:
نستخدم عداد Timer لإنشاء دورة اللعب.
قطع اللعب تنزل إلى أسفل.
تتحرك الأشكال بالأساس مربع بمربع و ليس بكسل ببكسل.
رياضياً اللوحة عبارة عن قائمة بسيطة للأرقام
المثال القادم عبارة عن نسخة معدلة من اللعبة و متاح مع ملفات تثبيت PyQt4.
tetris.py #!/usr/bin/ipy import clr clr.AddReference("System.Windows.Forms") clr.AddReference("System.Drawing") clr.AddReference("System") from System.Windows.Forms import Application, Form, FormBorderStyle from System.Windows.Forms import UserControl, Keys, Timer, StatusBar from System.Drawing import Size, Color, SolidBrush, Pen from System.Drawing.Drawing2D import LineCap from System.ComponentModel import Container from System import Random class Tetrominoes(object): NoShape = 0 ZShape = 1 SShape = 2 LineShape = 3 TShape = 4 SquareShape = 5 LShape = 6 MirroredLShape = 7 class Board(UserControl): BoardWidth = 10 BoardHeight = 22 Speed = 200 ID_TIMER = 1 def __init__(self): self.Text = 'Snake' self.components = Container() self.isWaitingAfterLine = False self.curPiece = Shape() self.nextPiece = Shape() self.curX = 0 self.curY = 0 self.numLinesRemoved = 0 self.board = [] self.DoubleBuffered = True self.isStarted = False self.isPaused = False self.timer = Timer(self.components) self.timer.Enabled = True self.timer.Interval = Board.Speed self.timer.Tick += self.OnTick self.Paint += self.OnPaint self.KeyUp += self.OnKeyUp self.ClearBoard() def ShapeAt(self, x, y): return self.board[(y * Board.BoardWidth) + x] def SetShapeAt(self, x, y, shape): self.board[(y * Board.BoardWidth) + x] = shape def SquareWidth(self): return self.ClientSize.Width / Board.BoardWidth def SquareHeight(self): return self.ClientSize.Height / Board.BoardHeight def Start(self): if self.isPaused: return self.isStarted = True self.isWaitingAfterLine = False self.numLinesRemoved = 0 self.ClearBoard() self.NewPiece() def Pause(self): if not self.isStarted: return self.isPaused = not self.isPaused statusbar = self.Parent.statusbar if self.isPaused: self.timer.Stop() statusbar.Text = 'paused' else: self.timer.Start() statusbar.Text = str(self.numLinesRemoved) self.Refresh() def ClearBoard(self): for i in range(Board.BoardHeight * Board.BoardWidth): self.board.append(Tetrominoes.NoShape) def OnPaint(self, event): g = event.Graphics size = self.ClientSize boardTop = size.Height - Board.BoardHeight * self.SquareHeight() for i in range(Board.BoardHeight): for j in range(Board.BoardWidth): shape = self.ShapeAt(j, Board.BoardHeight - i - 1) if shape != Tetrominoes.NoShape: self.DrawSquare(g, 0 + j * self.SquareWidth(), boardTop + i * self.SquareHeight(), shape) if self.curPiece.GetShape() != Tetrominoes.NoShape: for i in range(4): x = self.curX + self.curPiece.x(i) y = self.curY - self.curPiece.y(i) self.DrawSquare(g, 0 + x * self.SquareWidth(), boardTop + (Board.BoardHeight - y - 1) * self.SquareHeight(), self.curPiece.GetShape()) g.Dispose() def OnKeyUp(self, event): if not self.isStarted or self.curPiece.GetShape() == Tetrominoes.NoShape: return key = event.KeyCode if key == Keys.P: self.Pause() return if self.isPaused: return elif key == Keys.Left: self.TryMove(self.curPiece, self.curX - 1, self.curY) elif key == Keys.Right: self.TryMove(self.curPiece, self.curX + 1, self.curY) elif key == Keys.Down: self.TryMove(self.curPiece.RotatedRight(), self.curX, self.curY) elif key == Keys.Up: self.TryMove(self.curPiece.RotatedLeft(), self.curX, self.curY) elif key == Keys.Space: self.DropDown() elif key == Keys.D: self.OneLineDown() def OnTick(self, sender, event): if self.isWaitingAfterLine: self.isWaitingAfterLine = False self.NewPiece() else: self.OneLineDown() def DropDown(self): newY = self.curY while newY > 0: if not self.TryMove(self.curPiece, self.curX, newY - 1): break newY -= 1 self.PieceDropped() def OneLineDown(self): if not self.TryMove(self.curPiece, self.curX, self.curY - 1): self.PieceDropped() def PieceDropped(self): for i in range(4): x = self.curX + self.curPiece.x(i) y = self.curY - self.curPiece.y(i) self.SetShapeAt(x, y, self.curPiece.GetShape()) self.RemoveFullLines() if not self.isWaitingAfterLine: self.NewPiece() def RemoveFullLines(self): numFullLines = 0 statusbar = self.Parent.statusbar rowsToRemove = [] for i in range(Board.BoardHeight): n = 0 for j in range(Board.BoardWidth): if not self.ShapeAt(j, i) == Tetrominoes.NoShape: n = n + 1 if n == 10: rowsToRemove.append(i) rowsToRemove.reverse() for m in rowsToRemove: for k in range(m, Board.BoardHeight): for l in range(Board.BoardWidth): self.SetShapeAt(l, k, self.ShapeAt(l, k + 1)) numFullLines = numFullLines + len(rowsToRemove) if numFullLines > 0: self.numLinesRemoved = self.numLinesRemoved + numFullLines statusbar.Text = str(self.numLinesRemoved) self.isWaitingAfterLine = True self.curPiece.SetShape(Tetrominoes.NoShape) self.Refresh() def NewPiece(self): self.curPiece = self.nextPiece statusbar = self.Parent.statusbar self.nextPiece.SetRandomShape() self.curX = Board.BoardWidth / 2 + 1 self.curY = Board.BoardHeight - 1 + self.curPiece.MinY() if not self.TryMove(self.curPiece, self.curX, self.curY): self.curPiece.SetShape(Tetrominoes.NoShape) self.timer.Stop() self.isStarted = False statusbar.Text = 'Game over' def TryMove(self, newPiece, newX, newY): for i in range(4): x = newX + newPiece.x(i) y = newY - newPiece.y(i) if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight: return False if self.ShapeAt(x, y) != Tetrominoes.NoShape: return False self.curPiece = newPiece self.curX = newX self.curY = newY self.Refresh() return True def DrawSquare(self, g, x, y, shape): colors = [ (0, 0, 0), (204, 102, 102), (102, 204, 102), (102, 102, 204), (204, 204, 102), (204, 102, 204), (102, 204, 204), (218, 170, 0) ] light = [ (0, 0, 0), (248, 159, 171), (121, 252, 121), (121, 121, 252), (252, 252, 121), (252, 121, 252), (121, 252, 252), (252, 198, 0) ] dark = [ (0, 0, 0), (128, 59, 59), (59, 128, 59), (59, 59, 128), (128, 128, 59), (128, 59, 128), (59, 128, 128), (128, 98, 0) ] pen = Pen(Color.FromArgb(light[shape][0], light[shape][1], light[shape][2]), 1) pen.StartCap = LineCap.Flat pen.EndCap = LineCap.Flat g.DrawLine(pen, x, y + self.SquareHeight() - 1, x, y) g.DrawLine(pen, x, y, x + self.SquareWidth() - 1, y) darkpen = Pen(Color.FromArgb(dark[shape][0], dark[shape][1], dark[shape][2]), 1) darkpen.StartCap = LineCap.Flat darkpen.EndCap = LineCap.Flat g.DrawLine(darkpen, x + 1, y + self.SquareHeight() - 1, x + self.SquareWidth() - 1, y + self.SquareHeight() - 1) g.DrawLine(darkpen, x + self.SquareWidth() - 1, y + self.SquareHeight() - 1, x + self.SquareWidth() - 1, y + 1) g.FillRectangle(SolidBrush(Color.FromArgb(colors[shape][0], colors[shape][1], colors[shape][2])), x + 1, y + 1, self.SquareWidth() - 1, self.SquareHeight() - 2) pen.Dispose() darkpen.Dispose() class Shape(object): coordsTable = ( ((0, 0), (0, 0), (0, 0), (0, 0)), ((0, -1), (0, 0), (-1, 0), (-1, 1)), ((0, -1), (0, 0), (1, 0), (1, 1)), ((0, -1), (0, 0), (0, 1), (0, 2)), ((-1, 0), (0, 0), (1, 0), (0, 1)), ((0, 0), (1, 0), (0, 1), (1, 1)), ((-1, -1), (0, -1), (0, 0), (0, 1)), ((1, -1), (0, -1), (0, 0), (0, 1)) ) def __init__(self): self.coords = [[0,0] for i in range(4)] self.pieceShape = Tetrominoes.NoShape self.SetShape(Tetrominoes.NoShape) def GetShape(self): return self.pieceShape def SetShape(self, shape): table = Shape.coordsTable[shape] for i in range(4): for j in range(2): self.coords[i][j] = table[i][j] self.pieceShape = shape def SetRandomShape(self): rand = Random() self.SetShape(rand.Next(1, 7)) def x(self, index): return self.coords[index][0] def y(self, index): return self.coords[index][1] def SetX(self, index, x): self.coords[index][0] = x def SetY(self, index, y): self.coords[index][1] = y def MaxX(self): m = self.coords[0][0] for i in range(4): m = max(m, self.coords[i][0]) return m def MinY(self): m = self.coords[0][1] for i in range(4): m = min(m, self.coords[i][1]) return m def RotatedLeft(self): if self.pieceShape == Tetrominoes.SquareShape: return self result = Shape() result.pieceShape = self.pieceShape for i in range(4): result.SetX(i, self.y(i)) result.SetY(i, -self.x(i)) return result def RotatedRight(self): if self.pieceShape == Tetrominoes.SquareShape: return self result = Shape() result.pieceShape = self.pieceShape for i in range(4): result.SetX(i, -self.y(i)) result.SetY(i, self.x(i)) return result class IForm(Form): def __init__(self): self.Text = 'Tetris' self.Width = 200 self.Height = 430 self.FormBorderStyle = FormBorderStyle.FixedSingle board = Board() board.Width = 195 board.Height = 380 self.Controls.Add(board) self.statusbar = StatusBar() self.statusbar.Parent = self self.statusbar.Text = 'Ready' board.Start() self.CenterToScreen() Application.Run(IForm())
لقد بسط اللعبة قليلاً لذلك فهى الآن سهلة الفهم تبدأ اللعبة فى الحال و بعدها يمكننا إيقاف اللعبة مؤقتاً عن طريق الضغط على الزر p و زر المسافة space key سوف يهبط القطعة فى الحال إلى القاع و الزر d سوف يهبطها خطوة لأسفل و يمكن استخدامه لتسريع الهبوط قليلاً و عموماً اللعبة تسير فى سرعة ثابتة و لا يوجد تسريع و النقاط التى نحرزها هى عدد السطور التى حذفناها.
class Tetrominoes(object): NoShape = 0 ZShape = 1 SShape = 2 LineShape = 3 TShape = 4 SquareShape = 5 LShape = 6 MirroredLShape = 7
هناك سبع أنواع مختلفة من قطع البناء.
... self.curX = 0 self.curY = 0 self.numLinesRemoved = 0 self.board = [] ...
قبل البدأ فى اللعب سوف ننشأ بعض المتغيرات المهمة فالمتغير self.board هو قائمةTetrominoes و يمثل موقع الشكل و يثبته على اللوحة المحددة للعب.
def ClearBoard(self): for i in range(Board.BoardHeight * Board.BoardWidth): self.board.append(Tetrominoes.NoShape)
الدالة clearBoard() تنظف لوحة اللعب و تملاأ المتغير slef.board بالقيمة Tetrominoes.NoShape أى لا يوجد أشكال.
الرسم فى اللعبة يتم فى الدالة OnPaint().
for i in range(Board.BoardHeight): for j in range(Board.BoardWidth): shape = self.shapeAt(j, Board.BoardHeight - i - 1) if shape != Tetrominoes.NoShape: self.drawSquare(g, 0 + j * self.squareWidth(), boardTop + i * self.squareHeight(), shape)
الرسم فى اللعبة ينقسم إلى مرحلتين و فى المرحلة الأولى نرسم كل الأشكال أو الأشكال المتبقية و التى سقطت فى لوحة اللعب و كل المربعات موجودة داخل القائمة self.board و نصل إليها عن طريق الدالة ShapeAt()
if self.curPiece.shape() != Tetrominoes.NoShape: for i in range(4): x = self.curX + self.curPiece.x(i) y = self.curY - self.curPiece.y(i) self.drawSquare(g, 0 + x * self.squareWidth(), boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(), self.curPiece.shape())
المرحلة القادمة هى رسم القطع التى سوف تسقط داخل اللوحة.
فى الدالة OnKeyUp() نتفحص المفتاح المضغوط.
elif key == Keys.Left: self.tryMove(self.curPiece, self.curX - 1, self.curY)
إذا ضغطنا على زر الإتجاهات الأيسر قوسف نحاول تحريك القطعة إلى أٍفل و قلنا هنا نحاول لأنه من الممكن أن لا تتحرك القطعة.
فى الدالة TyrMove() نحاول تحريك الأشكال و إذا لم تتحرك نرجع القيمة False.
for i in range(4): x = newX + newPiece.x(i) y = newY - newPiece.y(i) if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight: return False if self.ShapeAt(x, y) != Tetrominoes.NoShape: return False
إذا كان الشكل على حافة الحدود أو مزاحماً لأى قطة أخرى نرجع القيمة False.
self.curPiece = newPiece self.curX = newX self.curY = newY self.Refresh() return True
غير ذلك فسوف نغير مكان القطعة الحالة إلى مكان جديد و نرجع القيمة True.
def OnTick(self, sender, event): if self.isWaitingAfterLine: self.isWaitingAfterLine = False self.NewPiece() else: self.OneLineDown()
فى الدالة OnTick() ننشأ أيضاً قطعة جديدة بعدما تصتدم الأخيرة بالقاع أو نحرك القطعة الساقطة لسطر واحد.
إذا صدمت القطعة القاع فسوف نستدعى الدالة RemoveFullLines و قبل ذلك نجد السطور المملؤة.
rowsToRemove = [] for i in range(Board.BoardHeight): n = 0 for j in range(Board.BoardWidth): if not self.ShapeAt(j, i) == Tetrominoes.NoShape: n = n + 1 if n == 10: rowsToRemove.append(i)
نتفحص اللوحة فالصف يمكنه أن يحوى عشرة قطع من الأِشكال و إذا كان مملؤ أى أن العدد n = 10 نخزن رقم السطر تجهيزاً للحذف.
rowsToRemove.reverse() for m in rowsToRemove: for k in range(m, Board.BoardHeight): for l in range(Board.BoardWidth): self.SetShapeAt(l, k, self.ShapeAt(l, k + 1))
هذه السطور من الأكواد تحذف السطور المكتملة و نعكس الترتيب فى قائمة الصفوف المحذوفة rowsToRemove لذلك سوف نبدأ بأول صف مكتمل من أسفل و ما فعلناه هو إزالة سطر مملوء و تحريك كل الصفوف لأسفل سف واحد و هذا يحدث لكل الصفوف المملوؤة و فى حالتنا سنتخدم الجاذبية الطبيعية native gravity و هذا يعنى أن أى قطعة سوف تحل فى المكان الفارغ أسفل منها.
def NewPiece(self): self.curPiece = self.nextPiece statusbar = self.Parent.statusbar self.nextPiece.SetRandomShape() self.curX = Board.BoardWidth / 2 + 1 self.curY = Board.BoardHeight - 1 + self.curPiece.MinY() if not self.TryMove(self.curPiece, self.curX, self.curY): self.curPiece.SetShape(Tetrominoes.NoShape) self.timer.Stop() self.isStarted = False statusbar.Text = 'Game over'
اادلة NewPiece() عشوائياً تنشأ فقطعة جديدة و إذا لم تذهب القطعة فى المكان المحد سوف ترجع الدالة TryMove() القيمة False و تنتهى اللعبة.
colors = [ (0, 0, 0), (204, 102, 102), ... ] light = [ (0, 0, 0), (248, 159, 171), ... ] dark = [ (0, 0, 0), (128, 59, 59), ... ]
هناك ثالثة قوائم للأاون و هذه القوائم تخزن قيم الألوان التى سوف تستخدم فى تلوين القطع المربعة و لكل قطعة لون معين و القائمتين light و dark تسجل الألوان للخطوط التى سوف تظهر المربع بالشكل ثلاثى الأبعاد و هذه الألون متساوية فقط الفرق فى درجتها و سوف نرسم خطين باللون الفاتح فى أعلى يسار المربعات و خطين باللون الغامق فى أسفل يمين المربعات.
g.DrawLine(pen, x, y + self.SquareHeight() - 1, x, y) g.DrawLine(pen, x, y, x + self.SquareWidth() - 1, y)
هذان السطران يرسمان الخطين باللون القاتح للمربع.
و الفئة Shape تخزن المعلومات عن القطة..
self.coords = [[0,0] for i in range(4)]عند الإنشاء ننشأ قائمة إحداثيات فارغة و التى سوف نسجل فيها إحداثيات القطع و كمثال هذه النقط (0, -1), (0, 0), (1, 0), (1, 1) تمثل شكل مدور من نوع S-Shape و الرسم التوضيحى التالى يوضح الشكل.
Figure: Coordinates
عند رسم القطعة الساقطة الحالية نرسمها فى الموقع self.curX و self.curY و يعد ذلك ننظر إلى جدول الإحداثيات و نرسم الأربعة مربعات.
الدالة ()RotateLeft تدور القطعة إلى اليسار.
if self.pieceShape == Tetrominoes.SquareShape: return self
إذا كان لدينا شكل من نوع Tetrominoes.SquareShape أى مربع لا نفعل شىء فالشكل هو نفسه.
result = Shape() result.pieceShape = self.pieceShape for i in range(4): result.SetX(i, self.y(i)) result.SetY(i, -self.x(i)) return result
فى الحالات الأخرى نغير الإحداثيات للقطعة و لتفهم هذا الكود أنظر إلى الرسم التوضيحى بالأعلى.
Figure: Tetris
‡ المحتويات‡