LCD life (pseudo graphics on a character LCD)

Here is a slice of conways life using a 4x20 lcd with each character cell split into three cells vertically using special characters so that it is a 12x20 life grid. Equipment is just a usb duino hotglued back to back with the LCD (with a layer of thin cardboard in-between), using a 4 wire interface. Sorry, I must be bored :slight_smile:

Code fragment:

#define rows 12
#define columns 20

byte display [] = {
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};

byte tmp [] = {
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
 
void loop (void){    
  lcd.init();      
  byte running=1;
  while(running==1){      
    running=0;

//update neighbor counts    
    for(int y = 0; y < rows; y++){
      for(int x = 0; x < columns; x++)
        tmp[y*columns+x]=getNeighborCount(display,x,y);          
    }    

//display the array

    for(int y = 0; y < rows; y+=3){
      lcd.gotoXY(0,y/3);
      for(int x = 0; x < columns; x++){
        byte n1=display[y*columns+x];
        byte n2=getNeighbor(display,x,y,4)*2;
        byte n3=getNeighbor(display,x,y+1,4)*4;
        lcd.LcdDataWrite(n1|n2|n3);
      }
    }      

//see who lives and who dies
    for(int y = 0; y < rows; y++){
      for(int x = 0; x < columns; x++){
        byte b = display[y*columns+x];
        byte n = tmp[y*columns+x];
        if(b==0){
          if(n==3) {
            display[y*columns+x] = 1; //birth
            running=1;
          }
        }else{
          if(n==2 || n==3) 
            display[y*columns+x] = 1; //survival
          else { 
            display[y*columns+x] = 0; //death
            running = 1;
          }
        }
      }

    }
  }      
  lcd.gotoXY(0,0);
  lcd.print("DONE");
  while(true);

}       

byte getNeighborCount(byte a[], byte x, byte y){
  byte c = 0;
  for(byte d = 0;d < 8; d++)
    if(getNeighbor(a,x,y,d) != 0)
      c++;
  return c;
}

byte getNeighbor(byte a[], byte x, byte y, byte dir){
  byte nx=x;
  byte ny=y;
  if(dir == 7 || dir == 0 || dir == 1) 
    ny--;
  if(dir == 3 || dir == 4 || dir == 5) 
    ny++;
  if(dir == 5 || dir == 6 || dir == 7) 
    nx--;
  if(dir == 1 || dir == 2 || dir == 3) 
    nx++;
  if(nx==255) nx = columns - 1;
  if(nx==columns) nx = 0;
  if(ny==255) ny = rows - 1;
  if(ny==rows) ny = 0;
  return a[ny*columns+nx];
}
...
//creating the custom fonts: 
  LcdCommandWrite(B01000000);  // set cgram 
  static byte chars[] PROGMEM ={ 
    B00000,B11111,B00000,B11111,B00000,B11111,B00000,B11111, 
    B00000,B11111,B00000,B11111,B00000,B11111,B00000,B11111, 
    B00000,B00000,B00000,B00000,B00000,B00000,B00000,B00000, 
    B00000,B00000,B11111,B11111,B00000,B00000,B11111,B11111, 
    B00000,B00000,B11111,B11111,B00000,B00000,B11111,B11111, 
    B00000,B00000,B00000,B00000,B00000,B00000,B00000,B00000, 
    B00000,B00000,B00000,B00000,B11111,B11111,B11111,B11111, 
    B00000,B00000,B00000,B00000,B11111,B11111,B11111,B11111}; 
 
    for(byte x=0;x<8;x++)       
      for(byte y=0;y<8;y++)       
          LcdDataWrite(pgm_read_byte(&chars[y*8+x])); //write the character data to the cgram

Hi dcb, that looks like it was fun to do.

Looking at the code I am not clear on how you are mapping pixels in the custom chars. The code you posted would seem to map alternating lines of pixels on and off for display values of 0 for n1,n2 and n3. I would expect the first custom char to be all zeros, but perhaps I misunderstand the logic of your code.

I ran your sketch and see most of the pixels on when they should be off. Certainly didn't look like the picture you posted.

It was kind of fun :)

I do a little trick when laying out the character bits and loading them into cgram. I lay the character bits out in the same orientation as they appear on the display (easier to maintain/design fonts that way), then I load "column" 0 first, into cgram 0

    B00000,
    B00000,
    B00000,
    B00000,
    B00000,
    B00000,
    B00000,
    B00000,

