Home

Awesome

Tic Tac Toe

This tutorial will walk you through a process of creation of a tic-tac-toe game

Built with Git Tutor

Project setup

Before we actually start writing code, I recommend to install editorconfig plugin for your ide/text editor. It will keep code consistent in terms of line-endings style, indentation, newlines

📄 .editorconfig

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

Every web-app needs an html entry-point, this ain't exception, so let's add simple html file

📄 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tic Tac Toe</title>
</head>
<body>

</body>
</html>

index.js will be a js main file

📄 src/index.js

console.log('Hello world');

Now we need to add script to index.html

📄 index.html

      <title>Tic Tac Toe</title>
  </head>
  <body>
-
+     <script src="./src/index.js"></script>
  </body>
  </html>

Most likely the codebase will grow, so eventually we'll need some module system. This tutorial is not about setting-up a javascript bundler like webpack, so let's just use es6 modules which are already supported by latest Chrome. To make chrome understand import statement, type attribute should be set to module

📄 index.html

      <title>Tic Tac Toe</title>
  </head>
  <body>
-     <script src="./src/index.js"></script>
+     <script src="./src/index.js" type="module"></script>
  </body>
  </html>

Let's get started

Game state

Let's define a game state variable

📄 src/index.js

- console.log('Hello world');
+ const GameState = {
+
+ }

We'll need an information about current player to know whether x or o should be placed on a game field.

📄 src/index.js

  const GameState = {
-
+     currentPlayer: 0,
  }

0 – x should be placed

1 – o

field property will represent a game state. That's an array of 9 elements (3 columns x 3 rows) with initial value -1. Simple if (fieldValue > 0) check will work to distinguish empty fields from filled.

📄 src/index.js

  const GameState = {
      currentPlayer: 0,
+     field: Array.from({ length: 9 }).fill(-1),
  }

Game state modifications

Now we need to implement a function which will switch a current player. Let's do this with XOR operator. (how xor works).

📄 src/index.js

      currentPlayer: 0,
      field: Array.from({ length: 9 }).fill(-1),
  }
+
+ function changeCurrentPlayer(gameState) {
+     gameState.currentPlayer = 1 ^ gameState.currentPlayer;
+ }

To modify field values in plain array we'll need a function to convert row and col indices to an array index

📄 src/index.js

  function changeCurrentPlayer(gameState) {
      gameState.currentPlayer = 1 ^ gameState.currentPlayer;
  }
+
+ function getArrayIndexFromRowAndCol(rowIndex, colIndex) {
+     return rowIndex * 3 + colIndex;
+ }

Game turn logic

Now we'll start handling game turn logic. Create a function placeholder

📄 src/index.js

  function getArrayIndexFromRowAndCol(rowIndex, colIndex) {
      return rowIndex * 3 + colIndex;
  }
+
+ function turn(gameState, rowIndex, colIndex) {
+
+ }

Convert row and col indices to plain array index

📄 src/index.js

  }

  function turn(gameState, rowIndex, colIndex) {
-
+     const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);
  }

If game field already contains some value, do nothing

📄 src/index.js


  function turn(gameState, rowIndex, colIndex) {
      const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);
+     const fieldValue = GameState.field[index];
+
+     if (fieldValue >= 0) {
+         return;
+     }
  }

Put player identifier to a field

📄 src/index.js

      if (fieldValue >= 0) {
          return;
      }
+
+     gameState.field[index] = gameState.currentPlayer;
  }

and change current player

📄 src/index.js

      }

      gameState.field[index] = gameState.currentPlayer;
+     changeCurrentPlayer(gameState);
  }

Win

The next thing we need to handle is a "win" state. Lets add helper variables which will contain array indices by rows:

📄 src/index.js

      field: Array.from({ length: 9 }).fill(-1),
  }

+ const Rows = [
+     [0, 1, 2],
+     [3, 4, 5],
+     [6, 7, 8],
+ ];
+
  function changeCurrentPlayer(gameState) {
      gameState.currentPlayer = 1 ^ gameState.currentPlayer;
  }

cols:

📄 src/index.js

      [6, 7, 8],
  ];

