前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Golang Project: Tic Tac Toe

Golang Project: Tic Tac Toe

作者头像
Miigon
发布于 2022-10-27 07:53:56
发布于 2022-10-27 07:53:56
78600
代码可运行
举报
文章被收录于专栏:Miigon's BlogMiigon's Blog
运行总次数:0
代码可运行

In this article, I’ll go through my process of writing a simple Tic-Tac-Toe game in Golang. Error handling, closures, iota as well as other golang features are used to create the game.

Before we start: After some more research to the language, It seems that Go is really not that different from order C-family programming languages. So I decided that it would not be worthwhile to document every single detail of Golang in my blog posts, since there’s a lot of resources readily available on the internet which go much deeper into the Go language features than my blog will ever be able to do. So it doesn’t make much sense for me to write about everything, a quick Google search will usually work better. As a result, this article series will now be focused on my experiences of making experimental and/or actual project with Golang. The goal is to show Golang’s features and neuances.

Full code on Github

Define the board and game state

The first thing we want to do is define the board on which the game will be played on.

The game Tic-Tac-Toe has a 3*3 board. Each of the squares can be one of three states: it can be a X or a O, or it can be just empty. We define these states with type alias and const definition, which is Go’s equivalance of enum:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// what kind of piece is on a certain square
type squareState int

const (
	none   = iota
	cross  = iota
	circle = iota
)

iota represents successive integer constants 0,1,2,…. It resets to 0 whenever the keyword const appears in the source code and increments after each const specification. In our case, none would be 0, cross would be 1 and circle would be 2.

Noted that all the iota(except for the first one) can be omitted and still have the same effect.

With suqareState we can represent the state of one square, now we can use a 3*3 array to represent the whole board.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
var board [3][3]squareState

For each turn, we also need to know whose turn it is. So we introduce another variable to represent that:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type player int

var turnPlayer player

We don’t have to define constants again, the constants defined for the squareState before can be used.

This is when we run into the discussion about whether Golang’s decision to not include C/C++ style enum keyword had made the language difficult to use in some cases. Some have argued that the lack of compile-time type checking makes the code more prone to mistakes. const names being in the same package scope also can cause some confusion. Since it’s not always immediately obvious which enum a const name belongs to when you see one.

The last thing for us to do is wrap them (the board & whose turn is it) up as the current game state struct:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// current state of the game
type gameState struct {
	board [3][3]squareState
	turnPlayer player
}

Storing them as saparate global variables will work as well, but using struct is usually a better practice. It makes the code easier to understand. And also enables us to do some other cool things. (eg. an “undo” feature, which we will talk about later)

Draw the board

Next, we need a way to show our board on the screen. We use fmt to draw the board to the terminal.

The code is pretty standard:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// define a method for struct type `gameState`
func (state *gameState) drawBoard() {
	for i, row := range state.board {
		for j, square := range row {
			fmt.Print(" ")
			switch square {
			case none:
				fmt.Print(" ")
			case cross:
				fmt.Print("X")
			case circle:
				fmt.Print("O")
			}
			if j != len(row)-1 {
				fmt.Print(" |")
			}
		}
		if i != len(state.board)-1 {
			fmt.Print("\n------------")
		}
		fmt.Print("\n")
	}
}

(Notice how break is not required in switch cases in Go.)

Now we test our drawBoard function with a main function like this:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
	state := gameState{}

	state.board[0][1] = cross
	state.board[1][1] = circle

	state.drawBoard()
}

which yields:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
$ go run main.go 
   | X |  
------------
   | O |  
------------
   |   |  

Hooray! It works.

Game logic

Now it’s time for the game logic.

The rule of tic-tac-toe is really simple. The whole game can be discribed as:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for {
	draw_the_board()

	row, column := enter_position_to_place_mark()
	place_mark(row, column)

	if any_one_has_won_the_game() == true {
		break
	}

	next_turn()
}

Placing mark

So far we can already draw the board, but we don’t really have a proper way to place a mark on a square. When we are testing our drawBoard function, we modified the board field of gameState directly, but that’s not good enough for our game logic. We need it to be able to do a little bit more, eg. checking if a square has already had a mark on it.

Therefore we write a function to do just that:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// define error types
type markAlreadyExistError struct {
	row    int
	column int
}

type positionOutOfBoundError struct {
	row    int
	column int
}

// implement Error()
func (e *markAlreadyExistError) Error() string {
	return fmt.Sprintf("position (%d,%d) already has a mark on it.", e.row, e.column)
}

func (e *positionOutOfBoundError) Error() string {
	return fmt.Sprintf("position (%d,%d) is out of bound.", e.row, e.column)
}

