一、前言

在游戏中,任务是非常常见的玩法,可能会有主线任务,支线任务以及其它一些类型的任务,各任务可能还会有前置任务,即需要完成某个任务之后,才能做当前任务。在游戏开发中,配置表可以使用Excel来编辑,如果是任务表,可能会是如下配置方式: TaskIDTaskTitlePreTask10任务10020任务20011任务111021任务2120

当任务比较多的时候,它们的依赖关系将变得不直观,很容易出错,出错也不容易发现。

有没比较直观的方式进行查看,排错呢?笔者想到了目前非常流程的Markdown文件,它可以简单地通过文本的方式输入然后输出强大的各种图表。这里就可以使用mermaid图来直观展现。

关于mermaid图可以去官网 https://mermaid.js.org/intro/查看用例。

下图为生成后的效果图:

注意:mermaid图在渲染时,如果不设置subgraph则可能会出现乱序问题,即不是按代码中出现的顺序渲染。

二、实现

为了方便Go读取Excel,需要使用相关的Excel库,笔者使用 excelize库。

根据前面的效果图,可以知道,这其实就是一个深度优先的树,实现方式有两种,一种是使用递归的方式来实现,这种方式实现起来简单,但是如果层次很深,那可能会出现栈溢出;另一种方式就是使用栈的方式来实现,将每一层节点先压栈,然后从栈顶取出一个节点然后再将其所有子节点入栈,再从栈顶取出一个节点处理,依此类推,直到栈中所有节点处理完毕。