+ const Cols = [
+     [0, 3, 6],
+     [1, 4, 7],
+     [6, 7, 8],
+ ];
+
  function changeCurrentPlayer(gameState) {
      gameState.currentPlayer = 1 ^ gameState.currentPlayer;
  }

and diagonals

📄 src/index.js

      [6, 7, 8],
  ];

+ const Diagonals = [
+     [0, 4, 8],
+     [2, 4, 6],
+ ];
+
  function changeCurrentPlayer(gameState) {
      gameState.currentPlayer = 1 ^ gameState.currentPlayer;
  }

Now let's take a look at some examples of a "win" state

  1 -1  0
  0  1 -1
 -1 -1  1

Winner is 1. Sum of diagonal values equals 3

We can assume that we can detect a winner by getting a sum of each row, col and diagonal values and comparing it to a 0 (0 + 0 + 0) or 3 (1 + 1 + 1)

But here's another example

  0 -1  1
  1  0 -1
 -1 -1  0

A sum of 1st and 2nd row = 0

Sum of both diagonals = 0

Sum of 1st and 3d cols = 0

That's not the right way to go... 😞

💡 Easy fix! Change initial value of field to -3 😎

📄 src/index.js

  const GameState = {
      currentPlayer: 0,
-     field: Array.from({ length: 9 }).fill(-1),
+     field: Array.from({ length: 9 }).fill(-3),
  }

  const Rows = [

Ok, now we are good. So let's create a simple sum function

📄 src/index.js

      gameState.field[index] = gameState.currentPlayer;
      changeCurrentPlayer(gameState);
  }
+
+ function sum(arr) {
+     return arr.reduce((a, b) => a + b, 0);
+ }

and a helper function which maps field indices to values

📄 src/index.js

  function sum(arr) {
      return arr.reduce((a, b) => a + b, 0);
  }
+
+ function getValues(gameState, indices) {
+     return indices.map(index => gameState.field[index]);
+ }

function getWinner should find if some row, col or diagonal sum is 0 or 3. Let's get values of all rows

📄 src/index.js

  function getValues(gameState, indices) {
      return indices.map(index => gameState.field[index]);
  }
+
+ function getWinner(gameState) {
+     const rows = Rows.map((row) => getValues(gameState, row));
+ }

and do the same for cols and diagonals

📄 src/index.js


  function getWinner(gameState) {
      const rows = Rows.map((row) => getValues(gameState, row));
+     const cols = Cols.map((col) => getValues(gameState, col));
+     const diagonals = Diagonals.map((col) => getValues(gameState, col));
  }

now let's create a single array of all values in field

📄 src/index.js

      const rows = Rows.map((row) => getValues(gameState, row));
      const cols = Cols.map((col) => getValues(gameState, col));
      const diagonals = Diagonals.map((col) => getValues(gameState, col));
+
+     const values = [...rows, ...cols, ...diagonals];
  }

and find if some chunk sum equals 0 or 3

📄 src/index.js

      const diagonals = Diagonals.map((col) => getValues(gameState, col));

      const values = [...rows, ...cols, ...diagonals];
+
+     let winner = -1;
+
+     values.forEach((chunk) => {
+         const chunkSum = sum(chunk);
+
+         if (chunkSum === 0) {
+             winner = 0;
+             return;
+         }
+
+         if (chunkSum === 3) {
+             winner = 1;
+             return;
+         }
+     });
+
+     return winner;
  }

Game loop

Now let's describe a game loop. We'll create a generator function to query row and col for each next turn from outside world. If you are not familliar with generator functions – read this medium post

📄 src/index.js


      return winner;
  }
+
+ function* gameLoop(gameState) {
+
+ }

Generator should execute until getWinner returns anything but -1.

📄 src/index.js

  }

  function* gameLoop(gameState) {
+     let winner = -1;
+
+     while (winner < 0) {

+         winner = getWinner(gameState);
+     }
  }

it should also make a turn befor each getWinner call

📄 src/index.js

      let winner = -1;

      while (winner < 0) {
+         const [rowIndex, colIndex] = yield;
+         turn(gameState, rowIndex, colIndex);

          winner = getWinner(gameState);
      }

