-- Connect4.exw ------------------------------------------------------------
-- A Euphoria version of the well known Connect-4 game:
-- Game Objective: The first player (you or computer) to get 4 of their tiles
-- in a straight line (diagonal,vertical or horizontal) wins!
-- -------------------------------------------------------------------------
-- (c) Fred Mangan 2010
-- Some stratagies:
-- On each player's play, the computer must check if there is a winner, and
-- if so, must report who won and end the game.
-- Scanning routine(s) will help with this and also will help in finding a
-- play move for the computer.
-- An outline of strategy for the computer's moves would be:
-- 1. Is there a winning move for computer? If so make it.
-- 2. Will human win on his/her next move? If so, block it, if can.
-- 3. Is there a move that will advance the computer's most developed
-- line of tiles? If so, make it.
-- 4. If MoveCounter > MAX_MOVES, game over, no winner.
-- 5. Make a random move.
-- This strategy can be refined later.
--Skeleton's code generated by "The Window Designer for EuWinGUI Applications"
--(C)2001-2006 by Andrea Cini.
--***************************************************************************
include EuWinGUI.ew
include eucrash.ew -- to handle credits
--***************************************************************************
--Handles of the controls
atom ConnectGroup,PromptText,CreditsButton,CloseButton,LogoDialog,
LogoPicture
--***************************************************************************
atom FontA
--***************************************************************************
-- My stuff:
-- The grid on WinHwnd is represented by 12 rows of PictureButtons, each
-- row containing 12 buttons. The handles of these buttons are stored
-- in sequence Tiles. Game activity is simulated by laying suitable small
-- BMP pictures on the PictureButtons.
sequence Tiles -- a sequence to hold all the PictureButtons
-- Game activity is shadowed by data in sequence PlayTable, which is used
-- as the basis for assessing the state of play.
sequence PlayTable -- working table for players' moves
constant WINSIZE = {450,387}, -- size for WinHwnd
CONPOSN = {10,5}, -- position, TLH cormer of grid
CONSIZE = {420,325}, -- size for the grid
-- BMP for introductory picture
LOGO_PIC = "Resources\\Logo.bmp",
-- BMPs for tiles
TILE_PICS = {"Resources\\MtPic.bmp",
"Resources\\PlayerPic.bmp",
"Resources\\ComputerPic.bmp"},
-- sound files
BOING = "Resources\\Boing.wav",
CHIRP = "Resources\\Chirps.wav",
PROMPT_TXT = " Click on a column to place your tile. ",
INTRO_MSG = "The first player (you or computer) to get 4 of \n" &
" their tiles in a straight line (diagonal,vertical\n" &
" or horizontal) wins!\n\n" &
"Will you play first?",
NULL_ID = 1, -- Entity IDs, also indices to TILE_PICS
HUMAN_ID = 2,
COMPUTER_ID = 3,
NROWS = 12,
NCOLS = 12,
MAX_MOVES = NROWS * NCOLS, -- i.e.,when array is full
TILE_CLR_DATA = 1, -- more indices
TILE_POS_DATA = 2
integer GameIsOn GameIsOn = False -- flag
integer MoveCounter -- counts moves in a game
integer ActivePlayer -- flags which player's turn is next
procedure TimeDelayLoop()
-- Delays after a SetWinTimer() command,
-- when only a Time Event is needed.
-- (most accurate if use WaitEvent here)
while True do -- endless loop
WaitEvent() -- wait for a windows' event
if Event = Time then
-- time is up; jump out of loop
exit
end if
end while
end procedure -- TimeDelayLoop
procedure TerminateProgram()
-- Tidy up and close program
EndCredits2() -- removes a memory bitmap
CloseApp(0)
end procedure -- TerminateProgram
procedure InitializeButtonPictures()
-- Places an "empty" picture on each of the PictureButtons
for i = 1 to length(Tiles) do
for j = 1 to length(Tiles[i]) do
SetPicture(Tiles[i][j], PictureButton, TILE_PICS[NULL_ID])
SetDim(Tiles[i][j],32,25)
end for
end for
end procedure -- InitializeButtonPictures
procedure InitializePlayTable()
-- PlayTable is a square table, reflecting the Tiles, and used to keep
-- track of what tiles players have played. It is initiallized to all-zeroes.
PlayTable = repeat(repeat(0,NCOLS),NROWS)
end procedure -- InitializePlayTable
procedure CreatePictureButtons()
-- Creates the PictureButtons used to represent the tiles.
-- It would be too tedious to create these in Designer, also
-- doing it this way gives Tiles sequence a row, column structure.
-- The buttons' handles are saved in array Tiles in row-column form.
sequence tile_size,tile_offsets,temp_row
atom butn
tile_size = CONSIZE / 13 -- adjusted by trial-and-error
tile_offsets = CONPOSN + 12
Tiles = {} -- initial to empty list
for row = 1 to NROWS do
temp_row = {} -- accumulates a row of buttons
-- build a row of buttons
for col = 1 to NCOLS do
-- create a button's handle
butn = Control(PictureButton,
"",
tile_offsets[1] + floor(tile_size[1]*(col-1)),
tile_offsets[2],
tile_size[1],tile_size[2])
temp_row = append(temp_row,butn) -- add button handle to sequence
end for
Tiles = append(Tiles,temp_row) -- add the row to Tiles
tile_offsets[2] += tile_size[2] -- advance down to next row
end for
end procedure -- CreatePictureButtons
procedure InitializeNewGame()
-- Sets up to play a new game
MoveCounter = 0
InitializeButtonPictures()
InitializePlayTable()
if AskMsg(INTRO_MSG,"First Player") then
-- human kicks off
ActivePlayer = HUMAN_ID
SetText(PromptText,PROMPT_TXT)
else
-- computer kicks off
ActivePlayer = COMPUTER_ID
SetText(PromptText,"")
end if
-- flag game is in progress
GameIsOn = True
end procedure -- InitializeNewGame
function GenScanPlayTable()
-- Procedure scans the PlayTable and returns a sequence, Scan, containing 4
-- sub-sequences. The sub-sequences are:
-- Scan[1][1]: values in diagonals sloping down to left in Playtable
-- Scan[1][2]: corresponding row-col coords for values in Scan[1][1]
-- Scan[2][1]: values in diagonals sloping down to right in PlayTable
-- Scan[2][2]: corresponding row-col coords for values in Scan[2][1]
-- Scan[3][1]: values in horizontals in PlayTable
-- Scan[3][2]: corresponding row-col coords for values in Scan[3][1]
-- Scan[4][1]: values in verticals in PlayTable
-- Scan[4][2]: corresponding row-col coords for values in Scan[4][1]
-- This routine has scope for optimization and generalization.
sequence Scan, top_scan, side_scan, temp, temp2, lim
integer n
Scan = repeat({{},{}}, 4) -- skeleton sequence for Scan
-- 1. this is for diagonals downwards to left
-- a. track across top
for col = 1 to NCOLS do
top_scan = {1,col}
lim = {col,1}
temp = {}
temp2 = {}
while top_scan[1] <= lim[1] and top_scan[2] >= lim[2] do
n = PlayTable[top_scan[1]][top_scan[2]]
temp = append(temp, n)
temp2 = append(temp2, {top_scan[1],top_scan[2]})
top_scan += {1,-1}
end while
Scan[1][TILE_CLR_DATA] = append(Scan[1][TILE_CLR_DATA], temp)
Scan[1][TILE_POS_DATA] = append(Scan[1][TILE_POS_DATA], temp2)
end for
-- b. track down RHS
for row = 2 to NROWS do
side_scan = {row,NCOLS}
lim = {NCOLS,row}
temp = {}
temp2 = {}
while side_scan[1] <= lim[1] and side_scan[2] >= lim[2] do
n = PlayTable[side_scan[1]][side_scan[2]]
temp = append(temp, n)
temp2 = append(temp2, {side_scan[1],side_scan[2]})
side_scan += {1,-1}
end while
Scan[1][TILE_CLR_DATA] = append(Scan[1][TILE_CLR_DATA], temp)
Scan[1][TILE_POS_DATA] = append(Scan[1][TILE_POS_DATA], temp2)
end for
-- 2. this is for diagonals downwards to right
-- a. track across top
for col = 1 to NCOLS do
top_scan = {1,col}
lim = {NROWS,NCOLS}
temp = {}
temp2 = {}
while top_scan[1] <= lim[1] and top_scan[2] <= lim[2] do
n = PlayTable[top_scan[1]][top_scan[2]]
temp = append(temp, n)
temp2 = append(temp2, {top_scan[1],top_scan[2]})
top_scan += {1,1}
end while
Scan[2][TILE_CLR_DATA] = append(Scan[2][TILE_CLR_DATA], temp)
Scan[2][TILE_POS_DATA] = append(Scan[2][TILE_POS_DATA], temp2)
end for
-- b. track down LHS
for row = 2 to NROWS do
side_scan = {row,1}
lim = {NROWS,NCOLS}
temp = {}
temp2 = {}
while side_scan[1] <= lim[1] and side_scan[2] <= lim[2] do
n = PlayTable[side_scan[1]][side_scan[2]]
temp = append(temp, n)
temp2 = append(temp2, {side_scan[1],side_scan[2]})
side_scan += {1,1}
end while
Scan[2][TILE_CLR_DATA] = append(Scan[2][TILE_CLR_DATA], temp)
Scan[2][TILE_POS_DATA] = append(Scan[2][TILE_POS_DATA], temp2)
end for
-- 3. horizontal scan
Scan[3][TILE_CLR_DATA] = PlayTable -- that's easy
for row = 1 to NROWS do
temp2 = {}
for col = 1 to NCOLS do
temp2 = append(temp2, {row,col})
end for
Scan[3][TILE_POS_DATA] = append(Scan[3][TILE_POS_DATA],temp2)
end for
-- 4. vertical scan
for col = 1 to NCOLS do
temp = {}
temp2 = {}
for row = 1 to NROWS do
temp = append(temp, PlayTable[row][col])
temp2 = append(temp2, {row,col})
end for
Scan[4][TILE_CLR_DATA] = append(Scan[4][TILE_CLR_DATA],temp)
Scan[4][TILE_POS_DATA] = append(Scan[4][TILE_POS_DATA],temp2)
end for
return Scan
end function -- GenScanPlayTable
function ScanForRunCtl(integer Protag, sequence Scan, integer run)
-- Searchs sequence Scan for a run of 'run' number of tiles, belonging
-- to Protag(onist).
-- Entry: Protag = ID of player for whom runs sought
-- Scan = freshly generated scan sequence
-- run = runs considered will have at least run number of tiles
-- Returns: info about the run owners's ID number and the coords {row,col}
-- of the begining and end of the run, if a run found....
sequence searcher, coord, runs_info
integer run_entity,p
searcher = repeat(Protag, run) -- what we are looking for
-- do the search
run_entity = False -- assume no winner
runs_info = {}
coord = {0,0}
for i = 1 to length(Scan) do
for j = 1 to length(Scan[i][1]) do
p = match(searcher,Scan[i][1][j])
if p then
run_entity = Protag
coord[1] = Scan[i][2][j][p]
coord[2] = Scan[i][2][j][p+run-1]
runs_info = append(runs_info, {run_entity, coord})
end if
end for
end for
return runs_info
end function -- ScanForRunCtl
function SearchForRuns(integer run, sequence protagonists)
-- Search for run_entity having a run of at least length 'run'
-- Returns the first such found, else False if none found.
sequence scan_seq, run_info
integer run_entity
-- generate scanning version of PlayTable
scan_seq = GenScanPlayTable()
for i = 1 to length(protagonists) do
-- do the scan for each protagonist
run_entity = False -- none identified
run_info = ScanForRunCtl(protagonists[i], scan_seq, run)
if length(run_info) > 0 and run_info[1][1] then
-- a run identified; get the identity and exit
run_entity = run_info[1][1]
run_entity = protagonists[i] -- alternative, may be clearer
exit
end if
end for
return run_entity
end function -- SearchForRuns
procedure TestForGameOver()
-- A game is over if a run of 4 similar tiles
-- exists in PlayTable (diagonal,horizontal or
-- vertical), or if all tile spaces are filled.
sequence msg, winner
integer play_again, check_4
check_4 = SearchForRuns(4,{HUMAN_ID,COMPUTER_ID})
if check_4 then
-- we have a winner
GameIsOn = False -- game is over
-- identify and name winner
if check_4 = COMPUTER_ID then
winner = "COMPUTER"
else
winner = "YOU"
end if
-- announce the winner
PlaySnd(CHIRP)
msg = sprintf("Game Over - WINNER IS %s!",{winner})
InfoMsg(msg, "Game Over")
StopSnd()
elsif MoveCounter >= MAX_MOVES then
-- we have end of game with no winner
GameIsOn = False
-- announce no winner
msg = "Game is over without a winner.\nPlay again?"
end if
if not GameIsOn then
-- ask if play again
msg = "Play another game?"
play_again = AskMsg(msg,"End of Game")
if play_again then
InitializeNewGame()
else
GameIsOn = False
TerminateProgram()
end if
end if
end procedure -- TestForGameOver
procedure DropTile(integer row_count, integer Col, integer Protag)
-- Graphically display falling tile; runs down column Col and briefly
-- display Protag's tile at each position until row row_count is
-- reached.
integer row
row = 0
while row < row_count do
row += 1
-- display Protag's tile
SetPicture(Tiles[row][Col], PictureButton, TILE_PICS[Protag])
SetWinTimer(50)
-- loop until time expired
TimeDelayLoop()
-- display the NULL_ID tile
SetPicture(Tiles[row][Col], PictureButton, TILE_PICS[NULL_ID])
end while
end procedure -- DropTile
function TryToAddTile(integer Column, integer Protag)
-- Try to add Protag's tile to the game at Column and
-- update PlayTable if tile is successfully added.
integer AddSuccess, row_count
-- find where tile will come to rest
row_count = 0
AddSuccess = False -- assume column is full
for row = 1 to NROWS do
if PlayTable[row][Column] = 0 then
row_count += 1
AddSuccess = True -- at least 1 spare space in the Column
end if
end for
if AddSuccess then
-- can add the move to the PlayTable and tile the display
PlayTable[row_count][Column] = Protag
-- visualise the fall of the tile
DropTile(row_count,Column,Protag)
-- show tile in its final position
SetPicture(Tiles[row_count][Column], PictureButton, TILE_PICS[Protag])
end if
return AddSuccess
end function -- TryToAddTile
function TryExtending(sequence coord_info, integer run_len)
-- Entry: coord_info = {{r1,c1}, {r2,c2}}
-- {r1,c1} = coords of tile at start of run
-- {r2,c2} = coords of tile at end of run
-- run_len = number of tiles in the run.
integer move_success, row, col
sequence delta_row_col, tester, msg
if run_len <= 1 then
-- invalid run_len; programmer error; give help
msg = sprintf("In TryExtending(), run_len = %d.\n It must be > 1.",
run_len)
WarnMsg(msg, "Programmer error - program will close.")
TerminateProgram()
else
-- go ahead
move_success = False -- assume move will fail
for i = 1 to length(coord_info) do
-- compute where in PlayTable we have coords bracketing tile
-- run specified by coord_info
delta_row_col = (coord_info[i][2][2] - coord_info[i][2][1]) / (run_len - 1)
-- coords in tester bracket the end tiles of the run of tiles
tester = {coord_info[i][2][1]-delta_row_col,
coord_info[i][2][2]+delta_row_col}
-- for each coord in tester
for j = 1 to length(tester) do
row = tester[j][1]
col = tester[j][2]
if row <= NROWS and row > 0 and col <= NCOLS and col > 0 then
-- we're inside the Tile array bounds
if PlayTable[row][col] = 0 then
-- try to play this column
-- ? {r,c} -- can activate to monitor ... (debugging)
-- move_success will be True if extending succeeds,
-- else it will be False.
move_success = TryToAddTile(col, COMPUTER_ID)
return move_success
end if
end if
end for
end for
end if
return move_success
end function -- TryExtending
function TryToExtendRun(integer run_len, integer ProtagToExtend)
-- Attempts to extend a run of at least run_len tiles belonging to
-- ProtagToExtend (COMPUTER_ID or HUMAN_ID).
sequence scan_seq, info
integer move_success
info = {}
if SearchForRuns(run_len,{ProtagToExtend}) then
-- have possible Protagonist to extend
-- generate scan sequence
scan_seq = GenScanPlayTable()
-- gather info on the ProtagonistToExtend's runs
info = ScanForRunCtl(ProtagToExtend, scan_seq, run_len)
-- attempt extending
move_success = TryExtending(info,run_len)
else
-- failed to to make a move
move_success = False
end if
return move_success
end function -- TryToExtendRun
procedure MakeComputerMove()
-- Computes move on behalf of computer
integer move_success, Col
SetText(PromptText,"")
-- computer's "thinking" time
SetWinTimer(500)
TimeDelayLoop()
move_success = False
while True do
if TryToExtendRun(3,COMPUTER_ID) then
-- computer has won!
exit
elsif TryToExtendRun(3,HUMAN_ID) then
-- computer has blocked a human move
exit
elsif TryToExtendRun(2,COMPUTER_ID) then
-- computer has developed a run
exit
else
-- make a random move for computer
while not move_success do
-- try to add tile to randomly selected column
Col = rand(NCOLS)
-- will be True if addition succeeds
if TryToAddTile(Col, COMPUTER_ID) then
exit
end if
end while
-- make sure get out (this should never execute)
exit
end if
end while
MoveCounter += 1 -- move has been made; tally
ActivePlayer = HUMAN_ID -- toggle for next player
TestForGameOver() -- end game if there is a winner
SetText(PromptText,PROMPT_TXT)
end procedure -- MakeComputerMove
procedure ClickScanTiles()
-- Scan all Tiles for a HUMAN_ID click
sequence msg
integer AddSuccess
for Row = 1 to NROWS do
for Col = 1 to NCOLS do
if EventOwner = Tiles[Row][Col] then
-- try to add a HUMAN_ID's tile
AddSuccess = TryToAddTile(Col, HUMAN_ID)
if AddSuccess then
-- successful move
MoveCounter += 1 -- tally another move
ActivePlayer = COMPUTER_ID -- toggle for next player
TestForGameOver() -- end game if there is a winner
else
-- failed move ... announce it
PlaySnd(BOING)
end if
end if
end for
end for
end procedure -- ClickScanTiles
procedure CheckIfTerminateProgram()
-- Handles end of program run
sequence msg
if GameIsOn then
msg = "A game is in progress.\n\nDo you really want to quit?"
if AskMsg(msg,"End Program") then
TerminateProgram()
end if
else
TerminateProgram()
end if
end procedure -- CheckIfTerminateProgram
procedure DisplayLogoDialog()
SetPicture(LogoPicture,Picture,LOGO_PIC)
SetVisible(LogoDialog,True)
SetWinTimer(4000)
TimeDelayLoop()
SetVisible(LogoDialog,False)
end procedure -- DisplayLogoDialog
--***************************************************************************
--The Event Loop
procedure EventLoop()
--Run continuosly the event checking
while True do
-- if it is COMPUTER_ID's move, make it
if ActivePlayer = COMPUTER_ID then
MakeComputerMove()
end if
--Wait for an event
WaitEvent()
--The event was a left mouse click?
if Event = Click then
-- scan for click on tile display (my interpolation in code here)
if ActivePlayer = HUMAN_ID then
ClickScanTiles()
end if
--Which control was the event Owner?
if EventOwner = WinHwnd then
elsif EventOwner = CreditsButton then
ShowCredits2()
elsif EventOwner = CloseButton then
CheckIfTerminateProgram()
end if
end if
--The default "close" button of a window has been clicked?
if Event = Close then
--Which window was the event Owner?
if EventOwner = WinHwnd then
CheckIfTerminateProgram()
end if
end if
end while
end procedure
--***************************************************************************
--The WinMain procedure
procedure WinMain()
FontA = NewFont("FN_DEFAULT",12,1,0,0)
--Enable "Close" events
CloseEventEnabled = True
--Create all windows, then their controls, and finally
--set their fonts, icon, colors and the needed pictures
WindowType = NoMaxWin
ShowFlag = False
Window("Connect-4",
floor((ScreenWidth()-WINSIZE[1])/2),
floor((ScreenHeight()-WINSIZE[2])/2),
WINSIZE[1],WINSIZE[2])
ShowFlag = True
ConnectGroup = Control(Group,
"",
CONPOSN[1],CONPOSN[2],
CONSIZE[1],CONSIZE[2])
PromptText = Control(Text,
"",
10,WINSIZE[2]-60,
220,15)
CreditsButton = Control(Button,
"Credits",
WINSIZE[1]-190,WINSIZE[2]-60,
80,25)
CloseButton = Control(Button,
"End Program",
WINSIZE[1]-100,WINSIZE[2]-60,
80,25)
CreatePictureButtons()
WindowType = NoTitleWin
ShowFlag = False
LogoDialog = Control(Dialog,"Logo",floor((ScreenWidth()-450)/2),floor((ScreenHeight()-368)/2),450,368)
SetParentWindow(LogoDialog)
ShowFlag = True
LogoPicture = Control(Picture,"",0,0,450,387)
-- set up to show credits
InitialCredits2({RDS,EUWINGUI},1,2010,CL_RED)
SetFont(PromptText,FontA)
SetColor(PromptText, CL_BLACK, CL_DKYELLOW)
DisplayLogoDialog()
--Show the window(s)
SetVisible(WinHwnd,True)
-- start new game
InitializeNewGame()
--Run the event loop
EventLoop()
-- Free memory and close the application
TerminateProgram()
end procedure
--***************************************************************************
--Start the application
WinMain()
-- End program listing --------------------------------------------------------
Conversion to HTML by PC2HTM.EXE