{"id":30840747,"url":"https://github.com/pgdr/gamedev","last_synced_at":"2025-09-06T19:11:11.205Z","repository":{"id":303882144,"uuid":"1012529914","full_name":"pgdr/gamedev","owner":"pgdr","description":null,"archived":false,"fork":false,"pushed_at":"2025-08-30T19:46:59.000Z","size":130,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-08-30T21:25:38.043Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pgdr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-07-02T13:23:37.000Z","updated_at":"2025-08-30T19:47:02.000Z","dependencies_parsed_at":"2025-07-10T08:46:13.098Z","dependency_job_id":"4335561e-174c-4dc2-af88-06a8074ace32","html_url":"https://github.com/pgdr/gamedev","commit_stats":null,"previous_names":["pgdr/gamedev"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/pgdr/gamedev","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdr%2Fgamedev","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdr%2Fgamedev/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdr%2Fgamedev/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdr%2Fgamedev/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pgdr","download_url":"https://codeload.github.com/pgdr/gamedev/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdr%2Fgamedev/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273949690,"owners_count":25196559,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-06T02:00:13.247Z","response_time":2576,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-09-06T19:11:08.357Z","updated_at":"2025-09-06T19:11:11.196Z","avatar_url":"https://github.com/pgdr.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"---\ndocumentclass: book\ntitle: Introduction to Graph Theory for Gamedevs\nauthor: Pål Grønås Drange\ndate: Summer 2025\npapersize:\n  - a5\nheader-includes:\n  - \\usepackage{microtype}\n  - \\usepackage{newpx}\n  - \\usepackage[USenglish]{babel}\n  - \\usepackage{tcolorbox}\n  - \\usepackage{froufrou}\n  - \\DeclareUnicodeCharacter{221E}{\\ensuremath{\\infty}}\n---\n\\frontmatter\n# Preface\n\nThis pamphlet serves two purposes.\n\n1. Introduce graph theory to teach concepts and graph structures for game\n   developers, both programmers and artistic creators. This also entails\n   the explanation of some algorithms, although all algorithms mentioned\n   herein are relatively simple.\n\n2. Through structures, models, and problems from graph theory, inspire game\n   creators about possible games, puzzles, and AI strategies. It may also\n   explain what makes certain graph problems easy or hard.\n\nEvery chapter in this brochure starts with an informal discussion about\na concept from graph theory.  This informal introduction should be\nunderstandable by just about anybody interested enough to pick up this\nbrochure from wherever it was found.\n\nThe informal discussion is meant to foster ideas about game mechanisms\nand how to take concepts from graph theory into the game world.\nFollowing the informal discussion will be a more formal part containing\nsome formalisms, and occasionally straight-up theorems.\nFinally, each chapter ends with a verse on algorithms for the specific\nconcepts described.\n\n\n\\tableofcontents\n\n\\mainmatter\n\n# Dots and Lines\n\nA graph is a mathematical object where we have objects (abstractly called nodes\nor vertices) and relations between them.  If two objects, e.g. $v_1$ and $v_2$\nhave a relation, we say that $v_1$ and $v_2$ have an _edge_.  Sometimes this\nrelationship is abstract, as in \"they live in the same city\", \"they went to the\nsame school\", and sometimes it's more concrete, $v_1$ and $v_2$ are\nintersections and $e$ is a road segment that goes directly between $v_1$ and\n$v_2$.\n\nWe will be completely abstract and say that we have a _set_ of _nodes_ and a\nset of _edges_, and that is it.\n\n![A simple graph](assets/simple-graph.pdf)\n\nOften the edges, i.e. relationships, are _symmetric_.  That means that if $v_1,\nv_2$ is an edge, then necessarily $v_2 v_1$ is an edge.  For example consider\nthat $v_1$ and $v_2$ are friends, or live in the same street, went to the same\nschool, or that there is a trail in the woods from $v_1$ to $v_2$.  But\nsometimes it's not symmetric.  Just because Jack follows Jill on some social\nsite, it's not necessarily the case that Jill follows Jack.  Furthermore, just\nbecause you can drive from $v_1$ to $v_2$ doesn't mean that you can go the\nother way, as in _one-way streets_ (or; you can, it's just not guaranteed to be\nlegal).  An edge can mean that $v_1$ is the mother of $v_2$.  Is $v_2$ the\nmother of $v_1$, then? Certainly not.\n\nWe call graphs that are inherently asymmetric _directed graphs_.\n\nSome times, we would like to put some _values_, often referred to as _weights_\nor _costs_, on our objects or edges.  Suppose that you have a road network, and\nyou want to know what the length, speed limits, or perhaps tolls, are on roads\n(or all).  Then we say that we have a _weighted graph_, and each edge $e =\n(v_1, v_2)$ has an edge cost $c(e)$.  If you have an _unweighted graph_, it's\nthe same as assuming that $c(e)= 1$ for all edges.\n\nIf you want to go from $A$ to $B$ in your road network, you might want to find\na route that is _shortest_, _fastest_, or _cheapest_.  Short means length, fast\nmeans length times (expected) speed, cheapest is perhaps sum of tolls.  In all\nthese cases, the weight of an edge is some non-negative value.  A road segment\ncannot be a negative length, a speed limit is unlikely to be negative, and the\ntolls on a road is likely non-negative (except someone has a crate of gold to\ngive you).  However, one can definitely imagine negative weights: Let the\nweight of a road segment represent the energy used by an electric vehicle (EV)\nto traverse it.  An EV uses a certain amount of energy to travel a segment,\ndepending on distance and slope.  If the road is steep downhill and the EV has\nregenerative braking, it might actually _gain energy_, resulting in a\n_negative_ net energy usage for that segment.  This means that if you want to\nfind a shortest path from $A$ to $B$ for an EV, you need to take into account\nbattery level and slope, and perhaps your path-finding algorithm needs to\nhandle negative weights---something we will see later that Dijkstra's algorithm\ndoes not do.\n\n\\froufrou\n\nUltimately, a graph is therefore---mathematically, and in code---a tuple\nconsisting of the vertices $V$, the edges $E \\subseteq V \\times V$, sometimes a\nweight function on vertices $w: V \\to \\mathbb{R}$, and a weight function on\nedges $c: E \\to \\mathbb{R}$.  In code,\n\n```python\nG = (V, E, w)\n```\n\n## Representation and Prerequisites\n\nWhen representing a graph in a computer program, we could go the easy way and\nuse two lists.  One list of nodes, $V$, and one list of edges $E$.  However, it\nturns out that for many algorithmic purposes, this is too slow.  Instead we use\none of two ways of representing graphs that are _adjacency list_ or _adjacency\nmatrix_.\n\n### Adjacency List\n\nThe simplest form of a graph represented in code is the adjacency list:\n\n![A simple graph with letters for nodes](assets/simple-named-graph.pdf)\n\n\n\\clearpage\n```python\nG = [\n     [b],        # a\n     [a, c, e],  # b\n     [b, d, f],  # c\n     [c, f],     # d\n     [b, f],     # e\n     [c, d, e],  # f\n]\n```\n\n### Adjacency Matrix\n\n\n\n```python\nG = [ # a  b  c  d  e  f\n       [0, 1, 0, 0, 0, 0],  # a\n       [1, 0, 1, 0, 1, 0],  # b\n       [0, 1, 0, 1, 0, 1],  # c\n       [0, 0, 1, 0, 0, 1],  # d\n       [0, 1, 0, 0, 0, 1],  # e\n       [0, 0, 1, 1, 1, 0],  # f\n]\n```\n\n### Grid Representation\n\nSometimes we have a very small tile world, and it might make sense to represent\nthe entire graph as a simple list of characters.  Here, a `.` means an empty\nspace, whereas a `#` means an obstacle.  The `S` and `T` are the start and goal\nof a maze.\n\n\n```\n####################\n#T.#.#....#...#....S\n##.###.##.###.##.###\n#..##..#..##..#..##.\n##..#.###....###....\n##....###....###.###\n##.######.###.##.###\n#..##..#..##.....##.\n#..#...#......#..#..\n####################\n```\n\nIn this maze, we are allowed to move `U`, `D`, `R`, and `L`, as long as we do\nnot enter an obstacle.  It is not hard to see that the following gives a path\nfrom `S` to `T`.  We will see in the next section how to find this path\nprogramatically.\n\n```\n####################\n#To#.#oooo#...#.oooS\n##o###o##o###.##o###\n#.o##oo#.o##..#.o##.\n##oo#o###o...###o...\n##.ooo###o...###o###\n##.######o###.##o###\n#..##..#.o##ooooo##.\n#..#...#.oooo.#..#..\n####################\n```\n\n\n# Paths, Navigation, and the Art of Finding\n\n## Path Finding and Mazes\n\nSuppose that you have an AI who wants to travel from $A$ to $B$, or that _you_\nwant to travel from $A$ to $B$, and you want to highlight the path on the map.\nIn this case, we need to find a _path_, whatever that is, that (a) starts in\n$A$, (b) ends in $B$, and is (c) as short as possible.  There are many path\nfinding algorithms, and they all serve different purposes.\n\nTo illustrate the simplicity of a path finding algorithm, here is DFS:\n\n```python\ndef dfs(G, u, visited=None):\n  if visited is None:\n    visited = set()\n  visited.add(u)\n  for v in G[u]:\n    if v not in visited:\n      dfs(G, v, visited)\n  return visited\n```\n\nThe problem with DFS, however, is that it does not necessarily find the\n_shortest_ path, only some path.  In addition, as it is now, it is recursive,\nand that is something we want to avoid.  So instead of using DFS, we will use\nBFS for our path finding.  Here is BFS, the simplest path finding algorithm\nthat finds (and returns) the shortest path from $s$ to _any other vertex_,\ntogether with the distance.  The running time of BFS is $O(n+m)$, meaning\n_linear time_, meaning that it essentially takes as long time to run BFS as it\ntakes to read the graph.\n\n\\clearpage\n\\scriptsize\n```python\nfrom collections import deque\n\ndef bfs(G, s):\n  visited = set()\n  dist = {s: 0}\n  parent = {s: None}\n  queue = deque([s])\n  while queue:\n    u = queue.popleft()\n    for v in G[u]:\n      if v not in visited:\n        visited.add(v)\n        dist[v] = dist[u] + 1\n        parent[v] = u\n        queue.append(v)\n  return dist, parent\n```\n\\normalsize\n\\clearpage\n\nSuppose that you have a map, and your player is in the spawn area, and wants to\nfind a route to the bomb site.  As long as your world has, for each coordinate,\na list of \"neighboring cells\", you can plug that directly into the `bfs`\nroutine above, where $G[u]$ means the neighboring cells of $u$.  The return\nvalue, i.e., when we write\n\n```python\ndist, parent = bfs(world, current_location)\n```\n\n\\noindent is the distance and a form of linked list leading from each vertex to\na vertex closer to the start.\n\nIn fact, since BFS (and as we will later see, Dijkstra's algorithm) finds the\nshortest path from a specific _source_ to all other vertices (unless\nspecifically aborted once we reach our target, of course), we sometimes refer\nto this problem as \\textsc{Single Source Shortest Path}, or SSSP for short.\n\n\n\n### Flood Filling\n\nFlood filling algorithms can simulate water flooding on discrete 2D surfaces\nembedded in 3D environments.  Imagine water pouring onto a terrain from a\nspecific aquifer point: the water naturally spreads outward, filling\nneighboring lower or equal-height cells before progressively rising to higher\nelevations.  Using BFS, the simulation accurately captures this spreading\nbehavior, incrementally marking cells as flooded while respecting the terrain's\ntopography.\n\nBy iteratively expanding the flooded region from the initial aquifer location,\ndevelopers can realistically model scenarios like rising water levels, dynamic\nriver flows, or inundation puzzles.  Such simulations not only enhance visual\nrealism but also open up compelling gameplay mechanics where players must\nstrategically manipulate terrain or barriers to direct water flow, protect\ncertain regions, or solve environmental puzzles involving flooding.\n\n\n\n\n### Weighted graphs\n\nWhen the \"cost\" of moving from one \"cell\" to another is not the same\neverywhere; for example running in water or running uphill can be more\nexpensive, we need an algorithm that can deal with these costs.  BFS can\nunfortunately not.\n\n![A graph with weights on the edges](assets/simple-weighted-graph.pdf)\n\nDijkstra's algorithm is an SSSP (single source shortest path) algorithm that\nworks similarly to BFS, but instead\nof a queue that we iterate through, we have a _priority queue_.  A priority\nqueue is a queue in which the First In First Out is replaced with \"Most\nImportant Out\".  That is, the order of insertion is irrelevant; when we pop\nfrom the queue, we pop the one with highest priority (typically this is the one\nwith _lowest possible value_).\n\n\\clearpage\n\\scriptsize\n```python\nimport heapq\n\ndef dijkstra(G, s):\n  dist = D(lambda: INF)  # distance from source\n  visited = D(bool)      # visited = False\n  dist[source] = 0       # starting point\n  queue = [(0, source)]\n\n  while queue:\n    dv, v = heappop(queue)\n    if dist[v] \u003c= dv:\n      continue\n\n    for u in neighbors[v]:\n      if dist[u] \u003e dv + w(v, u)\n        dist[u] = dv + w(v, u)\n        heappush(queue, (dist[u], u)\n```\n\\normalsize\n\\clearpage\n\n## Puzzle Solving\n\nIt is not only in the literal sense that path finding algorithms are useful.\nConsider Rubik's Cube, the 15-puzzle, or the classic Die Hard puzzle of filling\na 5 liter bucket with 4 liters of water using only 3 and 5 liter buckets.\n\n\n### Water Jug Problem\n\nSuppose, we want to help MacClane with his task of putting 4 liters of water in a 5 liter bucket using only one 3 and one 5 liter buckets.\nConstruct an abstract \"state\nspace graph\" where you have the node set as $(b_3, b_5)$ where $b_3 \\in \\{0, 1,\n2, 3\\}$ and $b_5 \\in \\{0, 1, 2, 3, 4, 5\\}$.  Suppose that you have $b_3$ liters\nof water in the small bucket and $b_5$ liters of water in the large bucket.\nWhat can you do?  Fill one of them, i.e., the next states are $(3, b_5)$ or\n$(b_3, 5)$.  Alternatively, you can empty one of them, i.e., the next states\nare $(0, b_5$) or $(b_3, 0)$.  Finally, you can pour from one to the other\nuntil either pourer becomes empty or the pouree becomes full: $(\\max(b_5 - b_3,\n0), \\min(b_5 + b_3, 5))$, or the other way around.  How do we go from $(0,0)$ to\n$(0, 4)$?  Simply run `bfs(G, (0,0))`.\n\n![Two jugs of water](assets/water-jug.pdf)\n\nLet us denote the possible moves as follows\n$$ b_3\\to 0\\quad b_5\\to 0\\quad b_3\\to b_5\\quad b_5\\to b_3\\quad \\infty \\to b_3\\quad \\infty \\to b_5, $$\nwhere $b_3 \\to 0$ means we empty the first bucket,\n$b_3\\to b_5$ means me transfer content from $b_3$ to $b_5$ until either $b_3$ is empty or $b_5$ is full\n(whichever happens first),\nand $\\infty \\to b_3$ means we fill up $b_3$ (similarly with $b_3$ and $b_5$ swapped).\n\n\n\n\\clearpage\n\\scriptsize\n```python\nfrom collections import deque, namedtuple\n\ndef bfs(s):\n  visited = set()\n  dist = {s: 0}\n  parent = {s: None}\n  queue = deque([s])\n  while queue:\n    u = queue.popleft()\n    for v in next_state(u):\n      if v not in visited:\n        visited.add(v)\n        dist[v] = dist[u] + 1\n        parent[v] = u\n        queue.append(v)\n  return dist, parent\n\nJugs = namedtuple(\"Jugs\", \"b3 b5\")\nSTART = Jugs(0, 0)\nGOAL = Jugs(0, 4)\n\ndef next_state(state):\n  yield Jugs(0, state.b5)  # b3 → 0\n  yield Jugs(state.b3, 0)  # b5 → 0\n  yield Jugs(3, state.b5)  # ∞ → b3\n  yield Jugs(state.b3, 5)  # ∞ → b5\n  yield Jugs(max(state.b3 - (5 - state.b5), 0),\n             min(5, state.b3 + state.b5))       # b3 → b5\n  yield Jugs(min(3, state.b3 + state.b5),\n             max(state.b5 - (3 - state.b3), 0)) # b5 → b3\n\ndef is_goal(state):\n  return state == GOAL\n\ndist, parent = bfs(START)\ncurrent = GOAL\nsolution = [GOAL]\nwhile (current := parent[current]) != START:\n  solution.append(current)\nsolution.append(START)\nfor step, state in enumerate(reversed(solution)):\n  print((1 + step), state)\n```\n\\normalsize\n\n\\clearpage\n\nThe above code solves the _Water Jug Problem_ and will print the steps needed to\ntake to achieve the solution.\n\n```\n1 Jugs(b3=0, b5=0)\n2 Jugs(b3=0, b5=5)\n3 Jugs(b3=3, b5=2)\n4 Jugs(b3=0, b5=2)\n5 Jugs(b3=2, b5=0)\n6 Jugs(b3=2, b5=5)\n7 Jugs(b3=3, b5=4)\n8 Jugs(b3=0, b5=4)\n```\n\n### Other Puzzles\n\nThe 15-puzzle and the Rubik's cube are other examples of games where we can\nrepresent the state space as a graph and use a search algorithm for finding the\nshortest path.\n\n### Exercise: Wolf, Goat, and Cabbage\n\nThe Wolf, Goat, and Cabbage problem is a classic river-crossing puzzle.  In\nthis scenario, a farmer needs to cross a river with a wolf, a goat, and a\ncabbage.  The farmer has a small boat that can only carry himself and one of\nthe three items at a time.  The challenge lies in ensuring that the wolf does\nnot eat the goat when left alone together and the goat does not eat the cabbage\nunder the same conditions.  Here are the rules:\n\n1. The farmer can take only one item at a time across the river.\n2. If left alone, the wolf will eat the goat, and the goat will eat the cabbage.\n3. The goal is to transport all three items across the river without any of them being eaten.\n\nUsing a similar approach as the Water Jug Problem, represent the state of the\ngame as a graph.  Define the states using a triplet notation $(F, W, G, C)$\nwhere these take values 0 and 1, meaning start or opposite side:\n\n- $F$ is the farmer's location (0 or 1)\n- $W$ is the wolf's location (0 or 1)\n- $G$ is the goat's location (0 or 1)\n- $C$ is the cabbage's location (0 or 1)\n\nOnce you have your implementation, demonstrate that you can reach the goal\nstate $(1, 1, 1, 1)$ starting from $(0, 0, 0, 0)$ and list the steps taken to\ndo so.\n\n\n\n## Informed Search\n\nDijkstra's algorithm works well for graphs with non-negative distances, but in\nsome cases, it explores an excessively large part of the graph.  We will now\nlook at an algorithm called $A^*$ that uses a _heuristic_ to \"steer\" the\ndirection of the search.  It is simply Dijkstra's algorithm, but with a small\nestimation of remaining distance.  However, contrary to Dijkstra's algorithm,\nwe necessarily terminate when we discover $t$, so this is _not_ an SSSP.\n\n\\clearpage\n\\scriptsize\n```python\nimport heapq\n\ndef astar(G, s, t, h):\n  dist = {s: 0}\n  parent = {s: None}\n  heap = [(h(s), s)]\n  while heap:\n    _, u = heapq.heappop(heap)\n    if u == t:\n      break\n    for v, w in G[u]:\n      alt = dist[u] + w\n      if v not in dist or alt \u003c dist[v]:\n        dist[v] = alt\n        parent[v] = u\n        heapq.heappush(heap, (alt + h(v), v))\n\n  return dist, parent\n```\n\\normalsize\n\\clearpage\n\n## Negative Distances\n\nAs mentioned above, suppose the energy usage for an EV is negative in certain\nsteep downhill segments.\n\nGraphs with negative edge weights introduce new complexities into pathfinding\nalgorithms.  While typical graph traversal problems---like roads or\npaths---naturally have non-negative weights (distance, cost, or time), negative\nweights can represent scenarios like energy regeneration in electric vehicles\ntraveling downhill.  Here, the EV actually gains energy, making the edge weight\nnegative.  This fundamentally alters the nature of shortest-path calculations.\n\nAlgorithms like Dijkstra's, which rely on non-negative edge weights, fail to\nhandle negative distances correctly because they assume that visiting a node\nonce guarantees the shortest path to it.  With negative edges, previously\ncomputed paths may need constant revising, potentially leading to incorrect\nresults.  Thus, we require specialized algorithms such as the Bellman--Ford or\nJohnson's algorithms that can accommodate negative weights and correctly\ncompute shortest paths even when negative cycles are present.\n\nInterestingly, negative cycles---loops with a negative total weight---can\nthemselves become part of the gameplay mechanics.  For instance, players might\nexploit negative cycles strategically to replenish energy or accumulate\nresources, introducing intriguing tactical considerations into game design.\n\n\n\n\\clearpage\n\\scriptsize\n```python\ndef bellman_ford(G, s):\n  dist = {v: float(\"inf\") for v in G}\n  dist[s] = 0\n  parent = {v: None for v in G}\n  n = len(G)\n\n  # Relax edges up to n-1 times\n  for _ in range(n - 1):\n    updated = False\n    for u in G:\n      for v, w in G[u]:\n        if dist[u] + w \u003c dist[v]:\n          dist[v] = dist[u] + w\n          parent[v] = u\n          updated = True\n    if not updated:\n      break\n\n  # Check for negative-weight cycles\n  for u in G:\n    for v, w in G[u]:\n      if dist[u] + w \u003c dist[v]:\n        raise ValueError(\"Graph contains a negative-weight cycle\")\n\n  return dist, parent\n```\n\\normalsize\n\\clearpage\n\n## Bidirectional search\n\nWe saw that BFS can be used to solve puzzles such as the *Wolf, Goat, and\nCabbage* problem and the *Water Jug* problem.  However, the number of nodes explored\ngrows exponentially in the depth; If the *branching factor* is $b$ and the\n_distance_ from the start to the goal is $d$, the total number of nodes\nexplored is $b^d$.\n\nThere is a way to drastically reduce the number of explored nodes, however.\nAs an illustration, imagine that the search is a circle, and the number of nodes explored is the\n_area_ of the circle.  Then the area is $\\pi d^2$, where $d$ (radius) is distance from\nstart to goal.  If we *instead* search from the start node and the goal node\n*simultaneously*, and abort the search once the two searches intersect, you can\nvisualize this as _two circles_ with radius $d/2$.  The area of two circles\nwith half the radius is much smaller than the area of one large circle with the\nfull radius (by quadratic order).\n\nThe same reasoning holds in our case, but where the quadratic improvement\nbecomes a (potentially) exponential improvement.  If the (and this is a **big\nif**) _branching factor_ is the same in both directions, then we expore $2\nb^{d/2}$ nodes instead, i.e. $\\sqrt{b^d}$, it significantly reduces the\nnumber of explored nodes.\n\n\n\n\\begin{tcolorbox}[title=Example]\nFor example, let $b=12$ and $d=24$ (a natural assumption for Rubik's cube).\nIf we explore a billion nodes per second,\nthen running BFS would take about 600 million years.\nBy using bidirectional search, it would take less than an hour.\n\\end{tcolorbox}\n\n\\paragraph{Backwards branching factor.}  So what is the _backwards branching\nfactor_ for different cases?  In the *Rubik's cube* problem, it is the same if\nwe go back or forth.  In each state we have 12 possible moves, and it doesn't\nmatter if we go forwards or backwards; if we want to move from the goal state\nto the start state, we have the same possible moves.  The same holds for the\n*15-puzzle* problem.  In each case, we can move the blank N/S/E/W, i.e., the\nbranching factor is 4 both forwards and backwards.  In undirected graph search,\nthe backward branching factor is the same as the forward one;\nIt is the _degree_ of the\nvertex $\\deg(v)$.\n\nHowever, for the Water-Jug problem the approach breaks down.  Suppose that you\nare in a state, e.g. $(0,0)$.  In the forwards direction we have 6 potential\nmoves always.  However, in the backwards direction, we must ask: *where did we\ncome from* to reach $(0,0)$.  And it turns out that there are many more\npossibilities: the previous *move* could have been to empty the first jug.\nIn that case, the previous state could have been any of $(k, 0)$ for $1 \\leq k\n\\leq 5$.  The same for the second jug, $(0,k)$.\nThe problem becomes much worse if the jugs’ capacities are,\nsay, 17 and 43.  In the\nforwards direction we still only have 6 possibilities:\n\n$$ a\\to 0\\quad b\\to 0\\quad a\\to b\\quad b\\to a\\quad \\infty \\to a\\quad \\infty \\to b. $$\n\n\\paragraph{Exercise:} How many moves does it take to get to state `Jugs(b17=0,\nb43=4)` if your bottles are of capacities 17 and 43.  How many nodes are\nexplored in BFS versus bidirectional search?\n\n\\paragraph{Exercise:} Implement bidirectional search for the 15 puzzle.\nCompare the number of explored nodes with $A^*$.\n\n\\paragraph{Exercise:} Suppose you are at $x$ and $y$.  Your potential moves are\nalways $(x+y, x)$ and $(y, x+y)$.  Write an algorithm for finding out if there\nis a path from $(1,1)$ to $(a,b)$.\n\n\n\n## All-Pairs Shortest Paths\n\nIn certain cases, it's useful to compute all shortest paths between all pairs.\nHowever, it is important to realize that if we have $n$ vertices, then there\nare $n^2$ pairs (if the graph is undirected, we can get away with $1/2n^2$\npairs).  In any case, any algorithm that computes all these $n^2$ values has to\nuse at least $n^2$ time.  If we run Dijkstra's algorithm for each vertex, we\nend up with a running time of $O(n \\cdot ( m \\log n))$.\n\nThe APSP problem is a foundational problem in theoretical computer science and\nfaster algorithms are being developed every decade.  For the unweighted\nundirected version, the problem can be solved as quickly as matrix\nmultiplication, and for the weighted case, it can be approximated in the same\ntime.\n\nIn $n^3$ time, we can compute, using dynamic programming, the APSP dists\ndictionary.\n\n\\clearpage\n\\scriptsize\n```python\ndef apsp(G):\n  dists = {(u, v): INFTY for u, v in choice(G.nodes, 2)}\n  for node in G.nodes:\n    dists[(node, node)] = 0\n  for u, v, data in G.edges(data=True):\n    dists[(u, v)] = data.get(\"weight\", 1)\n    dists[(v, u)] = data.get(\"weight\", 1)\n  for k in G.nodes:\n    for i in G.nodes:\n      for j in G.nodes:\n        if dists[(i, j)] \u003e dists[(i, k)] + dists[(k, j)]:\n          dists[(i, j)] = dists[(i, k)] + dists[(k, j)]\n  return dists\n```\n\\normalsize\n\\clearpage\n\n\n## Future Reading\n\n* Problems\n  * Randomly pick $\\sqrt n$ nodes $P$ as _portals_ (keyword: transit nodes,\n    distance oracle, highways) and compute the APSP for $P$.\n  * Distance oracle for planar graphs\n* Algorithms\n  * Johnson's algorithm\n\n\n# Global Navigation\n\nIn the previous chapter, we saw problems where we wanted to find a path\nfrom one node in a graph to another, i.e., the fewest number of edges to\nconnect one vertex to another.  All of the problems in the previous\nchapter are \"easy\", in the sense that they have an \"efficient\nalgorithm\".  An _efficient algorithm_ is something we have defined to be\nan algorithm with a _polynomial running time_.  We will in this chapter\nsee some problems that don't necessarily have such efficient\nalgorithms.\n\nWe mentioned that the shortest path is the problem of finding the fewest\npossible number of edges connecting $A$ to $B$.  What about the\nfollowing: consider a road network where in each node, there is a house,\nexcept in one node, there is a power station.  You want to put up\nelectric wires along (some of) the roads so that every house is\nconnected to the power station, and you want to use as little wiring as\npossible.  In other words: find the fewest possible number of roads such\nthat _all nodes_ are connected.  Surprisingly, perhaps, this problem has\nan efficient algorithm.\n\n## Trees In Our Graphs\n\nThe aforementioned problem is called \\textsc{Minimum Spanning Tree} (MST).\nYou are given a graph, preferably with weights, and you want to find a\n_spanning tree_ in the graph, that is a _tree_ that contains all nodes.\nAnd out of all possible spanning trees, you want to find one which is as\ncheap as possible.  There are two famous algorithms for this problem,\nPrim's algorithm, which is Dijkstra's algorithm with simply this\nmodification.\n\n\n![A spanning tree in a graph](assets/mst.pdf)\n\n\n```diff\n15,16c15,16\n\u003c       if dist[u] \u003e dv + w(v, u)\n\u003c         dist[u] = dv + w(v, u)\n---\n\u003e       if dist[u] \u003e w(v, u)\n\u003e         dist[u] = w(v, u)\n```\n\nHence, we will focus on _Kruskal's algorithm_ for computing a _minimum\nspanning tree_, because it introduces a very nifty data structure:\nUnion--Find.\n\n\n```python\ndef find(u):\n  if comp[u] == u:\n    return u\n  else:\n    parent = find(u)\n    comp[u] = parent  # \"memoize\"\n    return parent\n\ndef union(u, v):\n  r1 = find(u)\n  r2 = find(v)\n  comp[r1] = r2\n```\n\n## Steiner Tree: Non-Full Spanning Trees\n\nWe now come to the first problem in this pamphlet that we cannot see a\npolynomial time algorithm for, even though ... in a sense ... it sounds simpler\nthan the previous problem.\n\nSuppose you have the same road network as above, with a power station and\nhouses, but now not every node is a house, there are just houses in some of the\nnodes.  Meaning that you have _fewer_ houses than before.  The problem,\nhowever, is the same: Find the minimum network (in terms of electric cabling)\npossible that connects all houses to the power station.\n\n![A Steiner Tree instance](assets/steiner-tree.pdf)\n\n## Travelling Salesman\n\nIf you are standing on a location $A$ and want to reach a location $B$,\nwe know what to do; shortest path.\nHowever, what if you want to visit not one but two locations?\nWell, either ABC or ACB.\nWhat if three locations?\nWell, either one of ABCD, ABDC, ACBD, ACDB, ADBC, ADCB.\nOkay, now four locations?\nThe possibilities are\n\n```\nABCDE, ABCED, ABDCE, ABDEC,\nABECD, ABEDC, ACBDE, ACBED,\nACDBE, ACDEB, ACEBD, ACEDB,\nADBCE, ADBEC, ADCBE, ADCEB,\nADEBC, ADECB, AEBCD, AEBDC,\nAECBD, AECDB, AEDBC, AEDCB.\n```\n\nThis set doesn't only grow _exponentially_, it\ngrows _super-exponentially_: Like $n^n$, which is something like $2^{n\n\\log_2 n}$.\n\nThere is, however, an algorithm that solves the problem in time $2^nn^2$.  But\nbefore we look at Travelling Salesman, we'll look at a fundamental graph\nproperty, _hamiltonicity_.\n\n\n\\clearpage\n\n### Hamiltonian Path\n\nLook at the following graph.\n\n![Is this graph Hamiltonian?](assets/simple-large.pdf)\n\nIs it possible to walk through this graph, visiting every vertex exactly once,\nending up in the same vertex you started in?  If so, the graph is\n_Hamiltonian_.  A Hamiltonian cycle is a simple cycle on $n$ vertices.  A\nHamiltonian path is a simple path on $n$ vertices.\n\n\nThe naïve algorithm for checking if a graph has a Hamiltonian path:\n\n\\clearpage\n\\scriptsize\n```python\ndef hamiltonian_path(G):\n  DP = defaultdict(lambda: INFTY)\n  for v in G.nodes:\n    DP[{v}, v] = 0\n  for S in subsets(G.nodes):\n    DP[S, v] = min(DP(S.minus(u), u) + G[u, v].weight for u in S)\n  return any(DP[set(G.nodes, v)] for v in G.nodes)\n```\n\\normalsize\n\nThis is a Hamiltonian _path_, not a Hamiltonian cycle:\n\n\n![A Hamiltonian **Path**](assets/simple-large-hamiltonian.pdf)\n\nThis, however, is a proper Hamiltonian cycle, which shows that the graph indeed\nis Hamiltonian.\n\n![The graph is indeed Hamiltonian](assets/simple-large-hamiltonian-cycle.pdf)\n\n\n\n\n### Travelling Salesman --- Revisited\n\nTravelling Salesman is basically the same problem as Hamiltonian path or cycle,\nexcept that you don't necessarily have to visit every node.  Suppose you have a\nbunch of _points of interests_ that are spread around in a large graph.  You\nneed to visit every POI in as short a distance as possible.\n\nThe easy solution is to run APSP and then take graph with only the POIs.\n\n\n### Longest Path\n\nIn \\textsc{Ticket 2 Ride}, the winning criterion is who has built the longest path.\nThis is in fact an NP-hard problem!  The game developers ask us to, after the game has been completed, to solve a problem known to need exponential time!  The audacity ...\n\n\n## Euler Tour, or: Bridges of Königsberg\n\nA problem very similar to the Hamiltonian Cycle problem is: What if you don't\nvisit every _node_ exactly once, but ever _edge_ exactly once.  Surprisingly,\nthis is solvable with a trivial check.  Graph theory was invented in 1736 when\nmathematician Leonhard Euler presented the solution to the famous problem of\nthe seven bridges of Königsberg.\n\nArranging domino tiles can be modeled by an Eulerian path, where each tile\nrepresents an edge and each face value corresponds to a vertex in the graph.\nTo successfully create a continuous path using the tiles, one must ensure that\nthe graph formed by the dominoes adheres to specific conditions: either all\nvertices have an even degree, allowing for a closed loop, or exactly two\nvertices have an odd degree, permitting a path.\n\n![A graph with only even degrees: Draw all edges without lifting the\npencil from the paper.  (Hint: Start and end at the same\nnode.)](assets/euler-graph.pdf)\n\n![A graph with two odd nodes: Draw all edges without lifting the pencil\nfrom the paper.  (Hint: Start and end at the two odd\nnodes.)](assets/euler-graph-odd.pdf)\n\n\nThis concept extends beyond dominoes into various game mechanics, such as\ndrawing a figure without lifting your pencil from the paper.\n\nYet a third game possibility: In each hallway there is a door and once you pass\nthrough a door you get a bucket of gold and the door closes shut forever (with\nyou on the other side).  Can you collect all the gold?\n\n\n### The algorithm\n\nTwo cases: Either you start and end in the same node.  Then you enter and exit\neach vertex the same number of times, hence check all degrees are even.\n\nOr you start and end in different nodes: Exactly two nodes have odd degree.\n\nThat's it.\n\n\\clearpage\n\\scriptsize\n```python\nimport networkx as nx\ndef euler(G, v):\n  while G[v]:\n    u = next(iter(G[v]))\n    G.remove_edge(u, v)\n    yield from euler(G, u)\n  yield v\n\nG = nx.Graph()\ne = \"ab ag bc be bg cd de ef eg fh hg\"\nfor u, v in e.split():\n  G.add_edge(u, v)\n\nprint(\" \".join(list(euler(G, \"a\"))))  # → a g h f e g b e d c b a\n```\n\\normalsize\n\\clearpage\n\n# Cutting and Flowing\n\nIn this chapter, we will discuss a fundamental graph theory concept.\n\nLet $G = (V, E, c)$ be a _weighted and directed_ graph, where $c: E \\to\n\\mathbb{N}$ denotes the _capacity_ of each edge, whatever capacity means.  In\nthis section, we will refer to $(G, s, t)$ as a _flow network_, and to confuse\nus a bit, we will compute the _network flow_ of the given _flow network_.\n\n![A flow network.  Water flowing into the leftmost node and coming out of the rightmost node.  With different capacities of each edge, the maximum possible flow varies.](assets/flow-network.pdf)\n\nIntuitively, the _flow_ of a network is easy to understand.  Suppose that you\nput a garden hose with _infinite_ water pressure and stick it into $s$, and you\nsee poke a hole in $t$, to see how much water comes out of $t$ (per time unit).\n\nIf the capacity of an edge $e = (u,v)$ is $c(e) = 3$, that means that $3$ units\nof flow per time unit can go through $e$.  For simplicity: $3$ liters of waters\nper second, i.e., $c(e) \\ell/s$.  How many $\\ell/s$ comes out of $t$?  This is\nthe \\textsc{Maximum Flow} problem.\n\n\\begin{tcolorbox}[title=Theory]\n\nMenger's theorem says that the number of disjoint paths from $s$ to $t$ is\nequal to the number of edges we have to remove to cut $s$ from $t$.\n\n\\end{tcolorbox}\n\n## Minimum Cut\n\nSuppose you have a tower defense like game, where the enemy sends units from\nsome source towards your castle in an intricate road network.  You can put up\nturrets that kill all enemies passing a certain point in the road network.\nWhere should you put turrets so that no enemy unit can reach your castle?\nHere we can also add different costs to the turrets positions (e.g. it's\nexpensive on grass and cheap on gravel).\nThis problem is called the \\textsc{Minimum $s$-$t$-Cut} problem.\n\nLet $G = (V, E)$ be a graph and $s$ and $t$ two vertices.  Which edges should\nyou remove from the graph such that you disconnect $s$ from $t$?\n\nMenger's theorem, and the so-called Max Flow--Min Cut theorem states that the\nmaximum $s$-$t$-flow value is exactly the same as the minimum $s$-$t$-cut\nvalue, and we can find the cut by running and algorithm for maximum flow.\n\nThe algorithm is quite simple, but at the same time not very intuitive.  We are\nnot diving into the reason why it works here, but just state that a simple\ngreedy approach does not work, but that what is needed is to construct a\n_residual graph_ that we actually can run a greedy-ish algorithm on.  The\nentire algorithm can be summarized in these few lines:\n\n\\clearpage\n\\scriptsize\n```python\ndef maxflow(graph, s, t):\n  flow = 0\n  while P := bfs(graph, s, t):\n    bottleneck = min(graph.F[v][u] for (v, u) in edges(P))\n    print(P, bottleneck)\n    if bottleneck == 0:\n      return flow\n    flow += bottleneck\n    for i in range(1, len(P)):\n      v, u = P[i - 1], P[i]\n      graph.F[v][u] -= bottleneck\n      graph.F[u][v] += bottleneck\n  return flow\n```\n\\normalsize\n\\clearpage\n\nIf we run the above algorithm on our Tower Defense graph, and then run a BFS\nfrom $s$ in the remaining graph, we will get the left hand side of the minimum\ncut; Simply place turrets on all edges that leave this set; this will be\noptimal, even in the weighted setting.\n\n* Problems\n  * Matching (assignment)\n  * Min s-t-Cut\n  * Max flow\n  * Disjoint Paths\n  * Vertex and edge capacities, Undirected to directed\n  * Min-cost max flow\n  * Max-weight perfect matching\n  * Circulation with demand\n* Algorithms\n  * Ford--Fulkerson / Edmonds--Karp\n  * The Hungarian Method\n  * Suurballe\n\n# Covering, Hitting, and Packing\n\n## Covering Problems\n\nIn this section, we will first discuss a problem known as \\textsc{Dominating\nSet}.  Suppose that you again have a _tower defense_ game, and the game takes\nplace on some kind of road network.  Your enemy can teleport to different\nintersections in your road network, and when they stand there, they will gather\ninformation.  You can place guards in intersections, and each guard can cover\nthe intersection and all the directly neighboring intersections.  Where should\nwe place the guards?\n\nThis is the \\textsc{Dominating Set} problem, and the problem is quite\nchallenging to solve computationally, however there is a greedy algorithm that\nachieves a $\\log n$-approximation.  For planar graphs, there is an $(1 +\n\\epsilon)$-approximation.\n\nThe \\textsc{Dominating Set} problem aims to cover all neighborhoods $N(v)$ using vertices in $V$.\n\nThe greedy algorithm is as follows:\n\n\\scriptsize\n```python\ndef greedy_domset(G):\n    V, D = set(G), set()\n    for _ in range(len(V)):\n        U = {v for v in G if not (({v} | set(G.neighbors(v))) \u0026 D)}\n        if not U: break\n        best, g = None, -1\n        for v in V - D:\n            s = len((({v} | set(G.neighbors(v))) \u0026 U))\n            if s \u003e g: g, best = s, v\n        D.add(best)\n    return D\n```\n\\normalsize\n\n\n* Problems\n  * R-B-Dom Set, Vertex Cover, Tower defense\n  * War defense, cutting-off\n  * Independent set (scattering)\n* Algorithms\n  * VC appx\n  * VC for bipartite graph\n  * reduction rules\n\n\n# Drawing, Partitioning, and Clustering\n\n* Problems\n  * Graph drawing\n  * Cluster editing\n  * Community detection\n  * Clique (?)\n  * Centrality measures\n  * Cascading and fire fighting, disease propagation\n* Algorithms\n  * Spring layout\n  * Louvain and LCA\n  * Betweenness, Page rank\n\n\n\n\n# Geometric Embeddings\n\nIf you look at a standard game board of, say, Risk, you'll see that the world\nis partitioned into regions using borders consisting of small line segments.\nSuppose that you create a bunch of walls, borders, or fences, and want to find\nout what the regions are.\n\nThis problem is on the outer perimeter of graph theory, but it is in some sense\nrelated to geometric embeddings of planar graphs, so we cover it here.\n\nSuppose that you draw a bunch of points in the plane, and you between some of\nthe pyoints you draw straight lines.  The points (fenceposts) in the plane are\nyour nodes and the straight lines (the fence itself) are your edges.\n\nIf you draw this graph in such a way that no edges intersect (except in their\nendpoints), you have what's called a _straight line drawing of a planar graph_.\n\nIf your graph is 2-connected (i.e., has no bridge), then every edge separates\ntwo regions, called _faces_.  How can we programmatically detect the faces\n(regions) of such a graph, and how would such a mapping look in code,\nspecifically, how can we color every region with a distinct color?\n\n\n\n\n\n\n* Problems\n  * Regions from fences (Planar dual)\n  * Convex hull ??\n* Algorithms\n  * Explain that `left of` (not well known) gives rise to\n    * planar dual (rotation system)\n    * convex hull (and convex polygon containment)\n    * line intersection\n    * triangle and polygon containment\n\n## Interval Graphs\n\n* Problems\n  * Interval scheduling (chromatic number)\n  * Interval partitioning (clique)\n  * Cops \u0026 robbers\n  * Rabbit hunting\n* Algorithms\n  * Greedy algorithms for interval scheduling and coloring\n\n\n# What Now?\n\nIn this final chapter, we see some things that we didn't get to cover due to\nspace or scope, but that nevertheless might be interesting to be aware of.\n\n## Dynamic Graphs\n\nDynamic graphs extend the static model by allowing changes to the graph\nstructure over time, including the addition or removal of vertices and edges.\nThis characteristic is crucial for modeling real-world scenarios such as social\nnetworks, traffic systems, and game environments, where the relationships\nbetween entities evolve.\n\nTraditional algorithms designed for static graphs often struggle in these\nsettings, as their assumptions about fixed structures can lead to\ninefficiencies or inaccuracies when applied to dynamic data.  As a result,\ndynamic graph algorithms must adapt to continually accommodate updates,\nenabling developers to maintain performance while responding to fluid gaming\nenvironments.\n\n\n## Online Algorithms\n\nOnline algorithms are essential for scenarios where data arrives in a\nsequential manner, and decisions must be made without knowledge of future\ninput.  This is particularly relevant in gaming, where an AI may need to\nrespond to player actions in real-time without the luxury of hindsight.  These\nalgorithms process information on the fly, making greedy choices based on the\ncurrent state of the game.  While such strategies can yield efficient results,\nthey often cannot guarantee optimal solutions; however, they can provide\nacceptable approximations quickly.\n\nUnderstanding the principles behind online algorithms allows game developers to\ncreate more responsive and engaging gaming experiences, as they can design\nsystems that treat each player interaction as a new opportunity to adjust\nstrategies dynamically.\n\n\n\\clearpage\n\n\\backmatter\n\n# Notes\n\n\\vfill\n\n\\phantom{X}\n\n\\pagebreak\n\n\\phantom{page intentionally left blank}\n\n\\pagebreak\n\n\\phantom{page intentionally left blank}\n\n\n\\pagebreak\n\n\\phantom{page intentionally left blank}\n\n\n\\pagebreak\n\n\\phantom{page intentionally left blank}\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpgdr%2Fgamedev","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpgdr%2Fgamedev","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpgdr%2Fgamedev/lists"}