// place a mark at a certain position
func (state *gameState) placeMark(row int, column int) error {
	if row < 0 || column < 0 || row >= len(state.board) || column >= len(state.board[row]) {
		return &positionOutOfBoundError{row, column}
	}
	if state.board[row][column] != none {
		return &markAlreadyExistError{row, column}
	}

	state.board[row][column] = squareState(state.turnPlayer) // the actual "placing"
	return nil // no error
}

You can see that aside from error handling, the code above really does nothing more than changing state.board[row][column]. However, in real projects, as the project grow more and more complex, instead of directly setting the value everywhere, using a function to do it allows for more flexibility and would pay off in the long run.

The code above defined two new error types markAlreadyExistError and positionOutOfBoundError. For them to be considered an error type, their respective Error() function must be implemented. That’s called composition (compared to inheritance).

Switching turn

Now we can place a mark on a square, what do we need next?

Well, the game has 2 players and they take turns to place marks. So after a mark is placed, we would like to switch whose mark will be placed on the next placeMark()

We do that by adding a nextTurn() function, which sets state.turnPlayer to the other player.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type gameResult int

const (
	noWinnerYet = iota
	crossWon
	circleWon
	draw
)

func (state *gameState) whosNext() player {
	return state.turnPlayer
}

func (state *gameState) nextTurn() {
	if state.turnPlayer == cross {
		state.turnPlayer = circle
	} else {
		state.turnPlayer = cross
	}
}

whosNext() and nextTurn() are pretty straightforward and does exactly what it says to do.

Checking for winner

So for each and every single turn, we need to check if anyone has won the game by placing a mark. This is when we get into a more interesting part of this project.

The naive approach

You might be tempted to do something like this:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (state *gameState) checkForWinner() gameResult {
	// check vertical
	if state.board[0][0] == state.board[0][1] &&
		state.board[0][1] == state.board[0][2] &&
		state.board[0][2] != none {
		return gameResult(state.board[0][0])
	}
	if state.board[1][0] == state.board[1][1] &&
		state.board[1][1] == state.board[1][2] &&
		state.board[1][2] != none {
		return gameResult(state.board[1][0])
	}
	if state.board[2][0] == state.board[2][1] &&
		state.board[2][1] == state.board[2][2] &&
		state.board[2][2] != none {
		return gameResult(state.board[2][0])
	}

	// check horizontal
	if state.board[0][0] == state.board[1][0] &&
		state.board[1][0] == state.board[2][0] &&
		state.board[2][0] != none {
		return gameResult(state.board[0][0])
	}
	if state.board[0][1] == state.board[1][1] &&
		state.board[1][1] == state.board[2][1] &&
		state.board[2][1] != none {
		return gameResult(state.board[0][1])
	}
	if state.board[0][2] == state.board[1][2] &&
		state.board[1][2] == state.board[2][2] &&
		state.board[2][2] != none {
		return gameResult(state.board[0][2])
	}

	// check diagonal
	// ...
	
	return noWinnerYet
}

This DOES work, but it’s a naive way of doing it. It’s long, verbose, and hard to modify. The board can also only be 3x3 and can not be expanded easily.

Using for-loops

A way better solution would be to use loops:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (state *gameState) checkForWinner() gameResult {
CheckHorizontal:
	for _, row := range state.board {
		var lastSquare squareState = row[0]
		for _, square := range row {
			if square != lastSquare {
				continue CheckHorizontal // continue with label, affects the outer loop instead of the inner one
			}
			lastSquare = square
		}
		if lastSquare == cross {
			return crossWon
		} else if lastSquare == circle {
			return circleWon
		}
	}

	// check for verticals and diagonals...

	return noWinnerYet
}

// (the complete code will be about 4 times as long as the code shown)

By doing it like this, we can handle board of any dimension. But due to the way the board is stored(board[row][column]), checking vertical lines using nested for-loops isn’t as intuitive as checking horizontal lines. And it gets even trickier when it comes to checking for diagonal lines.

Also, the function is still repetitive since the actual “checking” part inside each loop:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
		if lastSquare == cross {
			return crossWon
		} else if lastSquare == circle {
			return circleWon
		}

are the same.

A better approach

In order to know if anyone has won the game, we have to check for 3 horizontal lines, 3 vertical lines and 2 diagonal lines. We can think of the process of checking each line like this:

  1. for each iteration, check if next square(x + a, y + b) has the same mark as the current square(x, y)
  2. set current square position to (x + a, y + b)
  3. repeat the process until different marks between iteration was found or a border was hit.

