Retro Game Lights Out!

Retro Game Lights Out!

Retro Game Lights Out!

At my age I fondly recall many retro games as cutting edge technologies. While no longer cutting edge, some games are still just as much fun to play as when they were first released. There are a lot of retro-games out on the web. Many written in Python, C and C++. Some of the old games written in GWBasic, Apple Basic or QuickBasic can still be found on the internet. So why would I want write a retro game using Gambas?

Well, I think that these early games gave many of us our start in programming and computer science. The hours and even days spent toiling away as we hacked at our keyboards trying to get a game to work educated us in the art of programming. The many games that were shared among the early computer hobbyist and professionals alike, spread knowledge across the entire domain. Because of this I feel these early games are still a valuable learning tool for new Gambas users, and they are fun as well! So here I present a simple rendition of the Lights Out game.

In Lights Out, the game is played on 5×5 grid of buttons. When a button is clicked, its state (on/off) is toggled along with the state of it’s four cardinal neighbors. If the button is on, it will be turned off. If the button is off, it will be turned on. For example, imagine a 5×5 grid of buttons.  Clicking the center button will toggle it’s state, and the state of the buttons directly above, below, to the left and to the right. Five buttons in total.

The object of the game is to turn all the lights out on the board in as few moves as possible. Many puzzles can be solved in 15 steps, some fewer steps, and others require many more. It may sound simple but it is a challenging task until you play several games and build some intuition.

Cardinal Neighbors Image
fig. 1 – Selected Cardinal Neighbors of Center Position.

The game can be quite addicting. I spent hours playing it while developing the code for this article. Even when finished I wasted a few hours trying different strategies. Figure 1 show the center position and it’s four cardinal neighbors in the On state. Figure 2 demonstrates a cleared board. This is the Winning Position! Figures 3 and 4 show how the corners interact with the board.  You’ll notice that the when a top corner position is selected, it wraps around to the bottom corner.

Linghts Out Winning Board Image
fig. 2 – To win you must turn out all positions on the board.

Ok, to get started you will need to create a new GUI project in Gambas 3. Your form should be size to 304 wide by 384 in height. Next, add a label for the number of tries. Set the text value of this label to “Tries”. Now add a TexBox abnd change it’s name to tbTries and set the text value to “0”.

It is possible to create unsolvable games. Though I have been able to solve most. There are starting configurations that have known solutions. I have included 8 of the 1000+ that have been documented. However, I still like playing the randomly generate puzzles. So I included a Checkbox for selecting whether to use one of these known solvable starting position or randomly generate the puzzle.  To handle this we need to add a Checkbox and set it’s name to cbUseStoredGames and it’s text value to “Use Stored Games”.

Finally, we need a Button so we can start a new game. So add a Button to the form and set its text to “New Game”.

Cornet-Cases-Bottom
fig. 3 – Corner Case Bottom
Corner-Cases-Top
fig. 4 – Corner Case Top
Lights-Out-New-Game Initial State Example
fig. 5 – Initial State Example

 

Now open the MForm code view and enter the text shown in listing 1. This is not the cleanest code and there may be better ways of implementing this game. As always Gambas provides many ways to skin the proverbial cat.

 