下面列出使用递归方式实现的版本:

  1/*
  2MIT License
  3
  4# Copyright (c) 2023 WittonBell
  5
  6Permission is hereby granted, free of charge, to any person obtaining a copy
  7of this software and associated documentation files (the "Software"), to deal
  8in the Software without restriction, including without limitation the rights
  9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 10copies of the Software, and to permit persons to whom the Software is
 11furnished to do so, subject to the following conditions:
 12
 13The above copyright notice and this permission notice shall be included in all
 14copies or substantial portions of the Software.
 15
 16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 22SOFTWARE.
 23*/
 24package main
 25
 26import (
 27	"flag"
 28	"fmt"
 29	"os"
 30	"path/filepath"
 31	"strings"
 32
 33	"github.com/xuri/excelize/v2"
 34)
 35
 36var taskIDField string
 37var taskTitleField string
 38var preTaskField string
 39var noCaseSensitive bool // 是否不区分大小写
 40var fieldNameRow uint    // 字段名所在行号
 41var dataStartRow uint    // 数据开始行号
 42
 43type node struct {
 44	taskID    string
 45	taskTitle string
 46}
 47
 48type multiMap map[string][]*node
 49
 50func (slf multiMap) Add(key string, nd *node) {
 51	if len(slf) == 0 {
 52		slf[key] = []*node{nd}
 53	} else {
 54		slf[key] = append(slf[key], nd)
 55	}
 56}
 57
 58func (slf multiMap) Get(key string) []*node {
 59	if slf == nil {
 60		return nil
 61	}
 62	return slf[key]
 63}
 64
 65func (slf multiMap) Del(key string) {
 66	delete(slf, key)
 67}
 68
 69func searchKeyCol(rows *excelize.Rows) (TaskIDCol, PreTaskIDCol, TitleCol int) {
 70	row, err := rows.Columns()
 71	if err != nil {
 72		fmt.Println(err.Error())
 73	}
 74
 75	for i, col := range row {
 76		name := col
 77		if noCaseSensitive {
 78			name = strings.ToLower(col)
 79		}
 80		if name == preTaskField {
 81			PreTaskIDCol = i + 1
 82		} else if name == taskIDField {
 83			TaskIDCol = i + 1
 84		} else if name == taskTitleField {
 85			TitleCol = i + 1
 86		}
 87	}
 88	return
 89}
 90
 91func readExcel(filePath string) multiMap {
 92	fd, err := excelize.OpenFile(filePath)
 93	if err != nil {
 94		fmt.Printf("读取文件`%s`失败", filePath)
 95		return nil
 96	}
 97	defer func() {
 98		fd.Close()
 99	}()
100	TaskIDCol, PreTaskIDCol, TitleCol := -1, -1, -1
101	sheetName := fd.GetSheetName(0)
102	rows, err := fd.Rows(sheetName)
103	if err != nil {
104		return nil
105	}
106	defer func() {
107		rows.Close()
108	}()
109
110	m := multiMap{}
111	for i := 1; rows.Next(); i++ {
112		if i == int(fieldNameRow) {
113			TaskIDCol, PreTaskIDCol, TitleCol = searchKeyCol(rows)
114			isOk := true
115			if TaskIDCol < 0 {
116				isOk = false
117				fmt.Printf("要求字段名:%s\n", taskIDField)
118			}
119			if PreTaskIDCol < 0 {
120				isOk = false
121				fmt.Printf("要求字段名:%s\n", preTaskField)
122			}
123			if TitleCol < 0 {
124				isOk = false
125				fmt.Printf("要求字段名:%s\n", taskTitleField)
126			}
127			if !isOk {
128				return nil
129			}
130		}
131		if i < int(dataStartRow) {
132			continue
133		}
134		TaskIDCell, err := excelize.CoordinatesToCellName(TaskIDCol, i)
135		if err != nil {
136			continue
137		}
138		PreTaskIDCell, err := excelize.CoordinatesToCellName(PreTaskIDCol, i)
139		if err != nil {
140			continue
141		}
142
143		TitleColCell, err := excelize.CoordinatesToCellName(TitleCol, i)
144		if err != nil {
145			continue
146		}
147
148		TaskID, err := fd.GetCellValue(sheetName, TaskIDCell)
149		if err != nil || TaskID == "" {
150			continue
151		}
152
153		Title, err := fd.GetCellValue(sheetName, TitleColCell)
154		if err != nil || Title == "" {
155			continue
156		}
157
158		PreTaskID, err := fd.GetCellValue(sheetName, PreTaskIDCell)
159		if err != nil {
160			continue
161		}
162
163		if PreTaskID == "" {
164			PreTaskID = "0"
165		}
166
167		m.Add(PreTaskID, &node{taskID: TaskID, taskTitle: Title})
168	}
169
170	return m
171}
172
173func usage() {
174	w := flag.CommandLine.Output()
175	fmt.Fprintf(w, "%s 应用程序是将Excel任务表中的关系转换成Markdown的mermaid图,方便使用Markdown工具直观地查看任务依赖。", filepath.Base(os.Args[0]))
176	fmt.Fprintln(w)
177	fmt.Fprintf(w, "命令格式:%s -hr [字段所在行号] -dr [数据起始行号] [-nc] -id [任务ID字段名] -t [任务标题字段名] -pid [前置任务ID字段名] -o <输出文件> <Excel文件路径>", filepath.Base(os.Args[0]))
178	fmt.Fprintln(w)
179	flag.CommandLine.PrintDefaults()
180	fmt.Fprintln(w, "  -h")
181	fmt.Fprintln(w, "    \t显示此帮助")
182}
183
184func main() {
185	var outputFile string
186
187	flag.CommandLine.Usage = usage
188	flag.BoolVar(&noCaseSensitive, "nc", false, "字段名不区分大小写")
189	flag.UintVar(&fieldNameRow, "hr", 1, "字段所在行号")
190	flag.UintVar(&dataStartRow, "dr", 2, "数据起始行号")
191	flag.StringVar(&taskIDField, "id", "ID", "-id [任务ID字段名]")
192	flag.StringVar(&taskTitleField, "t", "Title", "-t [任务标题字段名]")
193	flag.StringVar(&preTaskField, "pid", "PreTask", "-pid [前置任务ID字段名]")
194	flag.StringVar(&outputFile, "o", "任务图.md", "-o <输出文件>")
195
196	flag.Parse()
197	if flag.NArg() < 1 {
198		usage()
199		return
200	}
201	if noCaseSensitive {
202		taskIDField = strings.ToLower(taskIDField)
203		taskTitleField = strings.ToLower(taskTitleField)
204		preTaskField = strings.ToLower(preTaskField)
205	}
206	mapTask := readExcel(flag.Arg(0))
207	buildGraph(mapTask, outputFile)
208}
209
210func buildGraph(mapTask multiMap, outputFile string) {
211	graph := "```mermaid\ngraph TB\n"
212	graph += "subgraph  \n"
213	root := mapTask.Get("0")
214	for _, v := range root {
215		graph += visit(rootNodeName, v, mapTask)
216	}
217	graph += "end\n"
218	graph += "```"
219
220	os.WriteFile(outputFile, []byte(graph), os.ModePerm)
221
222	fmt.Println("完成")
223}
224
225func visit(parent string, nd *node, mapTask multiMap) string {
226	slice := mapTask.Get(nd.taskID)
227	graph := fmt.Sprintf("%s --> %s:%s\n", parent, nd.taskID, nd.taskTitle)
228	if parent == rootNodeName {
229		graph += "subgraph  \n"
230	}
231	for _, x := range slice {
232		graph += visit(fmt.Sprintf("%s:%s", nd.taskID, nd.taskTitle), x, mapTask)
233	}
234	mapTask.Del(nd.taskID)
235	if parent == rootNodeName {
236		graph += "end\n"
237	}
238	return graph
239}

使用栈实现的版本笔者放在 excelTask2md了。