Now let's test our gameLoop

Create a mock scenario of a game:

📄 src/index.js

          winner = getWinner(gameState);
      }
  }
+
+ const turns = [
+     [1, 1],
+     [0, 1],
+     [0, 0],
+     [1, 2],
+     [2, 2],
+ ];

Create a game generator object

📄 src/index.js

      [1, 2],
      [2, 2],
  ];
+
+ const game = gameLoop(GameState);
+ game.next();

Iterate over game turns and pass each turn to generator

📄 src/index.js


  const game = gameLoop(GameState);
  game.next();
+
+ turns.forEach(turn => game.next(turn));

After execution of this scenario game generator should finish it execution. This means that leading .next() call should return an object { value: undefined, done: true }

📄 src/index.js

  game.next();

  turns.forEach(turn => game.next(turn));
+
+ console.log(game.next());

Let's check it with node.js

node src/index.js
{ value: undefined, done: true }

Yay, it works!

Refactor time

Now as a core of a game is ready let's start refactor our index.js and split it in several modules

Drop testing code

📄 src/index.js

      }
  }

- const turns = [
-     [1, 1],
-     [0, 1],
-     [0, 0],
-     [1, 2],
-     [2, 2],
- ];
-
  const game = gameLoop(GameState);
  game.next();
-
- turns.forEach(turn => game.next(turn));
-
- console.log(game.next());

Move everything but gameLoop from index.js to game-state.js.

📄 src/game-state.js

const GameState = {
    currentPlayer: 0,
    field: Array.from({ length: 9 }).fill(-3),
}

const Rows = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
];

const Cols = [
    [0, 3, 6],
    [1, 4, 7],
    [6, 7, 8],
];

const Diagonals = [
    [0, 4, 8],
    [2, 4, 6],
];

function changeCurrentPlayer(gameState) {
    gameState.currentPlayer = 1 ^ gameState.currentPlayer;
}

function getArrayIndexFromRowAndCol(rowIndex, colIndex) {
    return rowIndex * 3 + colIndex;
}

function turn(gameState, rowIndex, colIndex) {
    const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);
    const fieldValue = gameState.field[index];

    if (fieldValue >= 0) {
        return;
    }

    gameState.field[index] = gameState.currentPlayer;
    changeCurrentPlayer(gameState);
}

function sum(arr) {
    return arr.reduce((a, b) => a + b, 0);
}

function getValues(gameState, indices) {
    return indices.map(index => gameState.field[index]);
}

function getWinner(gameState) {
    const rows = Rows.map((row) => getValues(gameState, row));
    const cols = Cols.map((col) => getValues(gameState, col));
    const diagonals = Diagonals.map((col) => getValues(gameState, col));

    const values = [...rows, ...cols, ...diagonals];

    let winner = -1;

    values.forEach((chunk) => {
        const chunkSum = sum(chunk);

        if (chunkSum === 0) {
            winner = 0;
            return;
        }

        if (chunkSum === 3) {
            winner = 1;
            return;
        }
    });

    return winner;
}

📄 src/index.js

