Sudoku, a complete MFC application.  Part 8

AndyAinscowFreelance programmer / Consultant
CERTIFIED EXPERT
Published:
Updated:

Introduction:

Database storage, where is the exe actually on the disc? Playing a game selected randomly (how to generate random numbers).  Error trapping with try..catch to help the code run even if something goes wrong.

Continuing from the seventh article about sudoku.  

Random number generators.  There is a built in random number function in windows.  For playing our games of sudoku having a good random number generating algorithm isn’t necessary.  For other work it may well be critical, put another way having a poor algorithm could result in you generating rubbish from your application – because the data it is based on was not truly random.  Here we are going to provide a basic class for random number generation using a technique (which is supposed to be good) from literature, specifically about mathematical techniques in programming.  Don’t take this as any implication that the inbuilt routines are poor quality or unreliable.  The source of the algorithm here is referenced in the code.

I have created a new database with Microsoft Access, in Access 2000 format, and called it Sudoku.mdb. The database must be located in the same directory as the exe file.  The database contains one table (Games), this contains two fields ID (auto number, primary key) and GameDetail (Text, 82 char, required=true, Allow zero length=false, indexed=yes – no duplicates).

Open the project in visual studio.  Add a new C++ class (not MFC) to the project via the solution explorer and call it CRandomNumber.  The following are the .h and .cpp file contents respectively.

#pragma once
                      
                      class CRandomNumber
                      {
                      public:
                          CRandomNumber(long lSeed = 0);
                          ~CRandomNumber(void) {};
                      
                          long GetRandom(long lLower = 0, long lUpper = 0x7FFFFFFE);
                      private:
                          long m_lSeed;
                          float rand(long *idum);
                      
                          //Following are static members of the func in the original
                          //here made to class members to preserve values between calls
                          //but allows multiple instances of the class to be independant of one another
                          //(should that be required)
                          long iy;
                          long iv[32];    //array size MUST be the same as value of NTAB
                      };

Open in new window


and