and eventually I load the last "column" into cgram position 7

                                                    ,B11111,  
                                                    ,B11111,  
                                                    ,B00000,  
                                                    ,B11111,  
                                                    ,B11111,  
                                                    ,B00000,  
                                                    ,B11111,  
                                                    ,B11111};

By pretending each block of 1s is a bit, and the top block is the least significant "bit" I go through 3 "rows" at a time and OR the rows together to index into the correct character to display.

Here is a link to the latest complete source code, need a 4x20 LCD with 5x8 character font. And might have to change the pin assignments of course.

Thanks, that explains it. That code works but its not exactly intuitive for those familiar with the Hitachi chip, it’s the kind of thing that would benefit from a comment.

FWIW, here is a variation of your code that uses Peter Fleury’s LCD library

(http://homepage.hispeed.ch/peterfleury/lcdlibrary.zip)

// LcdLife.pde
#include <lcd.h> // library can be downloaded frm: http://homepage.hispeed.ch/peterfleury/lcdlibrary.zip

#define ROWS   12
#define COLUMNS 20

byte display [ROWS][COLUMNS] = {
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
};

byte tmp [ROWS][COLUMNS] = {
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
};
  
 
 
void setup(){ 
   lcd_init(LCD_DISP_ON);
   lcd_clrscr();
   CustomFont(); 
}

void loop (void){    
//   lcd_init(LCD_DISP_ON);     
  lcd_clrscr();
  boolean changing = true;
  while(changing){      
    changing = false; // will be set back to true if any cells change

//update neighbor counts    
    for(int y = 0; y < ROWS; y++){
      for(int x = 0; x < COLUMNS; x++)
        tmp[y][x]=getNeighborCount(display,x,y);          
    }    

   //display the array
    for(int y = 0; y < ROWS; y+=3){
      lcd_gotoxy(0,y/3);
      for(int x = 0; x < COLUMNS; x++){
        byte n1=display[y][x];
        byte n2=getNeighbor(display,x,y,4)*2;
        byte n3=getNeighbor(display,x,y+1,4)*4;
        lcd_data(n1|n2|n3);
      }
    }      
    delay(100);

    //see who lives and who dies
    for(int y = 0; y < ROWS; y++){       
      for(int x = 0; x < COLUMNS; x++){
        byte b = display[y][x];
        byte n = tmp[y][x];
        if(b==0){
          if(n==3) {
            display[y][x] = 1; //birth
            changing = true;
          }
        }else{
          if(n==2 || n==3) 
            display[y][x] = 1; //survival
          else { 
            display[y][x] = 0; //death
            changing = true;
          }
        }
      }
    }
  }      
  lcd_gotoxy(0,0);
  lcd_puts("DONE");
  while(true);

}       

byte getNeighborCount(byte a[][COLUMNS], byte x, byte y){
  byte c = 0;
  for(byte d = 0;d < 8; d++)
    if(getNeighbor(a,x,y,d) != 0)
      c++;
  return c;
}

byte getNeighbor(byte a[][COLUMNS], byte x, byte y, byte dir){
  byte nx=x;
  byte ny=y;
  if(dir == 7 || dir == 0 || dir == 1) 
    ny--;
  if(dir == 3 || dir == 4 || dir == 5) 
    ny++;
  if(dir == 5 || dir == 6 || dir == 7) 
    nx--;
  if(dir == 1 || dir == 2 || dir == 3) 
    nx++;
  if(nx==255) nx = COLUMNS - 1;
  if(nx==COLUMNS) nx = 0;
  if(ny==255) ny = ROWS - 1;
  if(ny==ROWS) ny = 0;
  return a[ny][nx];
}

  // custom chars showing 3 cells per character, each column is one character.
  static byte chars[] PROGMEM ={ 
    0B00000,0B11111,0B00000,0B11111,0B00000,0B11111,0B00000,0B11111, 
    0B00000,0B11111,0B00000,0B11111,0B00000,0B11111,0B00000,0B11111, 
    0B00000,0B00000,0B00000,0B00000,0B00000,0B00000,0B00000,0B00000, 
    0B00000,0B00000,0B11111,0B11111,0B00000,0B00000,0B11111,0B11111, 
    0B00000,0B00000,0B11111,0B11111,0B00000,0B00000,0B11111,0B11111, 
    0B00000,0B00000,0B00000,0B00000,0B00000,0B00000,0B00000,0B00000, 
    0B00000,0B00000,0B00000,0B00000,0B11111,0B11111,0B11111,0B11111, 
    0B00000,0B00000,0B00000,0B00000,0B11111,0B11111,0B11111,0B11111}; 
//       ^ this column is char 0
//             ^ this column is char 1 
 
 void CustomFont(){ 
   for(byte x=0;x<8;x++)       
      for(byte y=0;y<8;y++)  {                       
           uint8_t c = pgm_read_byte(&chars[y*8+x]);
          lcd_data(c);
      } 
 }

Thanks for posting your code, it gave me a chuckle this morning :slight_smile:

Cool, glad you got it working :)