- const GameState = {
-     currentPlayer: 0,
-     field: Array.from({ length: 9 }).fill(-3),
- }
-
- const Rows = [
-     [0, 1, 2],
-     [3, 4, 5],
-     [6, 7, 8],
- ];
-
- const Cols = [
-     [0, 3, 6],
-     [1, 4, 7],
-     [6, 7, 8],
- ];
-
- const Diagonals = [
-     [0, 4, 8],
-     [2, 4, 6],
- ];
-
- function changeCurrentPlayer(gameState) {
-     gameState.currentPlayer = 1 ^ gameState.currentPlayer;
- }
-
- function getArrayIndexFromRowAndCol(rowIndex, colIndex) {
-     return rowIndex * 3 + colIndex;
- }
-
- function turn(gameState, rowIndex, colIndex) {
-     const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);
-     const fieldValue = GameState.field[index];
-
-     if (fieldValue >= 0) {
-         return;
-     }
-
-     gameState.field[index] = gameState.currentPlayer;
-     changeCurrentPlayer(gameState);
- }
-
- function sum(arr) {
-     return arr.reduce((a, b) => a + b, 0);
- }
-
- function getValues(gameState, indices) {
-     return indices.map(index => gameState.field[index]);
- }
-
- function getWinner(gameState) {
-     const rows = Rows.map((row) => getValues(gameState, row));
-     const cols = Cols.map((col) => getValues(gameState, col));
-     const diagonals = Diagonals.map((col) => getValues(gameState, col));
-
-     const values = [...rows, ...cols, ...diagonals];
-
-     let winner = -1;
-
-     values.forEach((chunk) => {
-         const chunkSum = sum(chunk);
-
-         if (chunkSum === 0) {
-             winner = 0;
-             return;
-         }
-
-         if (chunkSum === 3) {
-             winner = 1;
-             return;
-         }
-     });
-
-     return winner;
- }
-
  function* gameLoop(gameState) {
      let winner = -1;


Export everything gameLoop depends on

📄 src/game-state.js

- const GameState = {
+ export const GameState = {
      currentPlayer: 0,
      field: Array.from({ length: 9 }).fill(-3),
  }
      return rowIndex * 3 + colIndex;
  }

- function turn(gameState, rowIndex, colIndex) {
+ export function turn(gameState, rowIndex, colIndex) {
      const index = getArrayIndexFromRowAndCol(rowIndex, colIndex);
      const fieldValue = gameState.field[index];

      return indices.map(index => gameState.field[index]);
  }

- function getWinner(gameState) {
+ export function getWinner(gameState) {
      const rows = Rows.map((row) => getValues(gameState, row));
      const cols = Cols.map((col) => getValues(gameState, col));
      const diagonals = Diagonals.map((col) => getValues(gameState, col));

and import it in index.js

📄 src/index.js

+ import { GameState, getWinner, turn } from './game-state.js';
+
  function* gameLoop(gameState) {
      let winner = -1;


Rendering game state on canvas

Add canvas to index.html

📄 index.html

  </head>
  <body>
      <script src="./src/index.js" type="module"></script>
+     <canvas></canvas>
  </body>
  </html>

and get a reference to canvas with querySelector

📄 src/index.js


  const game = gameLoop(GameState);
  game.next();
+
+ const canvas = document.querySelector('canvas');

Let's make body full-height

📄 index.html

      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Tic Tac Toe</title>
+     <style>
+     html, body {
+         height: 100%;
+     }
+     </style>
  </head>
  <body>
      <script src="./src/index.js" type="module"></script>

and reset default margins

📄 index.html

      html, body {
          height: 100%;
      }
+
+     body {
+         margin: 0;
+     }
      </style>
  </head>
  <body>

Setup canvas size

📄 src/index.js

  game.next();

  const canvas = document.querySelector('canvas');
+
+ const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);
+ canvas.width = size;
+ canvas.height = size;

and get a 2d context

📄 src/index.js

  const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);
  canvas.width = size;
  canvas.height = size;
+
+ const ctx = canvas.getContext('2d');

Move canvas setup code to separate file

📄 src/canvas-setup.js

export function setupCanvas() {
    const canvas = document.querySelector('canvas');

    const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);
    canvas.width = size;
    canvas.height = size;

    const ctx = canvas.getContext('2d');

    return { canvas, ctx };
}

📄 src/index.js


  const game = gameLoop(GameState);
  game.next();
-
- const canvas = document.querySelector('canvas');
-
- const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);
- canvas.width = size;
- canvas.height = size;
-
- const ctx = canvas.getContext('2d');

and import it to index.js

📄 src/index.js

  import { GameState, getWinner, turn } from './game-state.js';
+ import { setupCanvas } from './canvas-setup.js';

  function* gameLoop(gameState) {
      let winner = -1;

  const game = gameLoop(GameState);
  game.next();
+
+ const { canvas, ctx } = setupCanvas();

Now let's create render function which will visualize the game state

📄 src/renderer.js

/**
 * @typedef GameState
 * @property {Number} currentPlayer
 * @property {Array<number>} field
 *
 * @param {HTMLCanvasElement} canvas
 * @param {CanvasRenderingContext2D} ctx
 * @param {GameState} gameState
 */
export function draw(canvas, ctx, gameState) {

}

We'll need to clear the whole canvas on each render call

📄 src/renderer.js

   * @param {GameState} gameState
   */
  export function draw(canvas, ctx, gameState) {
-
+     ctx.clearRect(0, 0, canvas.width, canvas.height);
  }

We'll render each cell with strokeRect, so let's setup cellSize (width and height of each game field cell) and lineWidth (border width of each cell)

📄 src/renderer.js

   */
  export function draw(canvas, ctx, gameState) {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+     ctx.lineWidth = 10;
+     const cellSize = canvas.width / 3;
+
  }

And finally we rendered smth! 🎉

📄 src/renderer.js

      ctx.lineWidth = 10;
      const cellSize = canvas.width / 3;

+     gameState.field.forEach((_, index) => {
+         const top = Math.floor(index / 3) * cellSize;
+         const left = index % 3 * cellSize;
+
+         ctx.strokeRect(top, left, cellSize, cellSize);
+     });
  }

To see the result install live-server

npm i -g live-server
live-server .

Wait, what? Nothing rendered 😢 That's because we forgot to import and call draw function

📄 src/index.js

  import { GameState, getWinner, turn } from './game-state.js';
  import { setupCanvas } from './canvas-setup.js';
+ import { draw } from './renderer.js';

  function* gameLoop(gameState) {
      let winner = -1;
  game.next();

  const { canvas, ctx } = setupCanvas();
+ draw(canvas, ctx, GameState);

Let's make canvas a bit smaller to leave some space for other UI

📄 src/canvas-setup.js

  export function setupCanvas() {
      const canvas = document.querySelector('canvas');

-     const size = Math.min(document.body.offsetHeight, document.body.offsetWidth);
+     const size = Math.min(document.body.offsetHeight, document.body.offsetWidth) * 0.8;
      canvas.width = size;
      canvas.height = size;


and add a css border to make all cell edges look the same

📄 index.html

      body {
          margin: 0;
      }
+
+     canvas {
+         border: 5px solid black;
+     }
      </style>
  </head>
  <body>

It also looks weird in top-left corner, so align canvas to center with flex-box

📄 index.html

      <style>
      html, body {
          height: 100%;
+         display: flex;
+         align-items: center;
+         justify-content: center;
      }

      body {

So, we've rendered game field cells. Now let's render X and O symbols

📄 src/renderer.js

          ctx.strokeRect(top, left, cellSize, cellSize);
      });
  }
+
+ /**
+  * @param {CanvasRenderingContext2D} ctx
+  */
+ function drawX(ctx, top, left, size) {
+
+ }

We'll use path to render symbol both for X and O

📄 src/renderer.js

   * @param {CanvasRenderingContext2D} ctx
   */
  function drawX(ctx, top, left, size) {
+     ctx.beginPath();
+
+     ctx.closePath();
+     ctx.stroke();

  }

Draw a line from top-left to bottom-right

📄 src/renderer.js

  function drawX(ctx, top, left, size) {
      ctx.beginPath();

+     ctx.moveTo(left, top);
+     ctx.lineTo(left + size, top + size);
+
      ctx.closePath();
      ctx.stroke();


Draw a line from top-right to bottom-left

📄 src/renderer.js

      ctx.moveTo(left, top);
      ctx.lineTo(left + size, top + size);

+     ctx.moveTo(left + size, top);
+     ctx.lineTo(left, top + size);
+
      ctx.closePath();
      ctx.stroke();


Rendering O is even more simple

📄 src/renderer.js


      ctx.closePath();
      ctx.stroke();
+ }

+ /**
+  * @param {CanvasRenderingContext2D} ctx
+  */
+ function drawO(ctx, centerX, centerY, radius) {
+     ctx.beginPath();
+
+     ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
+     ctx.closePath();
+
+     ctx.stroke();
  }

And let's actually render X or O depending on a field value

📄 src/renderer.js

      ctx.lineWidth = 10;
      const cellSize = canvas.width / 3;

-     gameState.field.forEach((_, index) => {
+     gameState.field.forEach((value, index) => {
          const top = Math.floor(index / 3) * cellSize;
          const left = index % 3 * cellSize;

          ctx.strokeRect(top, left, cellSize, cellSize);
+
+         if (value < 0) {
+             return;
+         }
+
+         if (value === 0) {
+             drawX(ctx, top, left, cellSize);
+         } else {
+             drawO(ctx, left + cellSize / 2, top + cellSize / 2, cellSize / 2);
+         }
      });
  }


Nothing rendered? That's correct, every field value is -2, so let's make some turns

📄 src/index.js

  game.next();

  const { canvas, ctx } = setupCanvas();
+
+ turn(GameState, 0, 1);
+ turn(GameState, 1, 1);
+ turn(GameState, 2, 0);
+
  draw(canvas, ctx, GameState);

📄 src/renderer.js

          }

          if (value === 0) {
-             drawX(ctx, top, left, cellSize);
+             const margin = cellSize * 0.2;
+             const size = cellSize * 0.6;
+
+             drawX(ctx, top + margin, left + margin, size);
          } else {
-             drawO(ctx, left + cellSize / 2, top + cellSize / 2, cellSize / 2);
+             const radius = cellSize * 0.3;
+             drawO(ctx, left + cellSize / 2, top + cellSize / 2, radius);
          }
      });
  }