This is a generalized description of all the for-loops we discussed before. By using different delta (a, b), we can control how we move between different iterations. So the same code can be used to check for horizontals, verticals and diagonals.

This is the implementation:

First we define a lambda function for checking one line (can be either horizontal, vertical or diagonal):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
checkLine := func(startRow int, startColumn int, deltaRow int, deltaColumn int) gameResult {
	var lastSquare squareState = state.board[startRow][startColumn]
	row, column := startRow+deltaRow, startColumn+deltaColumn

	// loop starts from the second square(startRow + deltaRow, startColumn + deltaColumn)
	for row >= 0 && column >= 0 && row < boardSize && column < boardSize {

		// there can't be a winner if a empty square is present within the line
		if state.board[row][column] == none {
			return noWinnerYet
		}

		if lastSquare != state.board[row][column] {
			return noWinnerYet
		}

		lastSquare = state.board[row][column]
		row, column = row+deltaRow, column+deltaColumn
	}

	// someone has won the game
	if lastSquare == cross {
		return crossWon
	} else if lastSquare == circle {
		return circleWon
	}

	return noWinnerYet
}

Then we put it inside our checkForWinner() function, alongside some other code to utilize it.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (state *gameState) checkForWinner() gameResult {
	boardSize := len(state.board) // assuming the board is always square-shaped.

	// define a lambda function for checking one single line
	checkLine := func(startRow int, startColumn int, deltaRow int, deltaColumn int) gameResult {
		// ...
	}

	// check horizontal rows
	for row := 0; row < boardSize; row++ {
		if result := checkLine(row, 0, 0, 1); result != noWinnerYet {
			return result
		}
	}
	// check vertical columns
	for column := 0; column < boardSize; column++ {
		if result := checkLine(column, 0, 0, 1); result != noWinnerYet {
			return result
		}
	}
	// check top-left to bottom-right diagonal
	if result := checkLine(0, 0, 1, 1); result != noWinnerYet {
		return result
	}
	// check top-right to bottom-left diagonal
	if result := checkLine(0, boardSize-1, 1, -1); result != noWinnerYet {
		return result
	}
	// check for draw
	for _, row := range state.board {
		for _, square := range row {
			if square == none {
				return noWinnerYet
			}
		}
	}
	// if no one wins yet, but none of the squares are empty
	return draw
}

The code above uses the same checkLine() routine to check for horizontals, verticals and diagonals.

Putting everything together

Now that we can draw the board, place a mark, switch turns and check for potiential winners, it’s time to put everything together.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (e player) String() string {
	switch e {
	case none:
		return "none"
	case cross:
		return "cross"
	case circle:
		return "circle"
	default:
		return fmt.Sprintf("%d", int(e))
	}
}

func main() {
	state := gameState{}
	state.turnPlayer = cross // cross goes first

	var result gameResult = noWinnerYet

	// the main game loop
	for {
		fmt.Printf("\nnext player to place a mark is: %v\n", state.whosNext())

		// 1. draw the board onto the screen
		state.drawBoard()

		fmt.Printf("where to place a %v? (input row then column, separated by space)\n> ", state.whosNext())

		// 2. use a loop to take input
		for {
			var row, column int
			fmt.Scan(&row, &column)

			e := state.placeMark(row-1, column-1) // -1 so coordinate starts at (1,1) instead of (0,0)

			// if a valid position was entered, break out from the input loop
			if e == nil {
				break
			}

			// if an invalid position was entered, prompt the player to re-enter another position
			fmt.Println(e)
			fmt.Printf("please re-enter a position:\n> ")
		}

		// 3. check if anyone has won the game
		result = state.checkForWinner()
		if result != noWinnerYet {
			break
		}

		// 4. if no one has won in this turn, go on for next turn and continue the game loop
		state.nextTurn()

		fmt.Println()
	}

	state.drawBoard()

	switch result {
	case crossWon:
		fmt.Printf("cross won the game!\n")
	case circleWon:
		fmt.Printf("circle won the game!\n")
	case draw:
		fmt.Printf("the game has ended with a draw!\n")
	}
}

Noted that we defined method String() for player type at the beginning.

This is done for the following line of code to work.

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
fmt.Printf("\nnext player to place a mark is: %v\n", state.whosNext())

Defining this method makes type player a Stringer, meaning something that has a String() method and can be converted into a string.

In this case, fmt.Printf uses the method internally to convert a player into a string, then prints it out.

Testing the game

Now we compile and run the game:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
next player to place a mark is: cross
   |   |  
------------
   |   |  
------------
   |   |  
where to place a cross? (input row then column, separated by space)
> 1 1