So do you think there is much interest in cheezy graphics on character LCDs? :D

Its surprisingly compelling.

I am thinking of enhancing it with a random number generator to get a more varied display :)

I had some similiar thoughts.

Random leaves a small "how do we know when we are done" problem if there is a glider or oscillator left. But not an insurmountable problem.

pre defined patterns could be progmem bitfields, could pack to 40 bytes per 16x20 pattern, or would be easier to edit at 48 bytes per pattern. Could have hundreds of patterns stored in flash. Could ask people to submit starting patterns :)

I don't know if the chunky dots are worth a library of line(x1,y1,x2,y2,color), circle(x,y,r,color), dot(x,y,color), paint(x,y,color), box(...) type commands.

The game Breakout comes to mind :) Also, I got three buttons on top of my gizmo on a3,a4,a5, left right and shoot? :D

I had some similiar thoughts... pre defined patterns could be progmem bitfields, could pack to 40 bytes per 16x20 pattern, or would be easier to edit at 48 bytes per pattern. Could have hundreds of patterns stored in flash. Could ask people to submit starting patterns :)

funny that, I am in the middle of converting the sketch to use packed arrays to make the seeding easier ;)

First cut of sketch that allows loading of patterns and objects

// LcdLife.pde
#include <lcd.h> 
#include "objects.h"

#define ROWS   12
#define COLUMNS 20
#define ELEMENTS ((COLUMNS+7)/8) // round up
#define BITVAL(_pos) (1<< (7-(_pos % 8)))
#define ALIVE true
#define DEAD false

#define NBR_GENERATIONS 4  // must be at least 2, 
byte generations[NBR_GENERATIONS][ROWS][ELEMENTS];      

#define load(_object, _row, _column)  loadObject( (object_ptr_t)&(_object), _row, _column, generations[0]) 

void setup(){     
  lcd_init(LCD_DISP_ON);
  lcd_clrscr();
#ifdef PROGRAM_CUSTOM_FONT  
   CustomFont(); 
#endif   
}

void loop (void){     
  lcd_clrscr();
  seed();
  generate();  
  lcd_gotoxy(0,0);
  lcd_puts("done");
  delay(1500);
}   

int generate(){
  int iteration = 0;
  byte thisGeneration,nextGeneration;

  do{  
    thisGeneration = iteration;    
    if(++iteration >= NBR_GENERATIONS)
      iteration = 0;
    nextGeneration = iteration;

    //display the array
    for(int row = 0; row < ROWS; row+=3){
      lcd_gotoxy(0,row/3);
      for(int column = 0; column < COLUMNS; column++){
        byte ch = (isAlive(generations[thisGeneration],row,column)     ? 1 : 0) |
          (isNeighborAlive(generations[thisGeneration],row,column,4)   ? 2 : 0) | 
          (isNeighborAlive(generations[thisGeneration],row+1,column,4) ? 4 : 0) ;                      
        lcd_data(ch);        
      }
    }      
    delay(100);
    memset(generations[nextGeneration],0,sizeof(generations[0])); // clear the array for the next generation
    //see who lives and who dies
    for(int row = 0; row < ROWS; row++){       
      for(int column = 0; column < COLUMNS; column++){
        boolean cell =  isAlive(generations[thisGeneration],row,column);
        byte n = getNeighborCount(generations[thisGeneration],row,column);
        if(cell == DEAD){
          if(n==3) {
            setLife(generations[nextGeneration],row,column,ALIVE); // birth     
          }
        }
        else{
          if(n==2 || n==3) 
            setLife(generations[nextGeneration],row,column,ALIVE);  //survival
          else { 
            setLife(generations[nextGeneration],row,column,DEAD); //death 
          }
        }
      }
    }   
  }
  while(isStable(thisGeneration) == false ) ;
  return iteration;
}

void seed(){
  memset(generations,0,sizeof(generations)); // clear all generation arrays    
  // load objects, , objects defined in objects.h, arguments are offsets from the center of the screen
  load(glider,-5,-9);  // put a glider 5 rows above the center and 9 columns left of center
  load(table,2,-9);    // put a table object 2 down from the center and 9 columns left or center  
}

