In this second part of the series, we look at how Print_Cards produces a PDF file containing a specified number of unique Bingo cards.
Table of Contents
The symbol returns the reader to the top of the Table of Contents.
1. Introduction
This article is the second of three articles describing the Bingo game suite that I developed over the past few months. The articles are published in the hope that the software may prove useful to the articles' readers. The three articles address the US version of Bingo that has 75 numbered balls.
If the reader needs a refresher in the game of Bingo, please refer to the first article in this series.
In order for a game of Bingo to be played, it is necessary to generate and print the Bingo cards that players will use.
This article discusses the bolded items in the preceding figure. The Bingo card file must have been already generated by the Generate_Cards [^] program.
1.1. Printed Bingo Card
Printed Bingo cards are composed of six rows of five columns. The first row is the card heading and contains the letters 'B', 'I', 'N', 'G', and 'O', each letter appearing in its own column. The five remaining rows contain the numeric values. When 24 random numbers are inserted, the card may appear as follows:
The figure on the right was created with the requirement that stars be distributed throughout the Bingo cards. When this requirement is imposed, each Bingo card will have one star randomly placed thereon. The location of the star on each card is recorded in the Bingo card file.
The center square of the 5 x 5 card is known as a "free space" that is considered as called.
Physically, because I was building the suite on a home computer, I decided that the following design criteria should apply.
- The format of the output file of the program will be Portable Document Format (PDF).
- The dimensions of the paper on which the PDF images will be printed are either 8-1/2 x 11 or 8-1/2 x 14 inches.
1.2. Number of Cards
Bingo is played in a large number of locations, each with its own rules. In this article, I am going to suggest the following rules:
- A "game" is made up of three Bingo cards printed on a sheet. Each of the three Bingo cards on a sheet is printed in the same color.
- A "session" is made up of six games:
- During a day, there may be many sessions. For my purposes, the number of sessions per day is seven.
- Between sessions, there may be a special game that requires the winner to have all squares on a Bingo card (known as a "black out" or "cover all"). Normally, a single Bingo card is provided for this intersession game.
- The Bingo cards in play may have stars printed on them, allowing a player to call "stars" when the numbers with stars on all three cards on a sheet have been called.
Information required to determine how many Bingo cards to print is dependent on the following:
- How many Bingo cards can be printed on a sheet? A Bingo card has the dimensions 4 x 4-1/2 inches. On a 8-1/2 x 11 inch sheet, four Bingo cards can be printed; on a 8-1/2 x 14 inch, six Bingo cards can be printed.
- Note that in the commercial printing of Bingo cards, paper sheets are multiples of 8-1/2 x 14 inch. However, a home printer can usually only print a 8-1/2 x 11 inch sheet. For this project, a default sheet size of 8-1/2 x 14 inches will be used. This then means that six Bingo cards can be printed on a sheet.
- How many games are in a session? Alternately, how many colors are used to color sheets? The colors that are offered in this project are the following:
- The number of colors selected determines the number of games in a session. In this project, at least one color and up to all six colors may be chosen; there is no default.
- The number of sessions that will be played in a day needs to be specified.
- Finally, the number of people who will be playing Bingo on this day needs to be specified.
1.3. Bingo Card Dimensions
The most difficult part of the Print_Cards
project was determining the dimensions of a Bingo card and the positional relationship of one card with another. After some experimenting, the following figure depicts these dimensions.
These dimensions are defined in the Utilities
project in the class GDI_Card_Drawing_Data
.
using System.Drawing;
namespace Utilities
{
public class GDI_Card_Drawing_Data
{
public const int CARD_HEIGHT = 324;
public const int CARD_WIDTH = 288;
public const int X_OFFSET = 18;
public const int Y_OFFSET = 18;
public static Point CARDS_ORIGIN = new Point ( X_OFFSET,
Y_OFFSET );
public static Point HEADER_OFFSET = new Point ( 9,
9 );
public const int HEADER_WIDTH = 54;
public const int HEADER_HEIGHT = 36;
public const int HEADER_FONT_SIZE = 25;
public static Color HEADER_BRUSH_COLOR = Color.White;
public const int RECTANGLE_OFFSET_X = 9;
public const int RECTANGLE_OFFSET_Y = 36;
public static Point RECTANGLE_OFFSET =
new Point ( RECTANGLE_OFFSET_X,
RECTANGLE_OFFSET_Y );
public const int CARDS_OFFSET_X = CARD_WIDTH +
X_OFFSET;
public const int CARDS_OFFSET_Y = CARD_HEIGHT;
public const int CARDS_PER_SHEET_4 = 4;
public const int CARDS_PER_SHEET_6 = 6;
public const int CELL_HEIGHT = 54;
public static Point CELLS_ORIGIN = new Point ( X_OFFSET,
36 );
public const int CELL_WIDTH = 54;
public const int CELLS_PER_COLUMN = 5;
public const int CELLS_PER_ROW = 5;
public const int FOOTER_OFFSET_Y = RECTANGLE_OFFSET_Y +
( CELLS_PER_COLUMN *
CELL_HEIGHT );
public static Point FOOTER_ORIGIN =
new Point ( RECTANGLE_OFFSET_X,
FOOTER_OFFSET_Y );
public const int FOOTER_WIDTH = ( CELLS_PER_ROW *
CELL_WIDTH );
public const int FOOTER_HEIGHT = CARD_HEIGHT -
HEADER_HEIGHT -
( CELLS_PER_COLUMN *
CELL_HEIGHT );
}
}
The units for the various positional constants is Points, required by the PDFSharp [^] library. The values in inches are provided to the right as a comment.
Because stars may be requested, the Bingo cards are printed in the order P0, P2, P4, P1, P3, P5. This causes the group_face
number to increase down the game sheet from the top Bingo card to the bottom Bingo card.
Speaking of stars, they are drawn within a cell of a Bingo card using the dimensions in the following figure
2. Printing the Bingo Cards
The Print_Cards
program prints specified Bingo cards from a Bingo card file that was earlier generated by the Generate_Cards
program.
In addition to the name of the desired input Bingo card file, cards_per_sheet
, games_per_session
, sessions_per_day
, and number_of_players
, Print_Cards
must be told where, within the Bingo card file, the first Bingo card for a particular execution is found (starting_face_number
).
Recall from Part 1, the Bingo cards file is a direct access file and that the formula used to access a specific record is:
public const int CARD_DATA_VALUES = 24;
public const int CARD_VALUES_AT = 128;
public const int CARD_VALUES_SIZE =
CARD_DATA_VALUES *
sizeof ( byte );
public const int CARD_SIZE = CARD_VALUES_SIZE;
long file_offset = CONST.CARD_VALUES_AT +
( ( face_number - 1 ) *
CONST.CARD_VALUES_SIZE );
where initially face_number
is set equal to starting_face_number
.
2.1. Card_File_IO Class
To obtain the Bingo cards used for generating the PDF file, the FileStream Class [^] is used. The various methods needed for retrieving the card file are encapsulated in the Card_File_IO
class in the Utilities
project. Note that writing the PDF file is the responsibility of the PDFSharp library.
using System;
using System.IO;
using System.Linq;
using CONST = Utilities.Constants;
namespace Utilities
{
public class Card_File_IO
{
private FileStream fs = null;
public bool open_file ( string path,
ref string error_message )
{
bool success = false;
error_message = String.Empty;
try
{
fs = new FileStream ( path,
FileMode.Open,
FileAccess.ReadWrite,
FileShare.Read );
success = true;
}
catch ( Exception e )
{
error_message = e.Message;
success = false;
}
return ( success );
}
public int read_card_from_file (
ref byte [ ] card_buffer,
long file_offset,
ref string error_message )
{
int bytes_read = 0;
card_buffer = Enumerable.Repeat (
( byte ) 0,
card_buffer.Length ).ToArray ( );
if ( fs != null )
{
try
{
fs.Seek ( file_offset, SeekOrigin.Begin );
bytes_read = fs.Read ( card_buffer,
0,
card_buffer.Length );
}
catch ( Exception ex )
{
error_message = ex.Message;
bytes_read = -1;
}
}
return ( bytes_read );
}
public bool read_bytes_from_file (
byte [ ] bytes,
int bytes_offset,
int bytes_count,
long file_offset,
ref string error_message )
{
bool success = false;
error_message = String.Empty;
if ( fs != null )
{
try
{
fs.Seek ( file_offset, SeekOrigin.Begin );
fs.Read ( bytes, bytes_offset, bytes_count );
success = true;
}
catch ( Exception e )
{
error_message = e.Message;
success = false;
}
}
return ( success );
}
public bool close_file ( ref string error_message)
{
bool success = false;
error_message = String.Empty;
try
{
if ( fs != null )
{
fs.Close ( );
}
success = true;
}
catch ( Exception e )
{
error_message = e.Message;
success = false;
}
return ( success );
}
public bool Dispose ( ref string error_message )
{
bool success = false;
error_message = String.Empty;
try
{
if ( fs != null )
{
fs.Dispose ( );
fs = null;
}
success = true;
}
catch ( Exception e )
{
error_message = e.Message;
success = false;
}
return ( success );
}
}
}
As with Generate_Cards
, the FileStream
(fs
) is private to the Card_File_IO
class and within the Print_Cards
program, it is created once and closed by the application_cleanup
event handler, triggered by the application exit.
void application_cleanup ( object sender,
EventArgs e )
{
cfio.close_file ( ref error_message );
cfio.Dispose ( ref error_message );
}
There are really no surprises in the Card_File_IO
class. All of the methods contained therein are simple and straight-forward.
2.2. Generating the Cards
In discussing the layout of the printed Bingo card, it is useful to recall the relationship between the ordering of the Bingo card data in the Bingo card file and the ordering of the cells on the printed Bingo card. It is best summarized by the following figure:
The grayed-out square in the center of the Bingo card is the "free space" cell and is considered already called. face_number
is used to access a specific card. Generate_Cards
replaces the placeholders 0 ... 24 by random numbers. It is the responsibility of Print_Cards
to produce a correctly colored Bingo card in a format that can be printed.
Once required data has been collected, Print_Cards
validates the input with entries_valid
.
bool entries_valid ( )
{
if ( cards_per_sheet4_RB.Checked )
{
cards_per_sheet = CDD.CARDS_PER_SHEET_4;
}
else
{
cards_per_sheet = CDD.CARDS_PER_SHEET_6;
}
number_of_colors = PDF_desired_colors.Count;
if ( number_of_colors <= 0 )
{
display_in_message_RTB (
"At least one color must be specified" );
return ( false );
}
display_stars = display_stars_CHKBX.Checked;
bingo_cards_per_game =
( int ) bingo_cards_per_game_NUD.Value;
games_per_session =
Convert.ToInt32 ( games_per_session_TB.Text );
sessions_per_day = ( int ) sessions_per_day_NUD.Value;
number_of_players = ( int ) number_of_players_NUD.Value;
starting_face_number =
Convert.ToInt32 ( starting_face_number_TB.Text );
if ( starting_face_number <= 0 )
{
display_in_message_RTB (
"The starting number must be greater than zero" );
return ( false );
}
series_number = Convert.ToInt32 ( series_number_TB.Text );
if ( series_number <= 0 )
{
series_number = date_created;
series_number_TB.Text = series_number.ToString ( );
}
total_cards = bingo_cards_per_game *
games_per_session *
sessions_per_day *
number_of_players;
if ( total_cards > number_cards )
{
display_in_message_RTB (
String.Format (
"Total number of cards to print ({1}){0}" +
"is greater than the number of cards ({2}){0}" +
"in the Bingo card file.",
Environment.NewLine,
total_cards,
number_cards ) );
return ( false );
}
if ( ( total_cards + starting_face_number ) >
number_cards )
{
display_in_message_RTB (
String.Format (
"Total number of cards to print ({1}){0}" +
"plus 'Starting Face Number' is greater than " +
"the number{0}" +
"of cards ({2}){0} in the Bingo card file.",
Environment.NewLine,
total_cards,
number_cards ) );
return ( false );
}
display_pdf = false;
if ( can_display_pdf )
{
display_pdf = display_pdf_CHKBX.Checked;
}
ending_face_number = starting_face_number +
total_cards - 1;
return ( true );
}
2.3. PDFSharp Library
PDFSharp is the Open Source .NET library that easily creates and processes PDF documents on the fly from any .NET language. The same drawing routines can be used to create PDF documents, draw on the screen, or send output to any printer.
PDFSharp [^]
The PDFSharp library is available at SourceForge [^]. I downloaded it to C:\Downloads\PDFSharp and referred to its .DLL at:
C:\Downloads\
PDFSharp\
pdfsharp\
sourceCode\
sourceCode\
PDFsharp\
code\
PdfSharp\
bin\
Debug\
PdfSharp.dll
Once the project references were specified, the following using
statements could be included in Print_Card.cs:
using PdfSharp;
using PdfSharp.Drawing;
using PdfSharp.Pdf;
Throughout Print_Cards
are invocations of the PDFSharp library. The following were encapsulated in helper routines. open_PDF_document
is invoked once; add_a_PDF_page
and close_PDF_page
are invoked for each PDF sheet produced.
void open_PDF_document ( string title )
{
PDF_document = new PdfDocument ( );
PDF_document.Info.Title = title;
}
void open_PDF_document ( )
{
open_PDF_document ( "Bingo Cards" );
}
void add_a_PDF_page ( )
{
PDF_page = PDF_document.AddPage ( );
PDF_page.Orientation = PageOrientation.Portrait;
PDF_page.Size = PageSize.Legal;
PDF_graphics = XGraphics.FromPdfPage ( PDF_page );
}
void close_PDF_page ( )
{
if ( PDF_graphics != null )
{
PDF_graphics.Dispose ( );
PDF_graphics = null;
}
if ( PDF_page != null )
{
PDF_page.Close ( );
PDF_page = null;
}
}
The PDF variables used in Print_Cards
are:
List < XColor > PDF_desired_colors = new List < XColor > ( );
PdfDocument PDF_document = null;
XStringFormat PDF_format = new XStringFormat ( ) {
LineAlignment = XLineAlignment.Center,
Alignment = XStringAlignment.Center
};
XGraphics PDF_graphics = null;
PdfPage PDF_page = null;
Note that PDFSharp uses many of the same concepts as does the Graphics Class [^]. To differentiate, PDFSharp prefixes the GDI names with an "X". For example, "Graphics
" becomes "XGraphics
", "Color
" becomes "XColor
", and so forth.
2.4. Rendering the Bingo Cards
When entries_valid
returns true, Print_Cards
initiates the printing process. This process is a sequence of relatively simple methods, each of which performs a simple task:
render_PDF_document
render_Bingo_sheet
build_bingo_card_sheet
render_a_card
render_colored_background
render_header
render_cells_and_values
render_center_cell_contents
render_star
render_footer
The printing process is started by render_PDF_document
that opens a new PDF document, sets the initial face_number
from the starting_face_number
supplied by the user, and loops over the face_numbers
invoking render_Bingo_sheet
for each color specified by the user.
void render_PDF_document ( )
{
int face_number = 0;
open_PDF_document ( );
face_number = starting_face_number;
while ( face_number <= ending_face_number )
{
foreach ( XColor xcolor in PDF_desired_colors )
{
render_Bingo_sheet ( xcolor,
ref face_number );
}
}
PDF_document.Close ( );
}
Note that when rendering Bingo cards, Print_Cards
orders the cards by color.
To be able to pass the defining data about a Bingo card, the Bingo_Card
class was defined.
using System.Collections.Generic;
using PdfSharp.Drawing;
namespace Utilities
{
public class Bingo_Card
{
public List < int > Card_Numbers { get; set; }
public XPoint Card_Origin { get; set; }
public XColor Card_XColor { get; set; }
public int Face_Number { get; set; }
public int Star_At { get; set; }
public Bingo_Card ( )
{
Card_Numbers = null;
Card_Origin = new XPoint ( );
Card_XColor = XColor.Empty;
Face_Number = 0;
Star_At = 0;
}
}
}
The Bingo_Card
class merely collects information about one Bingo card.
render_Bingo_sheet
is invoked to render a single Bingo sheet that has Bingo cards printed with one color.
void render_Bingo_sheet ( XColor xcolor,
ref int face_number )
{
List < BC > bingo_card_sheet;
long file_offset = CONST.CARD_VALUES_AT +
( ( face_number - 1 ) *
CONST.CARD_VALUES_SIZE );
add_a_PDF_page ( );
bingo_card_sheet = build_bingo_card_sheet (
file_offset,
xcolor,
ref face_number );
foreach ( BC card in bingo_card_sheet )
{
render_a_card ( card );
progress_bar.PerformStep ( );
}
close_PDF_page ( );
}
By invoking add_a_PDF_page, PDFSharp begins a new PDF page, establishes the page orientation (portrait), establishes the page size (legal - 8-1/2 x 14), and creates the graphics object that will be used to draw the page. (If it has not already dawned on the reader, PDFSharp is a graphics library that produces printable pages. Familiarity with the .NET graphics library greatly assists the developer in understanding how PDFSharp operates.)
build_bingo_card_sheet
, as its name implies, builds the template for a bingo_card_sheet
that includes all of the data required to render a Bingo card sheet of a specific color and starting at a specific face_number
.
List < BC > build_bingo_card_sheet ( long file_offset,
XColor xcolor,
ref int face_number )
{
List < BC > cards = new List < BC > ( );
int cards_per_sheet_div_2 = cards_per_sheet / 2;
XPoint origin = new XPoint ( CDD.X_OFFSET,
CDD.Y_OFFSET );
cards.Clear ( );
for ( int i = 0; ( i < cards_per_sheet ); i++ )
{
byte [ ] buffer =
new byte [ CONST.CARD_DATA_VALUES ];
BC card = new BC ( );
List < int > numbers;
cfio.read_card_from_file ( ref buffer,
file_offset,
ref error_message );
numbers = byte_array_to_int_list ( buffer );
card.Star_At = numbers [ CONST.CENTER_CELL_INDEX ];
numbers.RemoveAt ( CONST.CENTER_CELL_INDEX );
numbers.Insert ( CONST.CENTER_CELL_INDEX,
face_number );
card.Card_XColor = xcolor;
card.Card_Numbers = numbers;
card.Card_Origin = origin;
card.Face_Number = face_number;
cards.Add ( card );
origin.Y += CDD.CARD_HEIGHT;
if ( i == ( cards_per_sheet_div_2 - 1 ) )
{
origin.X += CDD.CARD_WIDTH;
origin.Y = CDD.Y_OFFSET;
}
file_offset += CONST.CARD_SIZE;
face_number++;
}
return ( cards );
}
build_bingo_card_sheet
invokes byte_array_to_int_list
to convert the array buffer to a list. Lists are significantly easier to work with than arrays. When build_bingo_card_sheet
returns, the bingo_card_sheet
has been filled with the information needed to write a PDF page, accomplished by invoking render_a_card
for each card in the bingo_card_sheet
.
void render_a_card ( BC card )
{
int color_index = 0;
XPoint footer_origin = card.Card_Origin;
string group_face = String.Empty;
XPoint rectangle_origin = card.Card_Origin;
render_colored_background ( card );
render_header ( card );
rectangle_origin.X += CDD.RECTANGLE_OFFSET.X;
rectangle_origin.Y += CDD.RECTANGLE_OFFSET.Y;
render_card_rectangle ( rectangle_origin );
color_index =
PDF_card_colors.IndexOf ( card.Card_XColor );
group_face =
( color_index + 1 ).ToString ( ) +
card.Face_Number.ToString ( ).PadLeft ( 3, '0' );
render_cells_and_values ( rectangle_origin,
card,
group_face );
footer_origin.X += CDD.RECTANGLE_OFFSET.X;
footer_origin.Y += CDD.RECTANGLE_OFFSET.Y +
( CDD.CELLS_PER_COLUMN *
CDD.CELL_HEIGHT );
render_footer ( footer_origin, group_face );
}
It is within render_a_card
that the sequence, mentioned earlier, occurs.
render_colored_background fills the 4 x 4-1/2 inch rectangle that is the Bingo card background with the specified color. | | render_header draws the Bingo card header ("BINGO "). |
| | |
render_card_rectangle draws the white rectangle that will contain all of the Bingo card squares. | | render_cells_and_values draws the Bingo card squares, fills the center square with the free space annotation, displays the star, if any, for this Bingo card, and fills the squares with the Bingo card numbers. |
| | |
Finally, render_footer places the group_face and series_number at the bottom of the card. |
|
As with the .NET Graphics library, items that are drawn later overwrite items that were drawn earlier.
3. Executing Print_Cards
There are a number of values needed to print Bingo cards: the filename of the Bingo card file, generated by Generate_Cards
, that is the source of Bingo card data, card printing specifications, and the name of the PDF file that is to contain the Bingo card images.
First, Print_Cards
queries for the location where the Bingo card file is to be found.
To avoid permission issues, the default directory for Print_Cards
is C:\Users\Public\Bingo. The file may be located on a network drive.
Once an input Bingo card file is agreed, the statistics for the specified input Bingo card file and the Card Printing portion of the user interface are displayed.
The value in "Games per Session" reflects the number_of_colors
chosen. At least one color must be chosen. As colors are chosen (i.e., as the colored square is clicked), the border around the color square turns green, games_per_session
increments, and the color is added to the PDF_desired_colors
list. If a chosen color square is clicked again, the green border is removed, games_per_session
decrements, and the color is removed from the PDF_desired_colors
list.
The "Starting Face Number" defaults to 1. It may take on any value such that:
( total_cards + starting_face_number ) <= number_cards
where
total_cards = bingo_cards_per_game *
games_per_session *
sessions_per_day *
number_of_players;
The "Series Number" is a user supplied value that will be printed on each Bingo card generated. Its default value is the date on which Print_Cards
is executed. Checking "Display Stars" will cause Print_Cards
to generate a random star on each Bingo card.
When all of the card printing specifications have been collected and validated, the Go button is clicked and Print_Cards
generates the contents of the PDF file. The actual generation of the values that go into a card is very fast. However, Print_Cards
invokes the PDFSharp library routine PDF_document.Save
that appears to take an inordinately long time. There is no event in PDF_document.Save
to which we can subscribe to drive a progress bar. So there may be a delay between clicking Go and seeing the following window.
When the PDF file contents have been generated, Print_Cards
queries for the name of the PDF file into which to save the generated Bingo cards.
I'd recommend that the name include the series number.
If "Display PDF" was checked, Print_Cards
will display the PDF file using the default PDF reader. The 337th sheet of 630 sheets, generated by the preceding steps, is:
The display of the PDF file is independent of the execution of Print_Cards
so the Print_Cards
Exit button may be clicked once the PDF file is displayed..
4. Conclusion
Print_Cards
produces a PDF file containing a specified number of unique Bingo cards. The next step is to print these cards and use them in playing Bingo. This is the subject of the third and last article in this series.
5. References
6. Development Environment
The software presented in this article was developed in the following environment:
Microsoft Windows 7 Professional Service Pack 1 |
Microsoft Visual Studio 2008 Professional |
Microsoft .NET Framework Version 3.5 SP1 |
Microsoft Visual C# 2008 |
7. History
Bingo - Part 2 of 3 - Print Cards | 02/20/2023 | Original Article |