Interactions

Everything seems to be done, the only thing left – interactions. Let's start with cleanup:

📄 src/index.js


  const { canvas, ctx } = setupCanvas();

- turn(GameState, 0, 1);
- turn(GameState, 1, 1);
- turn(GameState, 2, 0);
-
  draw(canvas, ctx, GameState);

Add click listener and calculate clicked row and col

📄 src/index.js

  const { canvas, ctx } = setupCanvas();

  draw(canvas, ctx, GameState);
+
+ canvas.addEventListener('click', ({ layerX, layerY }) => {
+     const row = Math.floor(layerY / canvas.height * 100 / 33);
+     const col = Math.floor(layerX / canvas.width * 100 / 33);
+ });

Pass row and col indices to game loop generator

📄 src/index.js

  canvas.addEventListener('click', ({ layerX, layerY }) => {
      const row = Math.floor(layerY / canvas.height * 100 / 33);
      const col = Math.floor(layerX / canvas.width * 100 / 33);
+
+     game.next([row, col]);
  });

and reflect game state changes on canvas

📄 src/index.js

      const col = Math.floor(layerX / canvas.width * 100 / 33);

      game.next([row, col]);
+     draw(canvas, ctx, GameState);
  });

Now let's congratulate a winner

📄 src/index.js


          winner = getWinner(gameState);
      }