void loadObject(object_ptr_t object_ptr, char y, char x, byte target[][ELEMENTS]){
  // object_ptr is the object to be loaded 
  // y and x are offsets from the center of the display
  // a is the target array to be loaded
  byte row = (ROWS/2) + y;
  byte column = (COLUMNS/2) + x;   
  for(byte ys = 0; ys < object_ptr->height; ys++ ){
    for(byte xs=0; xs < object_ptr->length; xs++ ){
      if( (row +ys < ROWS) && (column +xs < COLUMNS)){
        // this code only works on lengths up to 8 bits 
        boolean value = object_ptr->bitfield[ys] & BITVAL(xs) ;   
        setLife(target, row+ys, column+xs, value) ;        
      }   
    }          
  }      
}

boolean isStable(byte thisGeneration){
  // returns true if any two captured generations are the same
  for(byte i=0; i < NBR_GENERATIONS; i++)
    if(i != thisGeneration) 
      if(memcmp(generations[thisGeneration], generations[i], sizeof(generations[0])) == 0)
        return true;
  return false;        
} 

boolean isAlive(byte a[][ELEMENTS], byte row, byte column){
  byte  b = a[row][column /8];
  return b &  BITVAL(column) ;   
}

void setLife(byte a[][ELEMENTS], byte row, byte column, boolean state){
  byte elem = column /8;  
  byte  b = a[row][elem];
  if(state != DEAD){
    b = b | BITVAL(column);
  }
  else{
    b &= ~(BITVAL(column)) ;  
  }
  a[row][elem] = b;
}

byte getNeighborCount(byte a[][ELEMENTS], byte row, byte column){
  byte count = 0;
  for(byte d = 0;d < 8; d++)
    if(isNeighborAlive(a,row,column,d) != 0)
      count++;
  return count;
}

boolean isNeighborAlive(byte a[][ELEMENTS], byte row, byte column, byte dir){
  byte nrow=row;
  byte ncol=column;
  if(dir == 7 || dir == 0 || dir == 1) 
    nrow--;
  if(dir == 3 || dir == 4 || dir == 5) 
    nrow++;
  if(dir == 5 || dir == 6 || dir == 7) 
    ncol--;
  if(dir == 1 || dir == 2 || dir == 3) 
    ncol++;
  if(ncol==255) ncol = COLUMNS - 1;
  if(ncol==COLUMNS) ncol = 0;
  if(nrow==255) nrow = ROWS - 1;
  if(nrow==ROWS) nrow = 0;
  return isAlive(a,nrow,ncol);
}

#ifdef PROGRAM_CUSTOM_FONT
// custom chars showing 3 cells per character, each column is one character.
static byte chars[] PROGMEM ={ 
// array removed to allow sketch to fit post limit, see earlier posts
}

void CustomFont(){ 
  lcd_command(0x40);
  for(byte x=0;x<8;x++)       
    for(byte y=0;y<8;y++)  {                       
      uint8_t c = pgm_read_byte(&chars[y*8+x]);
      lcd_data(c);
    }
  lcd_gotoxy(0,0);   
}
#endif

This file contains the Life object definitions and should be placed in a tab in the same sketch as the previous post. name this file objects.h

// objects.h
// these are objects that can be used to seed the Arduino life display

typedef struct {
         byte height ; 
         byte length ;  
         byte bitfield[];
     } object_t, *object_ptr_t;  // generic structure to reference any object 


struct {
         byte height ; 
         byte length ;  
         byte bitfield[2];
       } table = {2,4,  { 0b10010000 ,
                    0b11110000 } };
                          
struct {
         byte height ; 
         byte length ;  
         byte bitfield[3];
       } glider  = {3,3, { 0b00100000 ,
                     0b10100000 ,
                     0b01100000 } };  
                          
struct {
         byte height ; 
         byte length ;  
         byte bitfield[3];
       } glider2 = {3,3, { 0b10100000 ,
                    0b11000000 ,
                    0b11110000 } };                         
struct {
         byte height ; 
         byte length ;  
         byte bitfield[4];
       } loaf = {4,4,    { 0b01100000  ,
                     0b10010000  ,
                     0b01010000  ,
                     0b00100000} };                         
struct {
         byte height ; 
         byte length ;  
         byte bitfield[3];
       } ship = {3,3,    { 0b11000000  ,
                     0b10100000  ,
                     0b01100000   } };

struct {
         byte height ; 
         byte length ;  
         byte bitfield[4];
       } behive = {4,3,  { 0b01000000  ,
                     0b10100000  ,
                     0b10100000  ,
                     0b01000000} }; 
                           