#include "StdAfx.h"
                      #include "RandomNumber.h"
                      
                      
                      #define IA (16807)
                      #define IM (2147483647)
                      #define AM ((float)1.0/(float)IM)
                      #define IQ (127773)
                      #define IR (2836)
                      #define NTAB (32)
                      #define NDIV (1+(IM-1)/NTAB)
                      #define EPS ((float)1.2e-7)
                      #define RNMX ((float)(1.0-EPS))
                      
                      
                      CRandomNumber::CRandomNumber(long lSeed)
                      : m_lSeed(lSeed > 0 ? -lSeed : lSeed)
                      , iy(0)
                      {
                          //Check that the 'iv' array is the same size as NTAB - it should be so
                          ASSERT(sizeof(iv) == NTAB * sizeof(long));
                      
                          if(m_lSeed == 0)
                          {
                              //generate a seed, use the day of year and time of day as a base value
                              COleDateTime dte = COleDateTime::GetCurrentTime();
                              m_lSeed = -(dte.GetDayOfYear() * 24 * 60 * 60 + dte.GetHour() * 60 * 60 + dte.GetMinute() * 60 + dte.GetSecond());
                          }
                          rand(&m_lSeed);
                      }
                      
                      //Assume no overflows of the entered values, and that lLower is in fact less than lUpper
                      long CRandomNumber::GetRandom(long lLower, long lUpper)
                      {
                          if(lLower == lUpper)
                              return lLower;
                      
                          float fRes = rand(&m_lSeed);
                      
                          //lUpper would never be returned if this value was not incremented
                          lUpper++;
                      
                          return (long)((float)(lUpper - lLower) * fRes) + lLower;
                      }
                      
                      
                      float CRandomNumber::rand(long *idum)
                      {
                          int j;
                          long k;
                          float temp;
                      
                          if(idum <= 0 || !iy)    //initialise
                          {
                              if(-(*idum) < 1)    //idum should not be zero, if it is then set to 1 as an initial value
                                  *idum = 1;
                              else
                                  *idum = -(*idum);    //make positive
                      
                              for(j = NTAB+7; j >= 0; j--)
                              {
                                  k = (*idum)/IQ;
                                  *idum = IA * (*idum - k*IQ) - IR*k;
                                  if(*idum < 0)
                                      *idum += IM;
                                  if(j < NTAB)
                                      iv[j] = *idum;
                              }
                              iy = iv[0];
                          }
                      
                          //Starts here when not initialising
                          k = (*idum) / IQ;
                      
                          //computes idum=(IA*idum) % IM without overflows by Schrage's method
                          *idum = IA * (*idum - k*IQ) - IR*k;
                          if(*idum < 0)
                              *idum += IM;
                      
                          j = iy / NDIV;    //range 0..NTAB-1
                      
                          //Get previous value then replace the array with the new value
                          iy = iv[j];
                          iv[j] = *idum;
                      
                          temp = AM * iy;
                          
                          //In case temp is over the max value set it to the max value
                          if(temp > RNMX)
                              temp = RNMX;
                      
                          //the result is returned
                          return temp;
                      }
                      
                      
                      /*
                      The following is the source code from the literature:
                      Numerical recipies in C, second edition
                      W.H.Press, S.A.Teukolsky, W.T.Vetterling, B.P.Flannery
                      
                      chapter 7, ran1 method for random number generation
                      
                      
                      #define IA 16807
                      #define IM 2147483647
                      #define AM ((float)1.0/IM)
                      #define IQ 127773
                      #define IR 2836
                      #define NTAB 32
                      #define NDIV (1+(IM-1)/NTAB)
                      #define EPS (float)1.2e-7
                      #define RNMX (float)(1.0-EPS)
                      
                      float RAND(long *idum)
                      {
                          int j;
                          long k;
                          static long iy=0;
                          static long iv[NTAB];
                          float temp;
                      
                          if(idum <= 0 || !iy)    //initialise
                          {
                              if(-(*idum) < 1)    //idum should not be zero, if it is then set to 1 as an initial value
                                  *idum = 1;
                              else
                                  *idum = -(*idum);    //make positive
                      
                              for(j = NTAB+7; j >= 0; j--)
                              {
                                  k = (*idum)/IQ;
                                  *idum = IA * (*idum - k*IQ) - IR*k;
                                  if(*idum < 0)
                                      *idum += IM;
                                  if(j < NTAB)
                                      iv[j] = *idum;
                              }
                              iy = iv[0];
                          }
                      
                          //Starts here when not initialising
                          k = (*idum) / IQ;
                      
                          //computes idum=(IA*idum) % IM without overflows by Schrage's method
                          *idum = IA * (*idum - k*IQ) - IR*k;
                          if(*idum < 0)
                              *idum += IM;
                      
                          j = iy / NDIV;    //range 0..NTAB-1
                      
                          //Get previous value then replace the array with the new value
                          iy = iv[j];
                          iv[j] = *idum;
                      
                          temp = AM * iy;
                          
                          //In case temp is over the max value set it to the max value
                          if(temp > RNMX)
                              temp = RNMX;
                      
                          //the result is returned
                          return temp;
                      }
                      */

Open in new window


We now need to modify the document to support an Access database.  To the SudokuDoc.h file add the following at the top after the #pragma once

#include "afxdao.h"
                      #pragma warning(disable : 4995)

Open in new window


And in the class we need a new variable

private:
                          CDaoDatabase* m_pDB;

Open in new window


Go to the constructor and modify it to be the following:

CSudokuDoc::CSudokuDoc()
                      {
                          //Create the pointer to the database
                          m_pDB = new CDaoDatabase();
                      
                          //Get the path to the database and open it
                          //Note, a common fault is to rely on finding the current directory and ASSUMING that is where the 
                          //application.exe is located.  Unfortunately as part of a startup of an exe one can specify the directory 
                          //to start in so that assumption may work for you in testing, but a user can easily make it fail.
                          //You get the fallout (--> the app doesn't work is the feedback to your boss? )
                      
                          char szPath[MAX_PATH + 1];    //plus space for 0 string terminator
                          GetModuleFileName(NULL, szPath, MAX_PATH);    //NULL = use this module (the app)
                      
                          //Now remove the name of the exe from the returned string - find the final '\' char in the string
                          char* pch = strrchr(szPath, '\\');
                          *pch = 0;    //insert a 0 to act as a string terminator
                          strcat_s(szPath, MAX_PATH, "\\Sudoku.mdb");  //append the name of the mdb
                      
                          //open the database
                          m_pDB->Open(szPath);
                      }

Open in new window


Read the comments in the constructor code – that is how (and why) one finds where the exe is on disc when it is running.  What is the following line of code?

#pragma warning(disable : 4995)

Open in new window


MFC provides a suite of classes to work specifically with Access databases.  Microsoft has also decided they will at some time no longer be supported (however they are still there in VS 2010 so they should be available for a number of years).  Note however the DAO classes are NOT supported for 64 bit development in Visual Studio 2010 (and possibly earlier versions of Visual Studio).