next player to place a mark is: circle
 X |   |  
------------
   |   |  
------------
   |   |  
where to place a circle? (input row then column, separated by space)
> 1 2

...

next player to place a mark is: cross
 X | O | X
------------
 O | O | X
------------
   | X | O
where to place a cross? (input row then column, separated by space)
> 3 1
 X | O | X
------------
 O | O | X
------------
 X | X | O
the game has ended with a draw!

From another run:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
...

next player to place a mark is: circle
 X | X | O
------------
 X | O |  
------------
   |   |  
where to place a circle? (input row then column, separated by space)
> 3 1

 X | X | O
------------
 X | O |  
------------
 O |   |  
circle won the game!

There you have it, a fully working tic-tac-toe in Go.

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Python代写:CSC108H Tic-Tac-Toe
Tic-tac-toe is a two-player game that children often play to pass the time. The game is usually played using a 3-by-3 game board. Each player chooses a symbol to play with (usually an X or an O) and the goal is to be the first player to place 3 of their symbols in a straight line on the game board (either across a row on the board, down a column or along one of the two main diagonals).
拓端
2022/10/24
8230
Python代码编写:CSC108H Tic-Tac-Toe
Tic-tac-toe is a two-player game that children often play to pass the time. The game is usually played using a 3-by-3 game board. Each player chooses a symbol to play with (usually an X or an O) and the goal is to be the first player to place 3 of their symbols in a straight line on the game board (either across a row on the board, down a column or along one of the two main diagonals).
拓端
2022/10/25
6900
794. Valid Tic-Tac-Toe State
思路: 1. 由规则可知,”X”一定最先开始,所以当前局面存在”O”的个数大于”X”的个数为非法。 2. 其次,由于”X”和”O”轮流,因此,当前局面中”X”的个数要么和”O”相等,要么比”O”多一。 3. “O”在当前局面赢得比赛的情况下,上一轮的”X”一定不能赢得局面。 4. “O”在当前局面赢得比赛的情况下,上一轮的”X”没有赢得局面时,合法局面必须满足”O”的个数等于”X”的个数。 5. “X”在当前局面赢得比赛的情况下,意味着上一轮”O”没有赢得局面,合法局面下,”X”的个数正好比”O”的个数多一。
用户1147447
2019/05/26
8210
Codeforces Beta Round #3 C. Tic-tac-toe
Certainly, everyone is familiar with tic-tac-toe game. The rules are very simple indeed. Two players take turns marking the cells in a 3 × 3grid (one player always draws crosses, the other — noughts). The player who succeeds first in placing three of his marks in a horizontal, vertical or diagonal line wins, and the game is finished. The player who draws crosses goes first. If the grid is filled, but neither Xs, nor 0s form the required line, a draw is announced.
glm233
2020/09/28
5440
使用 Python 和 Pygame 制作游戏:第九章到第十章
推星星是 Sokoban 或“箱子推动者”的克隆。玩家位于一个房间,里面有几颗星星。房间中的一些瓷砖精灵上有星星标记。玩家必须想办法将星星推到有星星标记的瓷砖上。如果墙壁或其他星星在其后面,玩家就不能推动星星。玩家不能拉星星,所以如果星星被推到角落,玩家将不得不重新开始级别。当所有星星都被推到星星标记的地板瓷砖上时,级别完成,下一个级别开始。
ApacheCN_飞龙
2024/01/15
7250
使用 Python 和 Pygame 制作游戏:第九章到第十章
使用Python编程打造一款游戏
前几天在Python最强王者交流群有个叫【Chloe】的粉丝问了一个Python小游戏的问题,这里拿出来给大家分享下,一起学习下。
前端皮皮
2022/08/17
3630
使用Python编程打造一款游戏
python tictactoe游戏
import random, sys, time from tkinter import * from tkinter.messagebox import showinfo, askyesno from guimaker import GuiMakerWindowMenu
用户5760343
2022/05/13
1.5K0
python tictactoe游戏
js模块化例子
最近在看一本书,里面提到js的模块化,觉得很有必要,所以记录下来 Game.js
黒之染
2018/10/19
4.7K0
K哥教你用Python摸鱼
玩法:这让我想起了魂斗罗那第几关的boss,有点类似,不过魂斗罗那个难度肯定高点。
Python进击者
2022/03/14
8990
Python 小型项目大全 76~81
井字棋是一种在3 × 3网格上玩的经典纸笔游戏。玩家轮流放置 X 或 O 标记,试图连续获得三个。大多数井字棋都以平局告终,但如果你的对手不小心,你也有可能智胜他们。
ApacheCN_飞龙
2023/04/12
1.2K0
Python 小型项目大全 76~81
从零开始再造打爆李世石的AlphaGo:创造能下围棋的机器人
我们在上节完成了围棋规则和棋盘状态监测功能,本节我们在基于上节的基础上,设计一个能自己下棋的围棋机器人。首先我们设计一个类叫Agent,它的初始化代码如下:
望月从良
2019/04/09
7090
从零开始再造打爆李世石的AlphaGo:创造能下围棋的机器人
Python 小型项目大全 26~30
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987 . . .
ApacheCN_飞龙
2023/04/12
5180
Python 小型项目大全 26~30
从零开始再造打爆李世石的AlphaGo:快速构建棋盘和围棋规则
从本节开始,我们废话少说,迅速进入代码编写阶段。对技术而言“做”永远是比“讲”更好的说,很多用语言讲不清楚的道理,看一下代码自然就明白了。我们要实现的围棋机器人必须做到以下几点:
望月从良
2019/03/18
6660
从零开始再造打爆李世石的AlphaGo:快速构建棋盘和围棋规则
《effective Go》读后记录:GO基础
一个在线的Go编译器 如果还没来得及安装Go环境,想体验一下Go语言,可以在Go在线编译器 上运行Go程序。 格式化 让所有人都遵循一样的编码风格是一种理想,现在Go语言通过gofmt程序,让机器来处理大部分的格式化问题。gofmt程序是go标准库提供的一段程序,可以尝试运行它,它会按照标准风格缩进,对齐,保留注释,它默认使用制表符进行缩进。Go标准库的所有代码都经过gofmt程序格式化的。 注释 Go注释支持C风格的块注释/* */和C++风格的行注释//。块注释主要用作包的注释。Go官方提倡每个包都应包
Tencent JCoder
2018/07/02
7230
CYBER-DOJO.ORG上的编程操练题目100 doorsAnagramsBalanced ParenthesesBowling GameCalc StatsCombined NumberCoun
Cyber-dojo.org是编程操练者的乐园。下面是这个网站上的43个编程操练题目,供编程操练爱好者参考。
程序员吾真本
2018/08/20
1.2K0
Python 小型项目大全 36~40
沙漏程序实现了一个基本的物理引擎。一个物理引擎是模拟物理物体在重力作用下下落,相互碰撞,按照物理定律运动的软件。你会发现在视频游戏、计算机动画和科学模拟中使用的物理引擎。在第 91 到 102 行,每一粒沙子检查它下面的空间是否是空的,如果是,就向下移动。否则,它检查它是否可以向左下方移动(第 104 到 112 行)或向右下方移动(第 114 到 122 行)。当然,运动学,经典物理学的一个分支,处理宏观物体的运动,远不止这些。然而,你不需要一个物理学学位来制作一个沙漏中沙子的原始模拟,它看起来是令人愉快的。
ApacheCN_飞龙
2023/04/12
6720
Python 小型项目大全 36~40
Python 进阶指南(编程轻松进阶):十四、实践项目
到目前为止,这本书已经教会了你编写可读的 Python 风格代码的技巧。让我们通过查看两个命令行游戏的源代码来实践这些技术:汉诺塔和四人一排。
ApacheCN_飞龙
2023/04/09
8540
Python 进阶指南(编程轻松进阶):十四、实践项目
Quant求职系列:Jane Street烧脑Puzzle(2019-2020)
全球顶尖的自营交易公司Jane Street创立于1999年,其对自己的描述是:“a quantitative trading firm and liquidity provider with a unique focus on technology and collaborative problem solving.”
量化投资与机器学习微信公众号
2021/01/22
1.1K0
python 游戏(记忆拼图Memory
实现功能:翻开两个一样的牌子就显示,全部翻开游戏结束,设置5种图形,7种颜色,游戏开始提示随机8个牌子
py3study
2020/01/16
1.6K0
python 游戏(记忆拼图Memory
使用最大-最小树搜索算法和alpha-beta剪枝算法设计有效围棋走法
我们的世界纷繁复杂,看起来完全不可捉摸。但在很多场景下,它运行的本质其实是通过付出最小的代价获得最大化收益。例如在自然界里的自然选择,光的运行路径。对于人的世界更是如此,由于我们做任何事情,任何选择都要付出相应的成本,因此选择一种决策方式让我们以最小的代价获得最大化的回报无疑是我们行动思考的核心。
望月从良
2019/04/28
2.5K0
使用最大-最小树搜索算法和alpha-beta剪枝算法设计有效围棋走法
相关推荐
Python代写:CSC108H Tic-Tac-Toe
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文