struct {
         byte height ; 
         byte length ;  
         byte bitfield[1];
       } blinker = {1,3, { 0b11100000 } };
                          
                         
struct {
         byte height ; 
         byte length ;  
         byte bitfield[2];
       } block = {2,2, { 0b11000000  ,      
                   0b11000000 } };

I havent figured out the lcd link you sent, or the 4 bit library for that matter. I'm not at a level I can revamp very quickly yet, but I see where you are going with the shape library.

Just for fun, I was goofing off with 5x3 fonts like these:

And wondered what it would do in life? So I used this starting pattern:

And I was suprised to see it run for 392 generations! :)

byte display[] = {
  0,1,0,0,1,1,0,0,0,1,1,0,1,1,0,0,1,1,1,0,
  1,0,1,0,1,0,1,0,1,0,0,0,1,0,1,0,1,0,0,0,
  1,1,1,0,1,1,0,0,1,0,0,0,1,0,1,0,1,1,0,0,
  1,0,1,0,1,0,1,0,1,0,0,0,1,0,1,0,1,0,0,0,
  1,0,1,0,1,1,0,0,0,1,1,0,1,1,0,0,1,1,1,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  1,1,1,0,1,1,1,0,1,0,1,0,1,1,1,0,1,1,1,0,
  1,0,0,0,1,0,0,0,1,0,1,0,0,1,0,0,0,0,1,0,
  1,1,0,0,1,1,1,0,1,1,1,0,0,1,0,0,0,0,1,0,
  1,0,0,0,1,0,1,0,1,0,1,0,0,1,0,0,1,0,1,0,
  1,0,0,0,1,1,1,0,1,0,1,0,1,1,1,0,1,1,1,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};

Here is the sketch that continuously generates random patterns. It starts with the pattern in your sketch but then seeds each new cycle with a random selection of objects at random places on the screen.

I have used LCD code from the link here: http://opengauge.org/diympggauge/life.pde A few new functions were added and the pins have been mapped to my wiring but it should be easy to get going.

The sketch is in four files, all should be placed in same sketch directory:
LcdLife.pde ‚Äď this is the main file

Objects.h ‚Äď this is the definition for the shapes used to seed new patterns. You can add shapes by creating a new Object structure for each shape following the examples in the file. You need to add the new shape to the end of the objectTable array and add the name to the end of the object enum list.

Lcd.pde has the LCD code. Change the defines so that the pins match your wiring
Lcd.h is the header for the lcd class.

That’s it, have fun!

// LcdLife.pde
#include "lcd.h"
#include "objects.h"

/**** user configurable defines *****/
#define ROWS   12
#define COLUMNS 20

#define DELAY 20
#define MAX_ITERATIONS  500
#define STABLE_GENERATIONS 4

#define MIN_OBJECTS 2
#define MAX_OBJECTS 4
#define MARGIN 4
/** end of user configurable defines ****/

#define ELEMENTS ((COLUMNS+7)/8) //this is the number of bytes needed to hold a column
#define BITVAL(_pos) (1<< (7-(_pos % 8)))  // macro to get a bitfield value

#define ALIVE true
#define DEAD false

byte generations[STABLE_GENERATIONS][ROWS][ELEMENTS]; // arrays used to calcualte, draw and check if iterations are changing     
unsigned int loops;

void setup(){
  lcd.begin();
  loops = 0;
}

void loop (void){
  lcd.clear();
  seed(loops++);
  unsigned int i = generate();
  lcd.gotoXY(0,0);
  lcd.print(i);
  lcd.print(" iterations");
  delay(1500);
}   

