Back to November Calendar            ←Previous Entry                             Next Entry→

November 26, 2005

 

The Chemical Words Game

 

Behold the Periodic Table of the Elements.  Iridium at the foot of the cobalt column. Europium the bologna of the a samarium and gadolinium sandwich.  After writing the software to solve the NPR puzzle, I thought I’d try some visualization of the process and maybe even arrive at something like a pleasant pastime, a game.  Below you see a screenshot of what I’ve got so far.  At first, you are presented with an empty 3x3 grid positioned above the periodic table.  You can click on any element, causing a red outline to appear around that symbol cell, and then click in the corresponding square in the 3x3 grid where you want to place that chemical symbol.  You can see the result of having done this 9 times to fill the 3x3 grid in the screenshot.  Tierce is a variation on “terce,” the third of the canonical hours: the 12 hours between sunrise and sunset the ancients used to divide up the day. Calla is a genus of plants, of the order Araceae, if you were wondering.  Inlace is the decorative stuff you use to do inlay in woodwork…nobody in my neck of the woods need to be told what cacti are.

 

So let me run down some of the key features of this program.  I won’t include all the text of it my commentary, but I’ve bundled up the file for you here (hyperlink).

 

The start of the main file showing the includes, etc, is a good beginning:

#include "chemSymb.h"

#include <iostream>

#include <cstdlib>

#include <string>

#include <GL/glut.h>

#include <vector>

using namespace std;

 

const GLfloat cellW = 40.0;

const GLfloat top = 320.0;

vector<chemSymb> vCS;

 

Note in particular vCS, the vector of chemSymb.  The chemSymb class is declared and defined in chemSymb.h:   

 

// chemSymb.h

// declaration of class chemSymb

 

#include <cstring>

#include <gl/glut.h>

//using namespace std;

 

enum elemType {AlkaliMetal,

               AlkaliEarthMetal,

               TransitionMetal,

               Lanthanide,

               Actinide,

               PoorMetal,

               Metalloid,

               NonMetal,

               Halogen,

               NobelGas};

 

class chemSymb {

private:

      char* symbol;

      char* name;

      int atNo;

      char* atMass;

      elemType eType;

      GLfloat pos[2];

public: //default constructor

      chemSymb():symbol(""),
                 name(""),
                 atNo(0),
                 eType(AlkaliMetal) {};

      //copy constructor

      chemSymb(const chemSymb & );

      //constructor

      chemSymb(char*,char*,int,char*,elemType,GLfloat []);

      void drawChemSymb(GLfloat);

      char* getSymbol();

      int getAtNo();

      void setPos(GLfloat []);

     

      chemSymb& operator=(const chemSymb &);

      //int getElemType();

      //void printInfo();

};

 

A chemSymb has private string variables symbol and name, and integer variable atNo, for the atomic mass: something that is not essential to the game now, but I thought I’d throw it in—who knows—maybe later the game may take on a whole new sudoku-like character?  The string variable atMass, is really a number, so it’ll be a number string, but I think maybe it was…well, it was a while ago now, but I think it has to do with printing scalable fonts in OpenGL.  Stay tuned, we’ll revisit that.  <Later: in fact, atMass is never used and atNo is converted to an array of characters.> The enumerated type elemType is defined just before the chemSymb class—it isn’t used either, yet.  However, pos[2]  is essential to identifying the chemSymb with a region of the screen…a somewhat troubling matter, as we shall see. 

 

So, among other things, most importantly, a chemSymb has a symbol, an atomic number, atNo, and a position pos[2];  there may be some correlation between the element type and the color of cell.

 

Below is the first part of the gargantuan function createPeriodicTable() which assigns values to all these attributes for each element in the part of the table we’ll be seeing. This is enough to get the idea of the rest: it’s all about loading specific information that can’t easily be done in a loop.  The array putItHere[2] contains the current position, starting at the top left, with hydrogen, H.  The chemSymb constructor is used to build the hydrogen object with

 

chemSymb H("H","hydrogen",1,"1.008",NonMetal,putItHere);

 