listing 1
  1. ' Gambas class file
  2.  
  3.  
  4. Public iColorFalse As Integer
  5. Public iColorTrue As Integer
  6.  
  7. Private aLbls As Label[]
  8.  
  9. Private ActiveIndex As Integer
  10. Private ActiveState As Boolean
  11.  
  12. Private ActiveGame As Boolean
  13. Private bUseStoredGames As Boolean
  14.  
  15. Private NumberOfStoredGames As Integer
  16. Private MaxStartingPositions As Integer
  17.  
  18. Private iRowSize As Integer = 5
  19. Private iColSize As Integer = 5
  20.  
  21. Public Sub _new()
  22.  
  23. End 
  24.  
  25.  
  26. ' Set up initial conditions
  27. Public Sub Form_Open()
  28.   aLbls = New Label[(iRowSize * iColSize)]
  29.   ActiveState = Int(False)
  30.   ActiveIndex = 0
  31.   iColorFalse = &H006600
  32.   iColorTrue = &H00FF00
  33.   bUseStoredGames = False
  34.   NumberOfStoredGames = 8
  35.   MaxStartingPositions = 16
  36.  
  37.   Me.Center
  38.   ResetBoard()
  39.   NewGame()
  40. End
  41.  
  42.  
  43. ' Clear board
  44. Private Sub ResetBoard()
  45.   Dim i, cellSize, cellSpace As Integer
  46.   Dim rowOffset, colOffset As Integer
  47.   Dim GridSize As Integer
  48.  
  49.   ActiveGame = False
  50.   tbTries.Text = "0"
  51.   gridSize = 5 ' Five cells Horz and Vert
  52.   cellSize = 50 ' cell is 50 x 50 px
  53.   cellSpace = 6 '6px between cells
  54.   rowOffset = 8 '8px down from top
  55.   colOffset = 15 '16px right from 0
  56.  
  57.   'Draw board
  58.   For i = 0 To 24
  59.     aLbls[i] = New Label(Me) As "Labelgroup"    ' Create a new label in the array and store it in an action group
  60.     With aLbls[i]
  61.       .X = ((i Mod gridSize) * (cellSize + cellSpace)) + colOffset 'Calculate column placement 
  62.       .Y = (i \ gridSize) * (cellSize + cellSpace) + rowOffset 'Calculate row placement
  63.       .Width = cellSize
  64.       .Height = cellSize
  65.       .Tracking = True
  66.       .Background = iColorFalse
  67.       .Tag = "idx:" & i & ";state:" & False ' Use tag to store state information
  68.       .Enabled = True
  69.     End With
  70.   Next  
  71. End
  72.  
  73.  
  74. ' Initialize a new game
  75. Private Sub NewGame()
  76.   Dim count, i, j As Integer
  77.   Dim pzl, pval As String
  78.   Dim pvals As String[]
  79.   Dim lbl As Label
  80.   Randomize
  81.   ' Use Rand() for Gambas Release 3.6
  82.   ' or CInt(Rnd()) for previous releases
  83.   count = Rand(24)
  84.   'count = CInt(Rnd(24.0))
  85.  
  86.   If bUseStoredGames Then
  87.     'Randomly select puzzle
  88.     count = Rand(NumberOfStoredGames)
  89.     pzl = GetPuzzle(count)
  90.     If pzl Then
  91.       pvals = Split(pzl, ",")
  92.       'Set the puzzle positions
  93.       For Each pval In pvals
  94.         i = Val(pval) - 1
  95.         ChangeState(i)
  96.       Next
  97.     Else
  98.       Message.Error("Puzzle Index Out of Range!")
  99.     Endif
  100.   Else
  101.     ' Randomly set starting positions
  102.     For i = 1 To count
  103.       j = Rand(1, MaxStartingPositions)
  104.       ChangeState(j)
  105.     Next
  106.   Endif
  107.   Me.Refresh()
  108. End
  109.  
  110.  
  111. ' Update the board after a move
  112. Private Sub UpdateBoard()
  113.   Dim x1, x2, y1, y2, i As Integer
  114.  
  115.   x1 = ActiveIndex - 1
  116.   x2 = ActiveIndex + 1
  117.   y1 = ActiveIndex - 5
  118.   y2 = ActiveIndex + 5
  119.  
  120.   If x1 >= 0 And (x1 Mod 5) < 4 Then
  121.     ChangeState(x1)
  122.     aLbls[x1].Refresh()
  123.   Endif
  124.  
  125.   If x2 < 25 And (x2 Mod 5) > 0 Then
  126.     ChangeState(x2)
  127.     aLbls[x2].Refresh()
  128.   Endif
  129.  
  130.   If y1 >= 0 Then
  131.     ChangeState(y1)
  132.     aLbls[y1].Refresh()
  133.    Else
  134.      y1 = (y1 + 25) Mod 25
  135.      ChangeState(y1)
  136.      aLbls[y1].Refresh()
  137.   Endif
  138.  
  139.   If y2 < 25 Then
  140.     ChangeState(y2)
  141.     aLbls[y2].Refresh()
  142.   Endif
  143. End
  144.  
  145.  
  146. ' Test for winning condition
  147. Public Sub TestWin() As Boolean
  148.   Dim i As Integer
  149.   Dim flag As Boolean
  150.  
  151.   flag = True
  152.  
  153.   For i = 0 To aLbls.Length - 1
  154.     If GetState(i) = True Then
  155.       flag = False
  156.     Endif
  157.   Next
  158.  
  159.   Return Flag
  160. End
  161.  
  162.  
  163. ' Process mouse click
  164. Public Sub Labelgroup_MouseDown() 
  165.   Dim idx As Integer
  166.   Dim state As Boolean
  167.   Dim msg As String
  168.  
  169.   ActiveGame = True
  170.  
  171.   GetActiveLabel(Last)
  172.   ChangeState(ActiveIndex)
  173.   UpdateBoard()
  174.   UpdateScore()
  175.   If TestWin() Then
  176.     msg = "You Won!!!\n" & "in " & tbTries.Text & " tries."
  177.     Message.Info(msg)
  178.   Endif
  179. End
  180.  
  181.  
  182. ' Update Score text box
  183. Private Sub UpdateScore()
  184.   tbTries.Text = Str(Val(tbTries.Text) + 1)
  185. End
  186.  
  187.  
  188. 'Get the label clicked on and save it 
  189. 'in the active index and active state 
  190. Private Sub GetActiveLabel(lbl As Object)
  191.   Dim data As String
  192.   Dim datums, dats1, dats2 As String[]
  193.   Dim idx As Integer
  194.   Dim state As Boolean
  195.  
  196.   data = lbl.tag ' We stored state and index information in the tag...
  197.   datums = Split(data, ";")
  198.   dats1 = Split(datums[0], ":")
  199.   dats2 = Split(datums[1], ":")
  200.   ActiveIndex = dats1[1]
  201.   ActiveState = dats2[1]
  202. End
  203.  
  204.  
  205. 'Get the state of a label cell from the
  206. 'labels tag property
  207. Private Sub GetState(idx As Integer) As Boolean
  208.   Dim lbl As Label
  209.   Dim data As String
  210.   Dim datums, dats1 As String[]
  211.   Dim state As Boolean
  212.  
  213.   lbl = aLbls[idx] ' Get the label 
  214.  
  215.   data = lbl.tag ' We stored state and index information in the tag...
  216.   datums = Split(data, ";")
  217.   dats1 = Split(datums[1], ":")
  218.  
  219.   If CBool(dats1[1]) Then 
  220.     state = True
  221.   Else
  222.     state = False
  223.   Endif
  224.   Return state
  225. End
  226.  
  227.  
  228. 'Set the state of the lable identified by the passed
  229. 'index value to state
  230. Private Sub SetState(idx As Integer, state As Boolean)
  231.   Dim lbl As Label
  232.  
  233.   lbl = aLbls[idx]
  234.   If state Then
  235.     lbl.tag = "idx:" & Str(idx) & ";state:" & True
  236.      lbl.Background = iColorTrue
  237.   Else
  238.     lbl.tag = "idx:" & Str(idx) & ";state:" & False
  239.      lbl.Background = iColorFalse
  240.   Endif
  241. End
  242.  
  243.  
  244. 'Toggle state of the label identified by idx
  245. Private Sub ChangeState(idx As Integer)
  246.   Dim state As Boolean
  247.  
  248.   state = GetState(idx)
  249.   state = Not state
  250.   SetState(idx, state)
  251. End
  252.  
  253.  
  254. 'Process New Game button
  255. Public Sub Button1_Click()
  256.   ResetBoard()
  257.   ActiveGame = False
  258.   NewGame()
  259. End
  260.  
  261.  
  262. 'Return a string of known good starting positions
  263. 'These are only 8 of the more than a 1000 starting
  264. 'positions with known solutions. Add more if you like
  265. Private Sub GetPuzzle(n As Integer) As String
  266.   Select Case n
  267.     Case 1
  268.       Return "1,3,4,5,6,8,10,12,13,14,16,18,19,21,23"
  269.     Case 2
  270.       Return "2,5,7,8,9,11,13,14,17,19,22,24"
  271.     Case 3
  272.       Return "1,2,3,5,6,7,8,12,13,15,16,17,19,20,21,22,23,24"
  273.     Case 4
  274.       Return "1,2,3,5,6,12,15,16,18,20,21,23"
  275.     Case 5
  276.       Return "4,6,9,15,16,17,18,23"
  277.     Case 6
  278.       Return "1,2,3,4,5,8,9,10,12,14,17,18,19,22,24"
  279.     Case 7
  280.       Return "3,6,9,10,11,13,18,19,24"
  281.     Case 8
  282.       Return "1,4,7,9,10,12,15,18,19,21,22,24"
  283.     Case Else
  284.       Return ""
  285.   End Select
  286.  
  287. End
  288.  
  289.  
  290. 'Process check box for Use Stored Gamea
  291. Public Sub cbUseStoredGames_Click()
  292.   bUseStoredGames = cbUseStoredGames.Value 
  293. End

 