unsigned int generate(){
  unsigned int iteration = 0;
  byte thisGeneration,nextGeneration;
  thisGeneration = nextGeneration  = 0;
  do{
    thisGeneration  = iteration % STABLE_GENERATIONS;
    nextGeneration =  (thisGeneration+1) % STABLE_GENERATIONS;
    //display the array
    for(int row = 0; row < ROWS; row+=3){
      lcd.gotoXY(0,row/3);
      for(int column = 0; column < COLUMNS; column++){
        char ch = (isAlive(generations[thisGeneration],row,column)     ? 1 : 0) |
          (isNeighborAlive(generations[thisGeneration],row,column,4)   ? 2 : 0) | 
          (isNeighborAlive(generations[thisGeneration],row+1,column,4) ? 4 : 0) ;                      
        lcd.print(ch);
      }
    }
    if(iteration == 0)
       delay(1500);
    else
       delay(DELAY);
    memset(generations[nextGeneration],0,sizeof(generations[0])); // clear the array for the next generation
    //see who lives and who dies
    for(int row = 0; row < ROWS; row++){
      for(int column = 0; column < COLUMNS; column++){
        boolean cell =  isAlive(generations[thisGeneration],row,column);
        byte n = getNeighborCount(generations[thisGeneration],row,column);
        if(cell == DEAD){
          if(n==3) {
            setLife(generations[nextGeneration],row,column,ALIVE); // birth
          }
        }
        else{
          if(n==2 || n==3)
            setLife(generations[nextGeneration],row,column,ALIVE);  //survival
          else { 
            setLife(generations[nextGeneration],row,column,DEAD); //death
          }
        }
      }
    }
  }
  while(isStable(thisGeneration) == false  && ++iteration < MAX_ITERATIONS );
  return iteration;
}

void seed(boolean useRandom){
  // a hard coded seed is used if useRandom is zero, otherwise a random seed is used
  memset(generations,0,sizeof(generations)); // clear all generation arrays    
  if(useRandom == false){
    loadObject(Table,2,-9);
    loadObject(Glider, -5,-9);
  }
  else{
  // load some random objects and place at random positions 
     byte nbrObjects = random(MIN_OBJECTS,MAX_OBJECTS+1);  // make random number of objects
     for(byte i=0; i < nbrObjects;i++){
        byte obj = random(7); // OBJECT_COUNT);
        int column = random(MARGIN - (COLUMNS/2), (COLUMNS/2) - MARGIN); 
        int row = random(MARGIN - (ROWS/2)   , (ROWS/2) - MARGIN); 
        loadObject(obj,row,column);
     }  
  }  
}

void loadObject(object_ptr_t object_ptr, char y, char x){
  // y and x are offsets from center of display
  byte row = (ROWS/2) + y;
  byte column = (COLUMNS/2) + x;   
  for(byte ys = 0; ys < object_ptr->height; ys++ ){
    for(byte xs=0; xs < object_ptr->length; xs++ ){
      if( (row +ys < ROWS) && (column +xs < COLUMNS)){
        // this code only works on lengths up to 8 bits 
        boolean value = object_ptr->bitfield[ys] & BITVAL(xs) ; 
        setLife(generations[0], row+ys, column+xs, value) ;     
      }   
    }          
  }      
}

void loadObject(int object, char y, char x){
  loadObject(objectTable[object], y, x );
}

boolean isStable(byte thisGeneration){
  // returns true if any two captured generations are the same
  for(byte i=0; i < STABLE_GENERATIONS; i++)
    if(i != thisGeneration) 
      if(memcmp(generations[thisGeneration], generations[i], sizeof(generations[0])) == 0)
        return true;
  return false;        
} 

boolean isAlive(byte a[][ELEMENTS], byte row, byte column){
  byte  b = a[row][column /8];
  return b &  BITVAL(column) ;   
}

void setLife(byte a[][ELEMENTS], byte row, byte column, boolean state){
  byte elem = column /8;  
  byte  b = a[row][elem];
  if(state != DEAD){
    b = b | BITVAL(column);
  }
  else{
    b &= ~(BITVAL(column)) ;  
  }
  a[row][elem] = b;
}

byte getNeighborCount(byte a[][ELEMENTS], byte row, byte column){
  byte count = 0;
  for(byte d = 0;d < 8; d++)
    if(isNeighborAlive(a,row,column,d) != 0)
      count++;
  return count;
}

boolean isNeighborAlive(byte a[][ELEMENTS], byte row, byte column, byte dir){
  byte nrow=row;
  byte ncol=column;
  if(dir == 7 || dir == 0 || dir == 1) 
    nrow--;
  if(dir == 3 || dir == 4 || dir == 5) 
    nrow++;
  if(dir == 5 || dir == 6 || dir == 7) 
    ncol--;
  if(dir == 1 || dir == 2 || dir == 3) 
    ncol++;
  if(ncol==255) ncol = COLUMNS - 1;
  if(ncol==COLUMNS) ncol = 0;
  if(nrow==255) nrow = ROWS - 1;
  if(nrow==ROWS) nrow = 0;
  return isAlive(a,nrow,ncol);
}
//lcd.pde
// this file has the lcd interface code
// it should be modifed as necessary to interface with your LCD

#include "lcd.h"  
#include <avr/pgmspace.h>
  
/******   LCD Pins- change these to match your wiring   ********/   
#define DIPin     7 // register select RS       
#define EnablePin 5  // enable 