At compile time the compiler will give a warning that the DAO classes will be dropped at some time – this line of code suppresses that warning, specifically it hides the warning with the ID 4995.  (Warnings are that – something might be wrong with the code, but then again it doesn’t mean it will fail.  Take note of compiler warnings, check them and take appropriate action.)

Now we need some cleanup code in the destructor

CSudokuDoc::~CSudokuDoc()
                      {
                          if(m_pDB != NULL)
                          {
                              m_pDB->Close();
                              delete m_pDB;
                              m_pDB = NULL;
                          }
                      }

Open in new window


We assigned memory with the 'new' keyword, now we release it otherwise we have a memory leak.  When the app is closed one would see warnings about memory leaks in the debug window.  (Rant time:  I personally hate the garbage collection mechanism in .net – during testing a memory leak, should one ever appear, tells me that there is a logic error (and that happens even with the best of us no matter how careful one is).  The gc mechanism stops that useful information, it even promotes sloppy coding by not having to think about cleaning up memory you assign.  We should now have a database connection – simple wasn’t it – so lets use it.  ps. The supplied database has a few games already stored in it.

Now open the resource editor.  From the menu resources select the ‘Play Random’ entry, right click with the mouse and adds a new event handler.  Select COMMAND as the type, enter OnFilePlayrandom as the name and from the class list select the CSudokuDoc, now click the Add and Edit button.  Modify this new function to be as follows:

    CRandomNumber rn;
                      
                          CDaoRecordset RS(m_pDB);
                          RS.Open(AFX_DAO_USE_DEFAULT_TYPE, "SELECT ID, GameDetail FROM Games");
                          RS.MoveLast();
                          long lCount = RS.GetRecordCount();
                          RS.MoveFirst();
                          
                          //move to random record (eg. 5 records then VALID moves are 0, 1, 2, 3, 4 -- so we need lCount-1)
                          RS.Move(rn.GetRandom(0, lCount - 1));
                      
                          COleVariant var = RS.GetFieldValue("GameDetail");
                          //It is a string in the table, now convert it to a string we can use
                          CString szGame(var.pbVal);
                      
                          //Now for the info to display in the status bar
                          var = RS.GetFieldValue("ID");
                          m_szInfo.Format("ID: %ld", var.lVal);
                          
                          RS.Close();
                      
                          strcpy_s(m_arGame, sizeof(m_arGame), szGame);
                          
                          UpdateAllViews(NULL, eLoadGame, NULL);

Open in new window


And don’t forget to include the header file for the random number class

#include "SudokuDoc.h"
                      #include "RandomNumber.h"

Open in new window


That should be pretty clear to understand.  Note I can base a recordset directly on an SQL command, it doesn’t have to be a table or a query stored in the database.  You should be able to compile and test it now.  Note that the ‘random’ button on the toolbar and the menu is also now enabled – just by adding a handler for the command.

You might see it is messy code to convert the field values returned (as variants) into variables you can use.  I’ll leave it up to you to write a class to hide that messy code for you.  (eg. int COpenVar::GetString(CDaoRecordset* pSet, LPTSTR pszFieldName, CString& szResult) or similar)

How can we get games into the database?  Well we can create a new game and I haven’t said anything about the ‘lock’ button yet.  What if we enter the start numbers onto a new game, then ‘lock’ it as a game and at the same time add it to the database – sound reasonable?  Let's do it.

Using the resource editor (as above) add a handler OnEditLock for the ‘lock’ menu item to the document.  This enables the ‘lock’ button but there will be times when we don’t want to lock a game for example when we load a game from the database.  Back to the resource editor add another event handler for the menu item – this time the UPDATE_COMMAND_UI type and call it OnUpdateEditLock.  We want the lock button to be available for only a new game – when the m_arGame variable just contains zeroes.

void CSudokuDoc::OnUpdateEditLock(CCmdUI *pCmdUI)
                      {
                          char* pCell = m_arGame;
                          while(*pCell != 0)
                          {
                              if(*pCell != '0')
                              {
                                  pCmdUI->Enable(false);
                                  return;
                              }
                              pCell++;
                          }
                          pCmdUI->Enable(true);
                      }

Open in new window


Not so elegant code but it does work.  We assign a pointer to the start of the block containing the game information and loop through it until we find the terminating zero or a non ‘0’ number.  (Remember that 0 is not the same as ‘0’.)  If you compile and run you should find the lock button/menu is enabled when there is a new game and disabled when one loads a game from disc or database.  If one is enabled and the other is disabled then check the properties of the menu item and toolbar button in the resource editor – typically you will find that they have a different ID assigned to them.  Note how simple it is to provide visual updates of toolbar and menu in an SDI (or MDI).  Dialog based application require rather more work, this automatic updating is not enabled by default.

We have the visual update, now to save to the database.  As part of the save to disc routines we already have a line of code to help update the document.  This is the first attempt at updating the database

void CSudokuDoc::OnEditLock()
                      {
                          //tell the view to store the 'game' into the document
                          UpdateAllViews(NULL, eSaveGame, NULL);
                      
                          //Now update the database
                          CDaoRecordset RS(m_pDB);
                          RS.Open(AFX_DAO_USE_DEFAULT_TYPE, "SELECT * FROM Games WHERE (ID=0)");
                          RS.AddNew();
                          RS.SetFieldValue("GameDetail", m_arGame);
                          RS.Update();
                          RS.Close();
                      }

Open in new window


Minor point – note we have used a ‘*’ in the SELECT statement.  This means all fields – this is a shortcut but it can be abused.  If your table has 50 fields this returns the contents of all 50 however if you only needed two fields then it has a lot of extra network traffic.  

You can try to update the database now, in fact do so and we will do something really silly, we will fix it later in the article.

Compile and run, now before entering any number press the ‘lock’ button, now do it again and you should see the following:

 Error message
The first ‘lock’ did work and saved a completely blank game into the database (hmmm!), the second then attempted to create a new record again with a blank game.  The field in the table with the game details is defined as indexed with no duplicates allowed – hence the messagebox.  The first problem – lets leave that up to the user, we will ask for confirmation (only one number could have been entered, or two…., lots of cases to check for in code and not easy to check for either).  For the second save we either ignore this error or check before saving.

Go to the resource view, open the string resources section and the string table.  At the bottom click on the blank line – you should see something like IDS_STRING129 in the first column appear.  We can select that and overtype - IDS_QRY_SAVE_TO_DB.  Then in the third column we provide our question eg. Is this new game completely entered, no undo possibilities.

In the OnEditLock function we modify it to provide the prompt:

void CSudokuDoc::OnEditLock()
                      {
                          CString s;
                          s.LoadString(IDS_QRY_SAVE_TO_DB);
                          if(AfxMessageBox(s, MB_YESNO | MB_ICONQUESTION) != IDYES)
                              return;

Open in new window


We could have put the text of the question here but you see one can load it directly from a resource ID.  (Actually this is too much code - the AfxMessageBox copes with the resource ID itself.  Here we use the CString variable to demonstrate how to get a resource string into code).  Using a resource string has some advantages.  What happens if one uses the same prompt in a number of locations – well it is lots of copies to modify (hard coded) or just one (resource) – your choice.  Your app should also support multiple languages?  No problems, just change the resource string (the resource editor supports multiple language versions for each individual resource).  This means no if statement blocks in the code to select which prompt to use.  (Now enter the text you want into the string resource, I have: Do you want to save this game into the database?)

The second problem – duplicate entries.  We put the update call into a try….catch block.  A try...catch block is useful for making code more robust, is something fails one can allow the application to continue and take appropriate action.

    try
                          {
                              RS.Update();
                          }
                          catch(CDaoException* pe)
                          {
                              if(pe->m_pErrorInfo->m_lErrorCode != 3022)
                              {
                                  AfxMessageBox(pe->m_pErrorInfo->m_strDescription, MB_ICONEXCLAMATION);
                              }
                              pe->Delete();
                          }

Open in new window


This is error (exception) handling and trapping.  Here we attempt to call a function (Update) that will throw an exception and the code block will catch that exception.  We can check information in the exception – eg. the error code, in this case the code 3022 means a duplicate value being entered.  Note we display the error message if it is any other than our expected one.  For this case I don’t supply our own warning about a duplicate because the user has attempted to add this game to the database, it failed because it is already there – is the user interested in that?

How do I know the error code?  Simple, I put a breakpoint and attempted to add a duplicate then looked at what the value in the exception was.

When we load a random game we display the ID of that game.  Now we have added a new game to the database we ought to be consistent and now display that information.  Simple I hear you say, all we need is the following after the update of the recordset

    COleVariant var = RS.GetFieldValue("ID");

Open in new window


Try it – unfortunately you get an error about no current record.  This is a feature of the DAO recordset that the AddNew…Update does not remain on the new record.  We need to use something called bookmarks

    COleVariant varBkmark = RS.GetLastModifiedBookmark();
                          RS.SetBookmark(varBkmark);
                          COleVariant var = RS.GetFieldValue("ID");
                          m_szInfo.Format("ID: %ld", var.lVal);
                      
                          POSITION pos = GetFirstViewPosition();
                          static_cast<CMainFrame*>(GetNextView(pos)->GetParentFrame())->DisplayStatusInfo(GetInfo());

Open in new window


We retrieve a bookmark (an identifier) of the last record modified, then we position the set onto that record – now we can get the ID of the new record.  As this code is running in the document and the display is on the status bar we have to go a little roundabout route to display it – by looking at the views attached to a document.  We also need to #include the header for the MainFrame for this to work.

So the complete OnEditLock is as follows:

void CSudokuDoc::OnEditLock()
                      {
                          CString s;
                          s.LoadString(IDS_QRY_SAVE_TO_DB);
                          if(AfxMessageBox(s, MB_YESNO | MB_ICONQUESTION) != IDYES)
                              return;
                      
                          //tell the view to store the 'game' into the document
                          UpdateAllViews(NULL, eSaveGame, NULL);
                      
                          //Now update the database
                          CDaoRecordset RS(m_pDB);
                          RS.Open(AFX_DAO_USE_DEFAULT_TYPE, "SELECT * FROM Games WHERE (ID=0)");
                          RS.AddNew();
                          RS.SetFieldValue("GameDetail", m_arGame);
                          try
                          {
                              RS.Update();
                              COleVariant varBkmark = RS.GetLastModifiedBookmark();
                              RS.SetBookmark(varBkmark);
                              COleVariant var = RS.GetFieldValue("ID");
                              m_szInfo.Format("ID: %ld", var.lVal);
                      
                              POSITION pos = GetFirstViewPosition();
                              static_cast<CMainFrame*>(GetNextView(pos)->GetParentFrame())->DisplayStatusInfo(GetInfo());
                          }
                          catch(CDaoException* pe)
                          {
                              if(pe->m_pErrorInfo->m_lErrorCode != 3022)
                              {
                                  AfxMessageBox(pe->m_pErrorInfo->m_strDescription, MB_ICONEXCLAMATION);
                              }
                              pe->Delete();
                          }
                          RS.Close();
                      }

Open in new window


Finally let’s implement the ‘Delete Game’ from the edit menu – we might have a game saved by mistake.  I don’t need to tell you in detail how to add the handler and update handler for the menu item do I?

void CSudokuDoc::OnUpdateEditDeletegame(CCmdUI *pCmdUI)
                      {
                          // Only required if we have loaded a game from the Database
                          pCmdUI->Enable(m_szInfo.Left(2).CompareNoCase("ID") == 0);
                      }
                      
                      void CSudokuDoc::OnEditDeletegame()
                      {
                          CString s(m_szInfo.Mid(4));
                          CString szSQL;
                          szSQL.Format("DELETE * FROM Games WHERE (ID=%s)", s);
                          m_pDB->Execute(szSQL);
                          
                          AfxGetMainWnd()->SendMessage(WM_COMMAND, MAKEWPARAM(ID_FILE_PLAYRANDOM, 0), NULL);
                      }

Open in new window


We know the format of the info string so we can just extract the ID of the current game simply.  Now that is used in a command to remove a record from the table by using this ID to identify it.  (This ought to be in a try…catch block but for simplicity we are not doing that).  Finally we display a random game by faking a menu event with the WM_COMMAND message to the main application window.

This function ought to display a confirmation warning – but you know how to do that by now, also what happens if you delete the last game in the database?  A small exercise for you (hint: see IsBOF and IsEOF in help).

Conclusion:

We have been working with a database (Microsoft Access).
We have seen how to use random numbers.
We have implemented simple error handling with try...catch blocks.
We have a robust technique to find where the exe file is located on disc.


Click here for the source code for this article


Previous article in the series is here:  Sudoku in MFC: Part 7
There we provided hints for the cells, worked with a templated collection and a nested class.  We also performed some simple debugging to find and correct an error.

Next article in the series is here:  Sudoku in MFC: Part 9
Here we will be creating a class to implement a simple stack for an undo mechanism.


Two points to bear in mind.
You may use the code but you are not allowed to distribute the resulting application either for free or for a reward (monetary or otherwise).  At least not without my express permission.
I will perform some things to demonstrate a point – it is not to be taken as that meaning it is a ‘best’ practice, in fact an alternative might be simpler and suitable.  Some points in the code would even be called poor design and a possible source of errors.
1
5,256 Views
AndyAinscowFreelance programmer / Consultant
CERTIFIED EXPERT

Comments (0)

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.