The code is very straight forward and doesn’t need a lot of explanation. On form open we simply setup our initial conditions. The iColorFalse and iColorTrue values represent the colors to be used for the off and on states respectively.  Next, a call is made the ResetBoard() and to NewGame().

The ResetBoard() method iterates over all the board positions and places a label in each position with a height and width of 50 by 50 pixels. Adjustments are made for the gaps between each label in the grid. The label’s tag field is used to store the label index and state values.

The NewGame() method checks the bUseStoredGames property and if set, randomly selects one of the puzzles with a call to the GetPuzzle() method. This method returns a string of integer values representing the positions on the board to be turned on at the start of play. Otherwise, the method selects positions to be turned on as random. Finally, a call to refresh is made to ensure the board is visually updated. The program then waits for a mouse click to occur on one of the labels in the grid.

When the user clicks a label in the grid, a mouse_down event is fired and picked up by the Labelgroup_MouseDown() method. This method first, sets the ActiveGame flag. Then it finds the last active label using the GetActiveLabel() method. Next, a call is made to the ChangeState() method which updates the state of the active label and it’s four cardinal neighbors. A call to the UpdateScore() method simply updates the number of tries the user has taken to solve the puzzle. Finally, a test is made to see if the user action resulted in a complete lights out condition. If so, the a “You Won!” info message is shown.