#define USE_RW      // uncomment this if your lcd does not  have the RW pin permanently grounded
#ifdef USE_RW
#define RWPin     6        
#endif

#define DB4Pin 9     // data pins   
#define DB5Pin 10        
#define DB6Pin 11        
#define DB7Pin 12           

//#define USE_PWM   // uncomment this if you use PWM for brightness and contrast
#ifdef USE_PWM
#define brightness 180 //middle button cycles through these brightness settings       
#define contrast 15 
#define ContrastPin 6  
#define BrightnessPin 9 //control with a transistor please if you use it
#endif

/*********  end of user configurable defines ************/

LCD lcd = LCD();

void LCD::begin(){
  pinMode(EnablePin,OUTPUT);
  pinMode(DIPin,OUTPUT);  
  pinMode(DB4Pin,OUTPUT);
  pinMode(DB5Pin,OUTPUT);
  pinMode(DB6Pin,OUTPUT);
  pinMode(DB7Pin,OUTPUT);
#ifdef USE_RW
  pinMode(RWPin,OUTPUT); 
  digitalWrite(RWPin,LOW);
#endif 

#ifdef USE_PWM 
pinMode(BrightnessPin,OUTPUT);      
  analogWrite(BrightnessPin,255-brightness);
  delay(500); 
  pinMode(ContrastPin,OUTPUT);       
  analogWrite(ContrastPin,contrast);       
  delay(500); 
#endif  

  lcd.init();
  customFont(); // create the custom fonts
  lcd.LcdCommandWrite(B00000001);  // clear display, set cursor position to zero          
  lcd.LcdCommandWrite(B10000000);  // set dram to zero 
} 

//LCD functions       
LCD::LCD(){       
}       
    
void LCD::gotoXY(byte x, byte y){       
  // x and y start from 0   
  byte dr=x+0x80;       
  if (y==1)        
    dr += 0x40;       
  if (y==2)        
    dr += 0x14;       
  if (y==3)        
    dr += 0x54;       
  LcdCommandWrite(dr);         
}       
  
void LCD::print(char * string){       
  byte x = 0;       
  char c = string[x];       
  while(c != 0){       
    LcdDataWrite(c);        
    x++;       
    c = string[x];       
  }       
}       
  
void LCD::print(char c){          
    LcdDataWrite(c);              
}  

void LCD::print(unsigned int n){
   byte buf[6];  // prints up to 5 digits  
   byte i=0;
   if(n==0)
      print('0');
   else{
     while(n>0 && i <= 10){
        buf[i++] = n % 10;  // n % base
      n /= 10;   // n/= base
      }
      for(; i >0; i--)
         print((char) (buf[i-1] < 10 ? '0' + buf[i-1] : 'A' + buf[i-1] - 10));        
   }
}

void  LCD::clear(){
  lcd.LcdCommandWrite(B00000001);  // clear display, set cursor position to zero  
}
 
//do the lcd initialization voodoo       
void LCD::init(){       
  LcdCommandWrite(B00000010);  // 4 bit operation         
  
  LcdCommandWrite(B00101000);// 4-bit interface, 2 display lines, 5x8 font        
  LcdCommandWrite(B00001100);  // display control:        
  delay(1);
  LcdCommandWrite(B00000110);  // entry mode set: increment automatically, no display shift 
  delay(1);    
}         
  
void  LCD::tickleEnable(){        
  // send a pulse to enable        
  digitalWrite(EnablePin,HIGH);        
  delayMicroseconds(1);  // pause 1 ms according to datasheet        
  digitalWrite(EnablePin,LOW);        
  delayMicroseconds(1);  // pause 1 ms according to datasheet        
}         
  
void LCD::cmdWriteSet(){        
  digitalWrite(EnablePin,LOW);        
  delayMicroseconds(1);  // pause 1 ms according to datasheet        
  digitalWrite(DIPin,0);        
}        
  
byte LCD::pushNibble(byte value){        
  digitalWrite(DB7Pin, value & 128);        
  value <<= 1;        
  digitalWrite(DB6Pin, value & 128);        
  value <<= 1;        
  digitalWrite(DB5Pin, value & 128);        
  value <<= 1;        
  digitalWrite(DB4Pin, value & 128);        
  value <<= 1;        
  return value;       
}       
  
void LCD::LcdCommandWrite(byte value){        
  value=pushNibble(value);
  cmdWriteSet();      
  tickleEnable(); 
  value=pushNibble(value); 
  cmdWriteSet(); 
  tickleEnable();
  delay(1); 
}
  