This is a direct initialization invoking the unique constructor which matches its arguments.  It may be a good exercise, or even useful (!) to overload (add more) constructors with different parameter lists: it is, in fact, the unique set of parameters that determines which one of the constructors is implemented by the compiler. 

 

Below are a number of constructors I whipped up for you.  I have to confess the whipping was long enough ago that the wounds have healed and so I can’t readily provide incisive commentary.  From afar, it looks like the first is the one that’s being implemented in my code while the next two are variations on copy constructor and overloading the assignment operator methods.

 

chemSymb::chemSymb(char* symb, char* name,
                   int atNo, char* atMass,
                   elemType series,GLfloat place[2])

      : symbol(symb),

        name(name),

        atNo(atNo),

        atMass(atMass),

        eType(series)

{

      pos[0] = place[0];

      pos[1] = place[1];

}

 

chemSymb::chemSymb(const chemSymb &chemSymbtocopy) {

      symbol = chemSymbtocopy.symbol;

      name = chemSymbtocopy.name;

      atNo = chemSymbtocopy.atNo;

      atMass = chemSymbtocopy.atMass;

      eType = chemSymbtocopy.eType;

      pos[0] = chemSymbtocopy.pos[0];

      pos[1] = chemSymbtocopy.pos[1];

}

 

chemSymb& chemSymb::operator=(const chemSymb &rhs) {

  //GLfloat where[2] = {0,0};

  //chemSymb temp("","",0,"",AlkaliMetal,where);//); //(

  symbol = rhs.symbol;

  name = rhs.name;

  atNo = rhs.atNo;

  atMass = rhs.atMass;

  eType = rhs.eType;

  pos[0] = rhs.pos[0];

  pos[1] = rhs.pos[1];

  return *this;

}

 

The global variable top (320, initially) is used to align elements from the top of the table.  As you can see, the other global variable, cellW (40, initially) is then used to repeatedly shift downwards from top.  The table is built column by column.

 