This program can be better organized. For example, there are a couple places where the state information is parsed from the active label.  A keen user may want to consolidate this into a function. There are other ways to improve this program. But my aim here was two fold; First, show how easily a simple game can be produced using Gambas and second, keep things as simple and straight-forward as possible. Do to limited time I have left the improvements up to the reader.

Resources:
Print Friendly, PDF & Email

editor

5 Comments

I’m not sure if this is an error or not but if you click on the top row of “lights” then it rolls over to the bottom row however if you click on the bottom row it does NOT roll over to the top row.

The “fix” is quite easy (assuming that roll over should happen top and bottom)
in Private Sub UpdateBoard()
the last if which reads
If y2 < 25 Then
ChangeState(y2)
aLbls[y2].Refresh()
EndIf

needs amending
1 solution is
If y2 25 Then
y2 = y2 – 25
EndIF
ChangeState(y2)
aLbls[y2].Refresh()

This is all a great idea but there is a lot wrong with this code. There is no advice on how to copy the code as it is not accessible from the web page. There are numerous code errors including “amp” dotted throughout the code. The text contains an error to the naming of the checkbox. There is code used (Rand) that is not available in the latest stable release of Gambas.

Sorry, great idea but needs to be checked before publishing.

Hi Charlie,

The original post did not contain the “&” which is the html encoding of the “&” character. It also had line numbers. The plugin that does the syntax highlighting on the site was recently updated and I suspect an issue there as it also provides the line numbering. There also seems to be a missing link to the article resources which contains the complete Gambas project for the article. I’ll get this fixed soon.

The use of the Rand() function was added in Rev: r6282 and may not be available in the current stable release and distro releases are often far behind either the latest release from other repositories or the trunk version. A good work around for the Rand function is to use the rnd function. Rand(X) returns a integer random number between 0 and X included.
Rand(X,Y) returns a integer random number between X and Y included. The Rnd() function works similarly but returns a float. Using CInt(Rnd()) should provide a work around.

I don’t claim to be a writer, just a Gambas enthusiast who wants to support the community. So please do not take me as an expert. I do appreciate your feedback. It will help me to provide better articles in the future. Also, I am always looking for anyone willing to write articles.

I think the idea here was to give readers an idea of what could be accomplished. I appreciated the opportunity to view the code and see how the idea could be accomplished. A typographical error here or there is to be expected and finding them is part of the debugging process.

I just installed Gambas and am excited to jump in and start coding. It has been a few years since I’ve done some BASIC coding in Visual Basic and VBA as I’ve been coding primarily in C#, so just reading through this code helps to get my mind thinking BASIC again.

My first project will be to recreate this game, as typing the code in by hand, although not much fun, will be much more helpful for me then copying and pasting the code – and I will avoid all those pesky &’s and such.

Thanks R. Morgan. I appreciated your sharing and the work your doing supporting Gambas!

Leave a Reply