void LCD::LcdDataWrite(byte value){
  digitalWrite(DIPin, HIGH);
  value=pushNibble(value);
  tickleEnable();
  value=pushNibble(value);
  tickleEnable();
  delay(1);
}

static byte chars[] PROGMEM = {
    B00000,B11111,B00000,B11111,B00000,B11111,B00000,B11111,  
    B00000,B11111,B00000,B11111,B00000,B11111,B00000,B11111,  
    B00000,B00000,B00000,B00000,B00000,B00000,B00000,B00000,  
    B00000,B00000,B11111,B11111,B00000,B00000,B11111,B11111,  
    B00000,B00000,B11111,B11111,B00000,B00000,B11111,B11111,  
    B00000,B00000,B00000,B00000,B00000,B00000,B00000,B00000,  
    B00000,B00000,B00000,B00000,B11111,B11111,B11111,B11111, 
    B00000,B00000,B00000,B00000,B11111,B11111,B11111,B11111};
    
void LCD::customFont(){
//creating the custom fonts:
  LcdCommandWrite(B01000000);  // set cgram
  for(byte x=0;x<8;x++)     
     for(byte y=0;y<8;y++)     
        LcdDataWrite(pgm_read_byte(&chars[y*8+x])); //write the character data to the character generator ram
}
// lcd.h
#ifndef LOCAL_LCD_h
#define LOCAL_LCD_h

//LCD prototype
class LCD{
private: 
  void init();
  byte pushNibble(byte value);
  void tickleEnable();
  void cmdWriteSet();
  void LcdCommandWrite(byte value);
  void LcdDataWrite(byte value);
public:
  LCD( ) ;
  void begin();
  void gotoXY(byte x, byte y);
  void print(char * string);     
  void print(char c) ;                   
  void print(unsigned int n);
  void clear();
  void customFont();
};

extern LCD lcd; 

#endif
// objects.h
// these are objects that can be displayed on the Arduino life screen
// to add an object, create and initialise a structure following the examples here
// add a refernece to your structure to the object table, name your object by adding to the enum list        

// structure template for generic objects
typedef struct {
         byte height ; 
         byte length ;  
         byte bitfield[];
     } object_t, *object_ptr_t;

// definitions and initialisations for all defined objects here
struct {
         byte height ; 
         byte length ;  
         byte bitfield[2];
       } table = {2,4,  { 0b10010000 ,
                          0b11110000 } };
                          
struct {
         byte height ; 
         byte length ;  
         byte bitfield[3];
       } glider  = {3,3, { 0b00100000 ,
                           0b10100000 ,
                           0b01100000 } };  
                          
struct {
         byte height ; 
         byte length ;  
         byte bitfield[3];
       } glider2 = {3,3, { 0b10100000 ,
                          0b11000000 ,
                          0b11110000 } };                         
struct {
         byte height ; 
         byte length ;  
         byte bitfield[4];
       } loaf = {4,4,    { 0b01100000  ,
                           0b10010000  ,
                           0b01010000  ,
                           0b00100000} };                         
struct {
         byte height ; 
         byte length ;  
         byte bitfield[3];
       } ship = {3,3,    { 0b11000000  ,
                           0b10100000  ,
                           0b01100000   } };

struct {
         byte height ; 
         byte length ;  
         byte bitfield[4];
       } behive = {4,3,  { 0b01000000  ,
                           0b10100000  ,
                           0b10100000  ,
                           0b01000000} }; 
                           
struct {
         byte height ; 
         byte length ;  
         byte bitfield[1];
       } blinker = {1,3, { 0b11100000 } };
                          
                         
struct {
         byte height ; 
         byte length ;  
         byte bitfield[2];
       } block = {2,2, { 0b11000000  ,      
                         0b11000000 } };                          

// place all the objects in the object table                         
 object_ptr_t objectTable[] = {
             (object_ptr_t)&table,
             (object_ptr_t)&glider,
             (object_ptr_t)&loaf,
             (object_ptr_t)&ship,
             (object_ptr_t)&behive,
             (object_ptr_t)&blinker,
             (object_ptr_t)&block
 };   

#define OBJECT_COUNT (sizeof( objectTable/ sizeof(object_ptr_t)))

// enumerate all the objects, add any new objects to the end of this enum (note case difference )
// the life program references all objects using the names in this list
 enum  {Table,Glider,Loaf,Ship,Behive,Blinker,Block};

Big chunky LCD numbers would be an obvious display mechanism for your time library too :)