void createPeriodicTable() {

      GLfloat putItHere[2] = {0.0,top};


      chemSymb H("H","hydrogen",1,"1.008",NonMetal,putItHere);          H.drawChemSymb(cellW);  vCS.push_back(H);

      putItHere[1] -= cellW;


      chemSymb Li("Li","lithium",3,"6.941",AlkaliMetal,putItHere);      Li.drawChemSymb(cellW);  vCS.push_back(Li);

      putItHere[1] -= cellW;


      chemSymb Na("Na","sodium",11,"22.9898",AlkaliMetal,putItHere);

      Na.drawChemSymb(cellW);  vCS.push_back(Na);

      putItHere[1] -= cellW;


      chemSymb K("K","potassium",19,"39.098",AlkaliMetal,putItHere);

      K.drawChemSymb(cellW); vCS.push_back(K);

      putItHere[1] -= cellW;


      chemSymb Rb("Rb","Rubidium",37,"85.47",AlkaliMetal,putItHere);

      Rb.drawChemSymb(cellW);  vCS.push_back(Rb);

      putItHere[1] -= cellW;


      chemSymb Cs("Cs","cessium",55,"132.905",AlkaliMetal,putItHere);

      Cs.drawChemSymb(cellW);  vCS.push_back(Cs);

      putItHere[1] -= cellW;

 

Below is most of the chemSymb.cpp file including the outWord() function and drawChemSymb(). Of particular interest here is outWord() which takes the coordinates x and y of the upper left corner of the first letter in the string of characters, *text to be printed, as scaling factor s and, of course, the text.  We push everything currently on the matrix stack down one notch (leaving a copy of the top matrix on the top) with a glPushMatrix(), multiply this matrix by:

                                                                            

with glTranslatef(x, y, 0)to translate the painting position (?) to where we want it.  The command glScalef(s, s, s) further multiplies the matrix by

                                                                            

to scale the text and then

for (p = text; *p; p++)

      glutStrokeCharacter(GLUT_STROKE_ROMAN, *p);

loops through the characters in the text to be written renders each as a stroke character.  In OpenGL there are two types of text: stroke and raster.   Raster text is simple bitmap blocks.  In stroke text, vertices are used to describe line segments and curves that outline the character.  The outline can then be filled, scaled, stretched, etc—just like if it were a geometric object.  That’s a little bit more time-consuming for the computer (what’s time to a pig?) but convenient for my purpose here because I want to reproduce the symbol cell in the playfield somewhat larger than in the table.  Once this is done we can pop the matrix off the top of the stack (destroying it) with glPopMatrix().  It’s served its purpose.

 

// chemSymb.cpp

// member functions for class chemSymb

 

#include "chemSymb.h"

#include <iostream>

#include <gl/glut.h>

using std::cout;

//static GLfloat cellWidth = 40.0;

//static GLfloat cellHeight = 40.0;

 

void outWord(GLfloat x, GLfloat y, GLfloat s, char *text) //GLfloat s,

{

      char *p;

      glPushMatrix();

      glTranslatef(x, y, 0);

      glScalef(s, s, s);

      for (p = text; *p; p++)

            glutStrokeCharacter(GLUT_STROKE_ROMAN, *p);

      glPopMatrix();

}

 

void chemSymb::drawChemSymb(GLfloat cellSize) {

      char buffer [33];  // for itoa

      itoa(atNo,buffer,10);

      switch(eType) {

            case(AlkaliMetal):

                  glColor3f(1.0,0.5,0.5);

                  break;

            case(AlkaliEarthMetal):

                  glColor3f(1.0,1.0,0.4);

                  break;

            case(TransitionMetal):

                  glColor3f(1.0,0.8,0.8);

                  break;

            case(Lanthanide):

                  glColor3f(0.8,0.8,1.0);

                  break;

            case(Actinide):

                  glColor3f(0.97,0.97,0.86);

                  break;

            case(PoorMetal):

                  glColor3f(0.9,0.9,1.0);

                  break;

            case(Metalloid):

                  glColor3f(0.6,0.9,1.0);

                  break;

            case(NonMetal):

                  glColor3f(0.7,1.0,0.7);

                  break;

            case(Halogen):

                  glColor3f(0.8,1.0,1.0);

                  break;

            case(NobelGas):

                  glColor3f(1.0,0.8,1.0);

                  break;

            default:

                  glColor3f(1.0,1.0,1.0);

                  break;

      } // end switch

      //draw background color

      cout << "\npos[0] = " << pos[0];

      cout << "\npos[1] = " << pos[1];

      glBegin(GL_POLYGON);

         glVertex2f(pos[0],pos[1]);

         glVertex2f(pos[0]+cellSize,pos[1]);

         glVertex2f(pos[0]+cellSize,pos[1]+cellSize);

         glVertex2f(pos[0],pos[1]+cellSize);

      glEnd();

      //draw boundary

      glColor3f(0.0,0.0,0.0);

      glBegin(GL_LINE_LOOP);

         glVertex2f(pos[0],pos[1]);

         glVertex2f(pos[0]+cellSize,pos[1]);

         glVertex2f(pos[0]+cellSize,pos[1]+cellSize);

         glVertex2f(pos[0],pos[1]+cellSize);

      glEnd();

      glColor3f(0.0,0.0,1.0);

      //glScalef(0.14, 0.14, 0.14);

      if(strlen(symbol)==2)

         outWord(pos[0]+0.24*cellSize,
                 pos[1]+0.33*cellSize,
                 0.14*cellSize/40.,
                 symbol);

      else outWord(pos[0]+0.33*cellSize,
                   pos[1]+0.33*cellSize,
                   0.14*cellSize/40.,
                   symbol);

      glColor3f(0.0,0.0,0.0);

      cout << "\nstrlen(buffer) = "<< strlen(buffer);

      outWord(pos[0]+(0.48-0.08*(float)strlen(buffer))*cellSize,
              pos[1]+0.75*cellSize,
              0.08*cellSize/40.,
              buffer);

}

 

The first thing drawChemSymb() does is convert atNo to the array of characters buffer with itoa.  This is so it can be rendered as a sequence of stroke characters.  It then uses a switch structure to check what element type the chemical is and sets a color unique to that type.  This is followed by a dump of coordinates to the console, for debugging purposes (I’m bugged by a gradual drift in boundaries and still confounds me.) 

 

The solid rectangle is then filled in with the color peculiar to that element type at the current position (measured in pixels) with

glBegin(GL_POLYGON);

   glVertex2f(pos[0],pos[1]);

   glVertex2f(pos[0]+cellSize,pos[1]);

   glVertex2f(pos[0]+cellSize,pos[1]+cellSize);

   glVertex2f(pos[0],pos[1]+cellSize);

glEnd();

We then draw the boundary of this cell in black (see more complete code further above) and, as shown below, call outWord to draw the symbol.  

if(strlen(symbol)==2)

     outWord(pos[0]+0.24*cellSize,
             pos[1]+0.33*cellSize,
             0.14*cellSize/40.,symbol);

else outWord(pos[0]+0.33*cellSize,
             pos[1]+0.33*cellSize,
             0.14*cellSize/40.,symbol);

glColor3f(0.0,0.0,0.0);

If it’s a two-letter symbol, the pos[](aka putItHere[] in main)  coordinates are increased by 24% of the cellSize horizontally and 33% vertically to shift enough right and down to center the symbol characters in the cell.  If it’s only 1 letter, the horizontal shift is increased to 33% too.  Note that cellSize is the only parameter passed to drawChemSymb—the same value as the global variable cellW so, is that necessary?  Hmmm. 

 

There’s a dump to console of strlen(buffer) that I’ve left in for it’s nostalgic charm.

 

The final call to outWord() draws the atNo we recast as an array of characters in buffer[] positioned at 48% less 8% of cellSize per character horizontally and way up at 75% of cellSize (40 pixels) vertically.  The cast to float is needed since strlen is an int.

 

outWord(pos[0]+(0.48-0.08*(float)strlen(buffer))*cellSize,
        pos[1]+0.75*cellSize,
        0.08*cellSize/40.,buffer);

 

When we’re done displaying the Periodic Table we move on to the 3x3 grid which comprises the playfield above the table.  Here’s the code to initialize it:

 

void drawPlayArea() {

      glLineWidth(3.0);

      glColor3f(0.8,0.8,0.8);

      glBegin(GL_LINE_LOOP);

        glVertex2f(160.,280.);

        glVertex2f(400.,280.);

        glVertex2f(400.,520.);

        glVertex2f(160.,520.);

      glEnd();

      glBegin(GL_LINES);

        glVertex2f(240.,280.);

        glVertex2f(240.,520.);

        glVertex2f(320.,280.);

        glVertex2f(320.,520.);

        glVertex2f(160.,360.);

        glVertex2f(400.,360.);

        glVertex2f(160.,440.);

        glVertex2f(400.,440.);

      glEnd();

      glLineWidth(1.0);

}

 

First it draws the outline around the whole thing as a LOOP, then it fills in with lines.  This is a nice display of the GL_LINES vs GL_LINE_LOOP parameter specifications.   Passing location and dimensions of a cell is all we need to highlight its boundary.  Note color is red and the width is 3 pixels:

 

void highlightBoundary(GLfloat x, GLfloat y, GLfloat w, GLfloat h) {

      glColor3f(1.,0.,0.);

      glLineWidth(3.);

      glBegin(GL_LINE_LOOP);

        glVertex2f(x,y);

        glVertex2f(x+w,y);

        glVertex2f(x+w,y+h);

        glVertex2f(x,y+h);

      glEnd();

      glLineWidth(1.0);

}

 

Here’s the display function: pretty straightforward: draw the Periodic Table and draw the play area.

 

//<<<<<<<<<<<<<<<<<<<<<<<< display >>>>>>>>>>>>>>>>>

void display(void)

{

      glClear(GL_COLOR_BUFFER_BIT); // clear the screen

      glPushMatrix();

      glColor3f(1.0,1.0,1.0); //white

      createPeriodicTable();

      drawPlayArea();

      glPopMatrix();

      glFlush(); // send all output to display

}

 

Now it seems there must be a better way to handle the mouse, but here’s an outline of what I’ve got…which kind of works, so who’s to argue?   

 

First, I create a static Boolean, first, to determine whether the mouse has been clicked to highlight a Periodic Table cell or not (whether it’s the first click or not.)  A chemSymb named chosenSymb is then instantiated to represent…hmm, the chosen (clicked on) symbol!   When the left mouse button is clicked, we check to see whether it’s coordinates are in any of the 91 cells of the Periodic Table or any of the 9 cells of the play area.  If it’s in one of the Periodic Table cells and first is true, then chosenSymb is assigned the attributes of the symbol via the overloaded assignment operator, the cell is surrounded by a red boundary and first is set to false. 

 

If the mouse is clicked over the play area with first set to false then the corresponding cell of the play area is drawn with the chosenSymb, but at a size of 80 pixels instead of 40.  Also, first is set back to true

 

//<<<<<<<<<<<<<<<<<<<<<<<< mouse >>>>>>>>>>>>>>>>>

void mouse(int button, int state, int x, int y)

{

      static bool first = true;

      //GLfloat dumby[2] = {0.,0.};

      static chemSymb chosenSymb;

      static GLfloat putItHere[2];

      switch(button) {

            case GLUT_LEFT_BUTTON:

                  if(state == GLUT_DOWN && 0.0<x && x<cellW

                              && 160.<y && y<200. && first) {

                        chosenSymb = vCS[0];                   
                        highlightBoundary(0.,320.,cellW,cellW);

                        first = false;
                        glFlush(); 

                  }

           //////////////////////////////////////////////////////////////////

     // The next 90 elements are similarly tested for

     //////////////////////////////////////////////////////////////////

                  if(state == GLUT_DOWN && 17*cellW<x && x<18*cellW
                              && 440.<y && y<480. && first) {

                        chosenSymb = vCS[90];      
                        highlightBoundary(720.,40.,cellW,cellW);

                        first = false;
                        glFlush();

                  }

 

 

                  //gamePlay zone////////////////////////////////////

                  if(state == GLUT_DOWN && 160.<x && x<240.
                              && 0.<y && y<80. && !first) {

                        first = true;

                        putItHere[0] = 160.; putItHere[1] = 440.;

                        chosenSymb.setPos(putItHere);

                        chosenSymb.drawChemSymb(80.);

                        glFlush();

                  }

                  if(state == GLUT_DOWN && 160.<x && x<240.
                              && 80.<y && y<160. && !first) {

                        first = true;

                        putItHere[0] = 160.; putItHere[1] = 360.;

                        chosenSymb.setPos(putItHere);

                        chosenSymb.drawChemSymb(80.);

                        glFlush();

           //////////////////////////////////////////////////////////////////

     // All 9 cells of the play area are similarly treated

     //////////////////////////////////////////////////////////////////

                                                                        }

                  if(state == GLUT_DOWN && 320.<x && x<400.
                              && 160.<y && y<240. && !first) {

                        first = true;

                        putItHere[0] = 320.; putItHere[1] = 280.;

                        chosenSymb.setPos(putItHere);

                        chosenSymb.drawChemSymb(80.);

                        glFlush();

                  }

                  break;

            case GLUT_RIGHT_BUTTON:

                  if(state == GLUT_DOWN)

                        glutIdleFunc(NULL);

                  break;

            default:

                  break;

      }

}

 

Finally, here’s the main routine. 

 

int main(int argc, char** argv)

{

      glutInit(&argc, argv); // initialize the toolkit

      glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);

      glutInitWindowSize(700,540); // set window size  //480?

 

      //static int clickCnt = 0;

      // set window position on screen

      glutInitWindowPosition(100, 150);

      // open the screen window and set the name

      glutCreateWindow(argv[0]);

      init();

      //register your functions

      glutDisplayFunc(display);

     

      glutMouseFunc(mouse);

      glutReshapeFunc(reshape);

      //init();

      glutMainLoop(); // go into a perpetual loop

      return 0;

}

 

To do:

  1. Fix the coordinate drift.
  2. Add sound.
  3. Have temporary outlines appear with a mouse over.
  4. Include a score card.
  5. Add AI using the code from yesterday to add a computer opponent.

 

 

Calcium was cornered by carbon and boron in the cabin with a field of cacti on it’s west side.  By tierce the sun rose to shine on a patch of calla I hoped to