+
+     setTimeout(() => {
+         alert(`Congratulations, ${['X', 'O'][winner]}! You won!`);
+     });
  }

  const game = gameLoop(GameState);

Oh, we forgot to handle a draw! No worries. Let's add isGameFinished helper:

📄 src/game-state.js


      return winner;
  }
+
+ export function isGameFinished(gameState) {
+     return gameState.field.every(f => f >= 0);
+ }

and call it on each iteration of a game loop

📄 src/index.js

- import { GameState, getWinner, turn } from './game-state.js';
+ import { GameState, getWinner, turn, isGameFinished } from './game-state.js';
  import { setupCanvas } from './canvas-setup.js';
  import { draw } from './renderer.js';

  function* gameLoop(gameState) {
      let winner = -1;

-     while (winner < 0) {
+     while (winner < 0 && !isGameFinished(gameState)) {
          const [rowIndex, colIndex] = yield;
          turn(gameState, rowIndex, colIndex);

      }

      setTimeout(() => {
-         alert(`Congratulations, ${['X', 'O'][winner]}! You won!`);
+         if (winner < 0) {
+             alert(`It's a draw`);
+         } else {
+             alert(`Congratulations, ${['X', 'O'][winner]}! You won!`);
+         }
      });
  }


LICENSE

WTFPL